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 MiniMax,
334 Moonshot,
335 XAi,
336 LlmSim,
337}
338
339impl ModelVendor {
340 pub fn slug(&self) -> &'static str {
344 match self {
345 ModelVendor::OpenAi => "openai",
346 ModelVendor::Anthropic => "anthropic",
347 ModelVendor::Google => "google",
348 ModelVendor::Nvidia => "nvidia",
349 ModelVendor::Qwen => "qwen",
350 ModelVendor::Microsoft => "microsoft",
351 ModelVendor::MiniMax => "minimax",
352 ModelVendor::Moonshot => "moonshot",
353 ModelVendor::XAi => "xai",
354 ModelVendor::LlmSim => "llmsim",
355 }
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
368#[cfg_attr(feature = "openapi", derive(ToSchema))]
369pub struct ModelProfile {
370 pub name: String,
372 pub family: String,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub description: Option<String>,
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub release_date: Option<String>,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub last_updated: Option<String>,
383 pub attachment: bool,
385 pub reasoning: bool,
387 pub temperature: bool,
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub knowledge: Option<String>,
392 pub tool_call: bool,
394 pub structured_output: bool,
396 pub open_weights: bool,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub cost: Option<ModelCost>,
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub limits: Option<ModelLimits>,
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub modalities: Option<ModelModalities>,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub reasoning_effort: Option<ReasoningEffortConfig>,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub speed: Option<SpeedConfig>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub verbosity: Option<VerbosityConfig>,
418 #[serde(default)]
422 pub tool_search: bool,
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
425 pub supported_parameters: Vec<String>,
426 #[serde(default)]
430 pub supports_phases: bool,
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_provider_type_serialization() {
439 assert_eq!(
441 serde_json::to_string(&DriverId::OpenAI).unwrap(),
442 "\"openai\""
443 );
444 assert_eq!(
445 serde_json::to_string(&DriverId::OpenRouter).unwrap(),
446 "\"openrouter\""
447 );
448 assert_eq!(
449 serde_json::to_string(&DriverId::OpenAICompletions).unwrap(),
450 "\"openai_completions\""
451 );
452 assert_eq!(
453 serde_json::to_string(&DriverId::AzureOpenAI).unwrap(),
454 "\"azure_openai\""
455 );
456 assert_eq!(
457 serde_json::to_string(&DriverId::Anthropic).unwrap(),
458 "\"anthropic\""
459 );
460 assert_eq!(
461 serde_json::to_string(&DriverId::Gemini).unwrap(),
462 "\"gemini\""
463 );
464 assert_eq!(
465 serde_json::to_string(&DriverId::LlmSim).unwrap(),
466 "\"llmsim\""
467 );
468 }
469
470 #[test]
471 fn test_provider_type_deserialization() {
472 assert!(matches!(
474 serde_json::from_str::<DriverId>("\"openai\"").unwrap(),
475 DriverId::OpenAI
476 ));
477 assert!(matches!(
478 serde_json::from_str::<DriverId>("\"openrouter\"").unwrap(),
479 DriverId::OpenRouter
480 ));
481 assert!(matches!(
482 serde_json::from_str::<DriverId>("\"openai_completions\"").unwrap(),
483 DriverId::OpenAICompletions
484 ));
485 assert!(matches!(
486 serde_json::from_str::<DriverId>("\"azure_openai\"").unwrap(),
487 DriverId::AzureOpenAI
488 ));
489 assert!(matches!(
490 serde_json::from_str::<DriverId>("\"anthropic\"").unwrap(),
491 DriverId::Anthropic
492 ));
493 assert!(matches!(
494 serde_json::from_str::<DriverId>("\"gemini\"").unwrap(),
495 DriverId::Gemini
496 ));
497 assert!(matches!(
498 serde_json::from_str::<DriverId>("\"llmsim\"").unwrap(),
499 DriverId::LlmSim
500 ));
501 }
502
503 #[test]
504 fn test_provider_type_from_str() {
505 assert!(matches!(
507 "openai".parse::<DriverId>().unwrap(),
508 DriverId::OpenAI
509 ));
510 assert!(matches!(
511 "openrouter".parse::<DriverId>().unwrap(),
512 DriverId::OpenRouter
513 ));
514 assert!(matches!(
515 "openai_completions".parse::<DriverId>().unwrap(),
516 DriverId::OpenAICompletions
517 ));
518 assert!(matches!(
519 "azure_openai".parse::<DriverId>().unwrap(),
520 DriverId::AzureOpenAI
521 ));
522 assert!(matches!(
523 "anthropic".parse::<DriverId>().unwrap(),
524 DriverId::Anthropic
525 ));
526 assert!(matches!(
527 "gemini".parse::<DriverId>().unwrap(),
528 DriverId::Gemini
529 ));
530 assert!(matches!(
531 "llmsim".parse::<DriverId>().unwrap(),
532 DriverId::LlmSim
533 ));
534 }
535
536 #[test]
537 fn test_model_limits_input_omitted_when_none() {
538 let limits = ModelLimits {
539 context: 200_000,
540 input: None,
541 output: 64_000,
542 max_media: None,
543 };
544 let json = serde_json::to_value(&limits).unwrap();
545 assert!(!json.as_object().unwrap().contains_key("input"));
546 }
547
548 #[test]
549 fn test_model_limits_input_included_when_some() {
550 let limits = ModelLimits {
551 context: 200_000,
552 input: Some(150_000),
553 output: 64_000,
554 max_media: None,
555 };
556 let json = serde_json::to_value(&limits).unwrap();
557 assert_eq!(json["input"], 150_000);
558 }
559
560 #[test]
561 fn test_model_limits_deserialize_without_input() {
562 let json = r#"{"context": 200000, "output": 64000}"#;
563 let limits: ModelLimits = serde_json::from_str(json).unwrap();
564 assert_eq!(limits.context, 200_000);
565 assert!(limits.input.is_none());
566 assert_eq!(limits.output, 64_000);
567 }
568
569 #[test]
570 fn test_provider_type_display() {
571 assert_eq!(DriverId::OpenAI.to_string(), "openai");
573 assert_eq!(DriverId::OpenRouter.to_string(), "openrouter");
574 assert_eq!(DriverId::AzureOpenAI.to_string(), "azure_openai");
575 assert_eq!(
576 DriverId::OpenAICompletions.to_string(),
577 "openai_completions"
578 );
579 assert_eq!(DriverId::Anthropic.to_string(), "anthropic");
580 assert_eq!(DriverId::Gemini.to_string(), "gemini");
581 assert_eq!(DriverId::LlmSim.to_string(), "llmsim");
582 }
583}