cellstate_core/
models.rs

1//! Model discovery types.
2//!
3//! Universal model information shared between providers. These types are
4//! provider-agnostic — every provider's model list is normalized into the
5//! same `ModelInfo` shape regardless of how the upstream API returns data.
6
7use serde::{Deserialize, Serialize};
8
9/// Universal model info — same shape regardless of provider.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13pub struct ModelInfo {
14    /// Model identifier (e.g. "anthropic/claude-sonnet-4-20250514")
15    pub id: String,
16    /// Human-readable name (e.g. "Claude Sonnet 4")
17    pub name: String,
18    /// Provider that serves this model (e.g. "anthropic")
19    pub provider: String,
20    /// Maximum input context length in tokens
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub context_length: Option<u64>,
23    /// Maximum output tokens per request
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub max_output: Option<u64>,
26    /// Model capabilities (e.g. ["chat", "vision", "tool_use", "json_mode"])
27    #[serde(default)]
28    pub capabilities: Vec<String>,
29    /// Pricing information (per million tokens)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub pricing: Option<ModelPricing>,
32}
33
34/// Pricing information for a model.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
38pub struct ModelPricing {
39    /// Cost per million input tokens (USD)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub input_per_million: Option<f64>,
42    /// Cost per million output tokens (USD)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub output_per_million: Option<f64>,
45}
46
47/// Summary information about a configured provider.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51pub struct ProviderInfo {
52    /// Provider identifier (e.g. "openrouter")
53    pub name: String,
54    /// Human-readable name (e.g. "OpenRouter")
55    pub display_name: String,
56    /// Whether this provider has credentials configured
57    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}