1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13pub struct ModelInfo {
14 pub id: String,
16 pub name: String,
18 pub provider: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub context_length: Option<u64>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub max_output: Option<u64>,
26 #[serde(default)]
28 pub capabilities: Vec<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub pricing: Option<ModelPricing>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
38pub struct ModelPricing {
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub input_per_million: Option<f64>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub output_per_million: Option<f64>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51pub struct ProviderInfo {
52 pub name: String,
54 pub display_name: String,
56 pub configured: bool,
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn model_info_serde_roundtrip() {
66 let model = ModelInfo {
67 id: "anthropic/claude-sonnet-4-20250514".into(),
68 name: "Claude Sonnet 4".into(),
69 provider: "anthropic".into(),
70 context_length: Some(200000),
71 max_output: Some(8192),
72 capabilities: vec!["chat".into(), "vision".into(), "tool_use".into()],
73 pricing: Some(ModelPricing {
74 input_per_million: Some(3.0),
75 output_per_million: Some(15.0),
76 }),
77 };
78 let json = serde_json::to_string(&model).unwrap();
79 let d: ModelInfo = serde_json::from_str(&json).unwrap();
80 assert_eq!(model.id, d.id);
81 assert_eq!(d.capabilities.len(), 3);
82 assert_eq!(d.pricing.unwrap().input_per_million, Some(3.0));
83 }
84
85 #[test]
86 fn model_info_optional_fields_omitted() {
87 let model = ModelInfo {
88 id: "test/model".into(),
89 name: "Test".into(),
90 provider: "test".into(),
91 context_length: None,
92 max_output: None,
93 capabilities: vec![],
94 pricing: None,
95 };
96 let json = serde_json::to_string(&model).unwrap();
97 assert!(!json.contains("context_length"));
98 assert!(!json.contains("pricing"));
99 }
100
101 #[test]
102 fn provider_info_serde_roundtrip() {
103 let info = ProviderInfo {
104 name: "openrouter".into(),
105 display_name: "OpenRouter".into(),
106 configured: true,
107 };
108 let json = serde_json::to_string(&info).unwrap();
109 let d: ProviderInfo = serde_json::from_str(&json).unwrap();
110 assert_eq!(info.name, d.name);
111 assert!(d.configured);
112 }
113}