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, Copy, PartialEq, Eq, Serialize, Deserialize)]
290#[cfg_attr(feature = "openapi", derive(ToSchema))]
291#[serde(rename_all = "lowercase")]
292pub enum ModelVendor {
293 OpenAi,
294 Anthropic,
295 Google,
296 Nvidia,
297 Qwen,
298 Microsoft,
299 MiniMax,
300 Moonshot,
301 XAi,
302 LlmSim,
303}
304
305impl ModelVendor {
306 pub fn slug(&self) -> &'static str {
310 match self {
311 ModelVendor::OpenAi => "openai",
312 ModelVendor::Anthropic => "anthropic",
313 ModelVendor::Google => "google",
314 ModelVendor::Nvidia => "nvidia",
315 ModelVendor::Qwen => "qwen",
316 ModelVendor::Microsoft => "microsoft",
317 ModelVendor::MiniMax => "minimax",
318 ModelVendor::Moonshot => "moonshot",
319 ModelVendor::XAi => "xai",
320 ModelVendor::LlmSim => "llmsim",
321 }
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
334#[cfg_attr(feature = "openapi", derive(ToSchema))]
335pub struct ModelProfile {
336 pub name: String,
338 pub family: String,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub description: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub release_date: Option<String>,
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub last_updated: Option<String>,
349 pub attachment: bool,
351 pub reasoning: bool,
353 pub temperature: bool,
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub knowledge: Option<String>,
358 pub tool_call: bool,
360 pub structured_output: bool,
362 pub open_weights: bool,
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub cost: Option<ModelCost>,
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub limits: Option<ModelLimits>,
370 #[serde(skip_serializing_if = "Option::is_none")]
372 pub modalities: Option<ModelModalities>,
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub reasoning_effort: Option<ReasoningEffortConfig>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub speed: Option<SpeedConfig>,
380 #[serde(default)]
384 pub tool_search: bool,
385 #[serde(default, skip_serializing_if = "Vec::is_empty")]
387 pub supported_parameters: Vec<String>,
388 #[serde(default)]
392 pub supports_phases: bool,
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_provider_type_serialization() {
401 assert_eq!(
403 serde_json::to_string(&DriverId::OpenAI).unwrap(),
404 "\"openai\""
405 );
406 assert_eq!(
407 serde_json::to_string(&DriverId::OpenRouter).unwrap(),
408 "\"openrouter\""
409 );
410 assert_eq!(
411 serde_json::to_string(&DriverId::OpenAICompletions).unwrap(),
412 "\"openai_completions\""
413 );
414 assert_eq!(
415 serde_json::to_string(&DriverId::AzureOpenAI).unwrap(),
416 "\"azure_openai\""
417 );
418 assert_eq!(
419 serde_json::to_string(&DriverId::Anthropic).unwrap(),
420 "\"anthropic\""
421 );
422 assert_eq!(
423 serde_json::to_string(&DriverId::Gemini).unwrap(),
424 "\"gemini\""
425 );
426 assert_eq!(
427 serde_json::to_string(&DriverId::LlmSim).unwrap(),
428 "\"llmsim\""
429 );
430 }
431
432 #[test]
433 fn test_provider_type_deserialization() {
434 assert!(matches!(
436 serde_json::from_str::<DriverId>("\"openai\"").unwrap(),
437 DriverId::OpenAI
438 ));
439 assert!(matches!(
440 serde_json::from_str::<DriverId>("\"openrouter\"").unwrap(),
441 DriverId::OpenRouter
442 ));
443 assert!(matches!(
444 serde_json::from_str::<DriverId>("\"openai_completions\"").unwrap(),
445 DriverId::OpenAICompletions
446 ));
447 assert!(matches!(
448 serde_json::from_str::<DriverId>("\"azure_openai\"").unwrap(),
449 DriverId::AzureOpenAI
450 ));
451 assert!(matches!(
452 serde_json::from_str::<DriverId>("\"anthropic\"").unwrap(),
453 DriverId::Anthropic
454 ));
455 assert!(matches!(
456 serde_json::from_str::<DriverId>("\"gemini\"").unwrap(),
457 DriverId::Gemini
458 ));
459 assert!(matches!(
460 serde_json::from_str::<DriverId>("\"llmsim\"").unwrap(),
461 DriverId::LlmSim
462 ));
463 }
464
465 #[test]
466 fn test_provider_type_from_str() {
467 assert!(matches!(
469 "openai".parse::<DriverId>().unwrap(),
470 DriverId::OpenAI
471 ));
472 assert!(matches!(
473 "openrouter".parse::<DriverId>().unwrap(),
474 DriverId::OpenRouter
475 ));
476 assert!(matches!(
477 "openai_completions".parse::<DriverId>().unwrap(),
478 DriverId::OpenAICompletions
479 ));
480 assert!(matches!(
481 "azure_openai".parse::<DriverId>().unwrap(),
482 DriverId::AzureOpenAI
483 ));
484 assert!(matches!(
485 "anthropic".parse::<DriverId>().unwrap(),
486 DriverId::Anthropic
487 ));
488 assert!(matches!(
489 "gemini".parse::<DriverId>().unwrap(),
490 DriverId::Gemini
491 ));
492 assert!(matches!(
493 "llmsim".parse::<DriverId>().unwrap(),
494 DriverId::LlmSim
495 ));
496 }
497
498 #[test]
499 fn test_model_limits_input_omitted_when_none() {
500 let limits = ModelLimits {
501 context: 200_000,
502 input: None,
503 output: 64_000,
504 max_media: None,
505 };
506 let json = serde_json::to_value(&limits).unwrap();
507 assert!(!json.as_object().unwrap().contains_key("input"));
508 }
509
510 #[test]
511 fn test_model_limits_input_included_when_some() {
512 let limits = ModelLimits {
513 context: 200_000,
514 input: Some(150_000),
515 output: 64_000,
516 max_media: None,
517 };
518 let json = serde_json::to_value(&limits).unwrap();
519 assert_eq!(json["input"], 150_000);
520 }
521
522 #[test]
523 fn test_model_limits_deserialize_without_input() {
524 let json = r#"{"context": 200000, "output": 64000}"#;
525 let limits: ModelLimits = serde_json::from_str(json).unwrap();
526 assert_eq!(limits.context, 200_000);
527 assert!(limits.input.is_none());
528 assert_eq!(limits.output, 64_000);
529 }
530
531 #[test]
532 fn test_provider_type_display() {
533 assert_eq!(DriverId::OpenAI.to_string(), "openai");
535 assert_eq!(DriverId::OpenRouter.to_string(), "openrouter");
536 assert_eq!(DriverId::AzureOpenAI.to_string(), "azure_openai");
537 assert_eq!(
538 DriverId::OpenAICompletions.to_string(),
539 "openai_completions"
540 );
541 assert_eq!(DriverId::Anthropic.to_string(), "anthropic");
542 assert_eq!(DriverId::Gemini.to_string(), "gemini");
543 assert_eq!(DriverId::LlmSim.to_string(), "llmsim");
544 }
545}