1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10pub use crate::provider::{DriverId, ProviderStatus};
11use crate::typed_id::{ModelId, ProviderId};
12
13#[cfg(feature = "openapi")]
14use utoipa::ToSchema;
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
24#[cfg_attr(feature = "openapi", derive(ToSchema))]
25#[cfg_attr(feature = "openapi", schema(example = "predefined"))]
26#[serde(rename_all = "snake_case")]
27pub enum ModelSource {
28 #[default]
30 Manual,
31 Discovered,
33 Predefined,
35}
36
37impl std::fmt::Display for ModelSource {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 ModelSource::Manual => write!(f, "manual"),
41 ModelSource::Discovered => write!(f, "discovered"),
42 ModelSource::Predefined => write!(f, "predefined"),
43 }
44 }
45}
46
47impl std::str::FromStr for ModelSource {
48 type Err = String;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 match s {
52 "manual" => Ok(ModelSource::Manual),
53 "discovered" => Ok(ModelSource::Discovered),
54 "predefined" => Ok(ModelSource::Predefined),
55 _ => Err(format!("Unknown model source: {}", s)),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62#[cfg_attr(feature = "openapi", derive(ToSchema))]
63pub struct Model {
64 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
66 pub id: ModelId,
67 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
69 pub provider_id: ProviderId,
70 pub model_id: String,
72 pub display_name: String,
74 pub capabilities: Vec<String>,
76 pub is_favorite: bool,
78 pub enabled: bool,
80 pub source: ModelSource,
82 pub created_at: DateTime<Utc>,
84 pub updated_at: DateTime<Utc>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[cfg_attr(feature = "openapi", derive(ToSchema))]
91pub struct ModelWithProvider {
92 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
94 pub id: ModelId,
95 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
97 pub provider_id: ProviderId,
98 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
100 pub model_id: String,
101 #[cfg_attr(feature = "openapi", schema(example = "Claude Sonnet 4.5"))]
103 pub display_name: String,
104 #[cfg_attr(feature = "openapi", schema(example = json!(["text", "tools", "vision", "thinking"])))]
106 pub capabilities: Vec<String>,
107 #[cfg_attr(feature = "openapi", schema(example = true))]
109 pub is_favorite: bool,
110 #[cfg_attr(feature = "openapi", schema(example = true))]
112 pub enabled: bool,
113 #[cfg_attr(feature = "openapi", schema(example = "predefined"))]
115 pub source: ModelSource,
116 #[cfg_attr(feature = "openapi", schema(example = "2026-01-04T11:23:00Z"))]
118 pub created_at: DateTime<Utc>,
119 #[cfg_attr(feature = "openapi", schema(example = "2026-05-27T15:24:00Z"))]
121 pub updated_at: DateTime<Utc>,
122 #[cfg_attr(feature = "openapi", schema(example = "Anthropic"))]
124 pub provider_name: String,
125 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
127 pub provider_type: DriverId,
128 #[cfg_attr(feature = "openapi", schema(example = true))]
132 pub healthy: bool,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub profile: Option<ModelProfile>,
136 #[serde(skip_serializing_if = "Option::is_none")]
139 pub model_vendor: Option<ModelVendor>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
149#[cfg_attr(feature = "openapi", derive(ToSchema))]
150pub struct ModelCost {
151 pub input: f64,
153 pub output: f64,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub cache_read: Option<f64>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub cost_tiers: Vec<CostTier>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
168#[cfg_attr(feature = "openapi", derive(ToSchema))]
169pub struct CostTier {
170 pub above_tokens: i32,
172 pub input: f64,
174 pub output: f64,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub cache_read: Option<f64>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[cfg_attr(feature = "openapi", derive(ToSchema))]
184pub struct ModelLimits {
185 pub context: i32,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub input: Option<i32>,
190 pub output: i32,
192 #[serde(skip_serializing_if = "Option::is_none", default)]
194 pub max_media: Option<i32>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
199#[cfg_attr(feature = "openapi", derive(ToSchema))]
200#[serde(rename_all = "snake_case")]
201pub enum Modality {
202 Text,
203 Image,
204 Audio,
205 Video,
206 Pdf,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[cfg_attr(feature = "openapi", derive(ToSchema))]
212pub struct ModelModalities {
213 pub input: Vec<Modality>,
215 pub output: Vec<Modality>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
221#[cfg_attr(feature = "openapi", derive(ToSchema))]
222#[serde(rename_all = "snake_case")]
223pub enum ReasoningEffort {
224 None,
225 Minimal,
226 Low,
227 Medium,
228 High,
229 Xhigh,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "openapi", derive(ToSchema))]
235pub struct ReasoningEffortValue {
236 pub value: ReasoningEffort,
238 pub name: String,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[cfg_attr(feature = "openapi", derive(ToSchema))]
245pub struct ReasoningEffortConfig {
246 pub values: Vec<ReasoningEffortValue>,
248 pub default: ReasoningEffort,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
258#[cfg_attr(feature = "openapi", derive(ToSchema))]
259#[serde(rename_all = "snake_case")]
260pub enum Speed {
261 Flex,
262 Default,
263 Priority,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268#[cfg_attr(feature = "openapi", derive(ToSchema))]
269pub struct SpeedValue {
270 pub value: Speed,
272 pub name: String,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[cfg_attr(feature = "openapi", derive(ToSchema))]
279pub struct SpeedConfig {
280 pub values: Vec<SpeedValue>,
282 pub default: Speed,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292#[cfg_attr(feature = "openapi", derive(ToSchema))]
293#[serde(rename_all = "snake_case")]
294pub enum Verbosity {
295 Low,
296 Medium,
297 High,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302#[cfg_attr(feature = "openapi", derive(ToSchema))]
303pub struct VerbosityValue {
304 pub value: Verbosity,
306 pub name: String,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312#[cfg_attr(feature = "openapi", derive(ToSchema))]
313pub struct VerbosityConfig {
314 pub values: Vec<VerbosityValue>,
316 pub default: Verbosity,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
324#[cfg_attr(feature = "openapi", derive(ToSchema))]
325#[serde(rename_all = "lowercase")]
326pub enum ModelVendor {
327 OpenAi,
328 Anthropic,
329 Google,
330 Nvidia,
331 Qwen,
332 Microsoft,
333 Meta,
334 MiniMax,
335 Moonshot,
336 XAi,
337 LlmSim,
338}
339
340impl ModelVendor {
341 pub fn slug(&self) -> &'static str {
345 match self {
346 ModelVendor::OpenAi => "openai",
347 ModelVendor::Anthropic => "anthropic",
348 ModelVendor::Google => "google",
349 ModelVendor::Nvidia => "nvidia",
350 ModelVendor::Qwen => "qwen",
351 ModelVendor::Microsoft => "microsoft",
352 ModelVendor::Meta => "meta",
353 ModelVendor::MiniMax => "minimax",
354 ModelVendor::Moonshot => "moonshot",
355 ModelVendor::XAi => "xai",
356 ModelVendor::LlmSim => "llmsim",
357 }
358 }
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
370#[cfg_attr(feature = "openapi", derive(ToSchema))]
371pub struct ModelProfile {
372 pub name: String,
374 pub family: String,
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub description: Option<String>,
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub release_date: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub last_updated: Option<String>,
385 pub attachment: bool,
387 pub reasoning: bool,
389 pub temperature: bool,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub knowledge: Option<String>,
394 pub tool_call: bool,
396 pub structured_output: bool,
398 pub open_weights: bool,
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub cost: Option<ModelCost>,
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub limits: Option<ModelLimits>,
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub modalities: Option<ModelModalities>,
409 #[serde(skip_serializing_if = "Option::is_none")]
411 pub reasoning_effort: Option<ReasoningEffortConfig>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub speed: Option<SpeedConfig>,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub verbosity: Option<VerbosityConfig>,
420 #[serde(default)]
424 pub tool_search: bool,
425 #[serde(default, skip_serializing_if = "Vec::is_empty")]
427 pub supported_parameters: Vec<String>,
428 #[serde(default)]
432 pub supports_phases: bool,
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_provider_type_serialization() {
441 assert_eq!(
443 serde_json::to_string(&DriverId::OpenAI).unwrap(),
444 "\"openai\""
445 );
446 assert_eq!(
447 serde_json::to_string(&DriverId::OpenRouter).unwrap(),
448 "\"openrouter\""
449 );
450 assert_eq!(
451 serde_json::to_string(&DriverId::OpenAICompletions).unwrap(),
452 "\"openai_completions\""
453 );
454 assert_eq!(
455 serde_json::to_string(&DriverId::AzureOpenAI).unwrap(),
456 "\"azure_openai\""
457 );
458 assert_eq!(
459 serde_json::to_string(&DriverId::Anthropic).unwrap(),
460 "\"anthropic\""
461 );
462 assert_eq!(
463 serde_json::to_string(&DriverId::Gemini).unwrap(),
464 "\"gemini\""
465 );
466 assert_eq!(
467 serde_json::to_string(&DriverId::LlmSim).unwrap(),
468 "\"llmsim\""
469 );
470 assert_eq!(serde_json::to_string(&DriverId::Meta).unwrap(), "\"meta\"");
471 }
472
473 #[test]
474 fn test_provider_type_deserialization() {
475 assert!(matches!(
477 serde_json::from_str::<DriverId>("\"openai\"").unwrap(),
478 DriverId::OpenAI
479 ));
480 assert!(matches!(
481 serde_json::from_str::<DriverId>("\"openrouter\"").unwrap(),
482 DriverId::OpenRouter
483 ));
484 assert!(matches!(
485 serde_json::from_str::<DriverId>("\"openai_completions\"").unwrap(),
486 DriverId::OpenAICompletions
487 ));
488 assert!(matches!(
489 serde_json::from_str::<DriverId>("\"azure_openai\"").unwrap(),
490 DriverId::AzureOpenAI
491 ));
492 assert!(matches!(
493 serde_json::from_str::<DriverId>("\"anthropic\"").unwrap(),
494 DriverId::Anthropic
495 ));
496 assert!(matches!(
497 serde_json::from_str::<DriverId>("\"gemini\"").unwrap(),
498 DriverId::Gemini
499 ));
500 assert!(matches!(
501 serde_json::from_str::<DriverId>("\"llmsim\"").unwrap(),
502 DriverId::LlmSim
503 ));
504 assert!(matches!(
505 serde_json::from_str::<DriverId>("\"meta\"").unwrap(),
506 DriverId::Meta
507 ));
508 }
509
510 #[test]
511 fn test_provider_type_from_str() {
512 assert!(matches!(
514 "openai".parse::<DriverId>().unwrap(),
515 DriverId::OpenAI
516 ));
517 assert!(matches!(
518 "openrouter".parse::<DriverId>().unwrap(),
519 DriverId::OpenRouter
520 ));
521 assert!(matches!(
522 "openai_completions".parse::<DriverId>().unwrap(),
523 DriverId::OpenAICompletions
524 ));
525 assert!(matches!(
526 "azure_openai".parse::<DriverId>().unwrap(),
527 DriverId::AzureOpenAI
528 ));
529 assert!(matches!(
530 "anthropic".parse::<DriverId>().unwrap(),
531 DriverId::Anthropic
532 ));
533 assert!(matches!(
534 "gemini".parse::<DriverId>().unwrap(),
535 DriverId::Gemini
536 ));
537 assert!(matches!(
538 "llmsim".parse::<DriverId>().unwrap(),
539 DriverId::LlmSim
540 ));
541 assert!(matches!(
542 "meta".parse::<DriverId>().unwrap(),
543 DriverId::Meta
544 ));
545 }
546
547 #[test]
548 fn test_model_limits_input_omitted_when_none() {
549 let limits = ModelLimits {
550 context: 200_000,
551 input: None,
552 output: 64_000,
553 max_media: None,
554 };
555 let json = serde_json::to_value(&limits).unwrap();
556 assert!(!json.as_object().unwrap().contains_key("input"));
557 }
558
559 #[test]
560 fn test_model_limits_input_included_when_some() {
561 let limits = ModelLimits {
562 context: 200_000,
563 input: Some(150_000),
564 output: 64_000,
565 max_media: None,
566 };
567 let json = serde_json::to_value(&limits).unwrap();
568 assert_eq!(json["input"], 150_000);
569 }
570
571 #[test]
572 fn test_model_limits_deserialize_without_input() {
573 let json = r#"{"context": 200000, "output": 64000}"#;
574 let limits: ModelLimits = serde_json::from_str(json).unwrap();
575 assert_eq!(limits.context, 200_000);
576 assert!(limits.input.is_none());
577 assert_eq!(limits.output, 64_000);
578 }
579
580 #[test]
581 fn test_provider_type_display() {
582 assert_eq!(DriverId::OpenAI.to_string(), "openai");
584 assert_eq!(DriverId::OpenRouter.to_string(), "openrouter");
585 assert_eq!(DriverId::AzureOpenAI.to_string(), "azure_openai");
586 assert_eq!(
587 DriverId::OpenAICompletions.to_string(),
588 "openai_completions"
589 );
590 assert_eq!(DriverId::Anthropic.to_string(), "anthropic");
591 assert_eq!(DriverId::Gemini.to_string(), "gemini");
592 assert_eq!(DriverId::LlmSim.to_string(), "llmsim");
593 assert_eq!(DriverId::Meta.to_string(), "meta");
594 }
595}