use std::collections::HashMap;
use deepseek_config::ProviderKind;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub id: String,
pub provider: ProviderKind,
pub aliases: Vec<String>,
pub supports_tools: bool,
pub supports_reasoning: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelResolution {
pub requested: Option<String>,
pub resolved: ModelInfo,
pub used_fallback: bool,
pub fallback_chain: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ModelRegistry {
models: Vec<ModelInfo>,
alias_map: HashMap<String, usize>,
}
impl Default for ModelRegistry {
fn default() -> Self {
let models = vec![
ModelInfo {
id: "deepseek-reasoner".to_string(),
provider: ProviderKind::Deepseek,
aliases: vec!["deepseek-r1".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek-chat".to_string(),
provider: ProviderKind::Deepseek,
aliases: vec!["deepseek-v3".to_string(), "deepseek-v3.2".to_string()],
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "gpt-4.1".to_string(),
provider: ProviderKind::Openai,
aliases: vec!["gpt4.1".to_string(), "gpt-4o".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "gpt-4.1-mini".to_string(),
provider: ProviderKind::Openai,
aliases: vec!["gpt-4o-mini".to_string()],
supports_tools: true,
supports_reasoning: false,
},
];
Self::new(models)
}
}
impl ModelRegistry {
#[must_use]
pub fn new(models: Vec<ModelInfo>) -> Self {
let mut alias_map = HashMap::new();
for (idx, model) in models.iter().enumerate() {
alias_map.insert(normalize(&model.id), idx);
for alias in &model.aliases {
alias_map.insert(normalize(alias), idx);
}
}
Self { models, alias_map }
}
#[must_use]
pub fn list(&self) -> Vec<ModelInfo> {
self.models.clone()
}
#[must_use]
pub fn resolve(
&self,
requested: Option<&str>,
provider_hint: Option<ProviderKind>,
) -> ModelResolution {
let mut fallback_chain = Vec::new();
if let Some(name) = requested {
fallback_chain.push(format!("requested:{name}"));
if let Some(idx) = self.alias_map.get(&normalize(name)) {
return ModelResolution {
requested: Some(name.to_string()),
resolved: self.models[*idx].clone(),
used_fallback: false,
fallback_chain,
};
}
}
let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
fallback_chain.push(format!("provider_default:{}", provider.as_str()));
if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
return ModelResolution {
requested: requested.map(ToOwned::to_owned),
resolved: model,
used_fallback: true,
fallback_chain,
};
}
let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
id: "deepseek-reasoner".to_string(),
provider: ProviderKind::Deepseek,
aliases: Vec::new(),
supports_tools: true,
supports_reasoning: true,
});
fallback_chain.push("global_default:deepseek-reasoner".to_string());
ModelResolution {
requested: requested.map(ToOwned::to_owned),
resolved: final_fallback,
used_fallback: true,
fallback_chain,
}
}
}
fn normalize(value: &str) -> String {
value.trim().to_ascii_lowercase()
}