use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub use crate::provider::{DriverId, ProviderStatus};
use crate::typed_id::{ModelId, ProviderId};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "predefined"))]
#[serde(rename_all = "snake_case")]
pub enum ModelSource {
#[default]
Manual,
Discovered,
Predefined,
}
impl std::fmt::Display for ModelSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModelSource::Manual => write!(f, "manual"),
ModelSource::Discovered => write!(f, "discovered"),
ModelSource::Predefined => write!(f, "predefined"),
}
}
}
impl std::str::FromStr for ModelSource {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"manual" => Ok(ModelSource::Manual),
"discovered" => Ok(ModelSource::Discovered),
"predefined" => Ok(ModelSource::Predefined),
_ => Err(format!("Unknown model source: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct Model {
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
pub id: ModelId,
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
pub provider_id: ProviderId,
pub model_id: String,
pub display_name: String,
pub capabilities: Vec<String>,
pub is_favorite: bool,
pub enabled: bool,
pub source: ModelSource,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ModelWithProvider {
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000001"))]
pub id: ModelId,
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
pub provider_id: ProviderId,
#[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
pub model_id: String,
#[cfg_attr(feature = "openapi", schema(example = "Claude Sonnet 4.5"))]
pub display_name: String,
#[cfg_attr(feature = "openapi", schema(example = json!(["text", "tools", "vision", "thinking"])))]
pub capabilities: Vec<String>,
#[cfg_attr(feature = "openapi", schema(example = true))]
pub is_favorite: bool,
#[cfg_attr(feature = "openapi", schema(example = true))]
pub enabled: bool,
#[cfg_attr(feature = "openapi", schema(example = "predefined"))]
pub source: ModelSource,
#[cfg_attr(feature = "openapi", schema(example = "2026-01-04T11:23:00Z"))]
pub created_at: DateTime<Utc>,
#[cfg_attr(feature = "openapi", schema(example = "2026-05-27T15:24:00Z"))]
pub updated_at: DateTime<Utc>,
#[cfg_attr(feature = "openapi", schema(example = "Anthropic"))]
pub provider_name: String,
#[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
pub provider_type: DriverId,
#[cfg_attr(feature = "openapi", schema(example = true))]
pub healthy: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<ModelProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_vendor: Option<ModelVendor>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ModelCost {
pub input: f64,
pub output: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cost_tiers: Vec<CostTier>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct CostTier {
pub above_tokens: i32,
pub input: f64,
pub output: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ModelLimits {
pub context: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<i32>,
pub output: i32,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub max_media: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum Modality {
Text,
Image,
Audio,
Video,
Pdf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ModelModalities {
pub input: Vec<Modality>,
pub output: Vec<Modality>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffort {
None,
Minimal,
Low,
Medium,
High,
Xhigh,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ReasoningEffortValue {
pub value: ReasoningEffort,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ReasoningEffortConfig {
pub values: Vec<ReasoningEffortValue>,
pub default: ReasoningEffort,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum Speed {
Flex,
Default,
Priority,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct SpeedValue {
pub value: Speed,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct SpeedConfig {
pub values: Vec<SpeedValue>,
pub default: Speed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum Verbosity {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct VerbosityValue {
pub value: Verbosity,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct VerbosityConfig {
pub values: Vec<VerbosityValue>,
pub default: Verbosity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum ModelVendor {
OpenAi,
Anthropic,
Google,
Nvidia,
Qwen,
Microsoft,
MiniMax,
Moonshot,
XAi,
LlmSim,
}
impl ModelVendor {
pub fn slug(&self) -> &'static str {
match self {
ModelVendor::OpenAi => "openai",
ModelVendor::Anthropic => "anthropic",
ModelVendor::Google => "google",
ModelVendor::Nvidia => "nvidia",
ModelVendor::Qwen => "qwen",
ModelVendor::Microsoft => "microsoft",
ModelVendor::MiniMax => "minimax",
ModelVendor::Moonshot => "moonshot",
ModelVendor::XAi => "xai",
ModelVendor::LlmSim => "llmsim",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ModelProfile {
pub name: String,
pub family: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub release_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated: Option<String>,
pub attachment: bool,
pub reasoning: bool,
pub temperature: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub knowledge: Option<String>,
pub tool_call: bool,
pub structured_output: bool,
pub open_weights: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost: Option<ModelCost>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limits: Option<ModelLimits>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modalities: Option<ModelModalities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffortConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<SpeedConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verbosity: Option<VerbosityConfig>,
#[serde(default)]
pub tool_search: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_parameters: Vec<String>,
#[serde(default)]
pub supports_phases: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_type_serialization() {
assert_eq!(
serde_json::to_string(&DriverId::OpenAI).unwrap(),
"\"openai\""
);
assert_eq!(
serde_json::to_string(&DriverId::OpenRouter).unwrap(),
"\"openrouter\""
);
assert_eq!(
serde_json::to_string(&DriverId::OpenAICompletions).unwrap(),
"\"openai_completions\""
);
assert_eq!(
serde_json::to_string(&DriverId::AzureOpenAI).unwrap(),
"\"azure_openai\""
);
assert_eq!(
serde_json::to_string(&DriverId::Anthropic).unwrap(),
"\"anthropic\""
);
assert_eq!(
serde_json::to_string(&DriverId::Gemini).unwrap(),
"\"gemini\""
);
assert_eq!(
serde_json::to_string(&DriverId::LlmSim).unwrap(),
"\"llmsim\""
);
}
#[test]
fn test_provider_type_deserialization() {
assert!(matches!(
serde_json::from_str::<DriverId>("\"openai\"").unwrap(),
DriverId::OpenAI
));
assert!(matches!(
serde_json::from_str::<DriverId>("\"openrouter\"").unwrap(),
DriverId::OpenRouter
));
assert!(matches!(
serde_json::from_str::<DriverId>("\"openai_completions\"").unwrap(),
DriverId::OpenAICompletions
));
assert!(matches!(
serde_json::from_str::<DriverId>("\"azure_openai\"").unwrap(),
DriverId::AzureOpenAI
));
assert!(matches!(
serde_json::from_str::<DriverId>("\"anthropic\"").unwrap(),
DriverId::Anthropic
));
assert!(matches!(
serde_json::from_str::<DriverId>("\"gemini\"").unwrap(),
DriverId::Gemini
));
assert!(matches!(
serde_json::from_str::<DriverId>("\"llmsim\"").unwrap(),
DriverId::LlmSim
));
}
#[test]
fn test_provider_type_from_str() {
assert!(matches!(
"openai".parse::<DriverId>().unwrap(),
DriverId::OpenAI
));
assert!(matches!(
"openrouter".parse::<DriverId>().unwrap(),
DriverId::OpenRouter
));
assert!(matches!(
"openai_completions".parse::<DriverId>().unwrap(),
DriverId::OpenAICompletions
));
assert!(matches!(
"azure_openai".parse::<DriverId>().unwrap(),
DriverId::AzureOpenAI
));
assert!(matches!(
"anthropic".parse::<DriverId>().unwrap(),
DriverId::Anthropic
));
assert!(matches!(
"gemini".parse::<DriverId>().unwrap(),
DriverId::Gemini
));
assert!(matches!(
"llmsim".parse::<DriverId>().unwrap(),
DriverId::LlmSim
));
}
#[test]
fn test_model_limits_input_omitted_when_none() {
let limits = ModelLimits {
context: 200_000,
input: None,
output: 64_000,
max_media: None,
};
let json = serde_json::to_value(&limits).unwrap();
assert!(!json.as_object().unwrap().contains_key("input"));
}
#[test]
fn test_model_limits_input_included_when_some() {
let limits = ModelLimits {
context: 200_000,
input: Some(150_000),
output: 64_000,
max_media: None,
};
let json = serde_json::to_value(&limits).unwrap();
assert_eq!(json["input"], 150_000);
}
#[test]
fn test_model_limits_deserialize_without_input() {
let json = r#"{"context": 200000, "output": 64000}"#;
let limits: ModelLimits = serde_json::from_str(json).unwrap();
assert_eq!(limits.context, 200_000);
assert!(limits.input.is_none());
assert_eq!(limits.output, 64_000);
}
#[test]
fn test_provider_type_display() {
assert_eq!(DriverId::OpenAI.to_string(), "openai");
assert_eq!(DriverId::OpenRouter.to_string(), "openrouter");
assert_eq!(DriverId::AzureOpenAI.to_string(), "azure_openai");
assert_eq!(
DriverId::OpenAICompletions.to_string(),
"openai_completions"
);
assert_eq!(DriverId::Anthropic.to_string(), "anthropic");
assert_eq!(DriverId::Gemini.to_string(), "gemini");
assert_eq!(DriverId::LlmSim.to_string(), "llmsim");
}
}