use std::collections::HashMap; use codewhale_config::ProviderKind; use serde::{Deserialize, Serialize}; /// High-level model family used for shared identity affordances across clients. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ModelFamily { DeepSeek, Anthropic, OpenAI, Google, Meta, Mistral, Qwen, Grok, Cohere, GptOss, Inferencer, } /// Metadata for a single model entry in the registry. /// /// Each model has a canonical `id` used by the provider, a list of `aliases` /// that users may reference, and capability flags indicating whether the model /// supports tool use and reasoning. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelInfo { /// The canonical model identifier used by the provider (e.g. `"deepseek-v4-pro"`). pub id: String, /// The provider that serves this model. pub provider: ProviderKind, /// Alternative names that users can use to reference this model (case-insensitive). pub aliases: Vec, /// Whether this model supports tool/function calling. pub supports_tools: bool, /// Whether this model supports extended reasoning. pub supports_reasoning: bool, } /// The result of resolving a user-requested model name to a concrete model entry. /// /// Contains the resolved [`ModelInfo`], whether a fallback was used, and the /// chain of resolution strategies that were attempted. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelResolution { /// The original model name requested by the user, if any. pub requested: Option, /// The concrete model that was resolved. pub resolved: ModelInfo, /// Whether a fallback was used because the requested model was not found. pub used_fallback: bool, /// The ordered list of resolution strategies that were attempted. pub fallback_chain: Vec, } /// A registry of supported models and their aliases, used to resolve user-facing /// model names to concrete provider-specific model entries. /// /// The default registry is populated with all built-in models across supported /// providers (DeepSeek, NVIDIA NIM, OpenAI-compatible, and others). #[derive(Debug, Clone)] pub struct ModelRegistry { models: Vec, alias_map: HashMap, } /// Creates a registry pre-populated with all built-in models and their aliases. impl Default for ModelRegistry { fn default() -> Self { let models = vec![ ModelInfo { id: "deepseek-v4-pro".to_string(), provider: ProviderKind::Deepseek, aliases: vec![], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-v4-flash".to_string(), provider: ProviderKind::Deepseek, aliases: vec![ "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "deepseek-r1".to_string(), "deepseek-v3".to_string(), "deepseek-v3.2".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/deepseek-v4-pro".to_string(), provider: ProviderKind::NvidiaNim, aliases: vec![ "deepseek-v4-pro".to_string(), "nvidia-deepseek-v4-pro".to_string(), "nim-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/deepseek-v4-flash".to_string(), provider: ProviderKind::NvidiaNim, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "nvidia-deepseek-v4-flash".to_string(), "nim-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-v4-pro".to_string(), provider: ProviderKind::Openai, aliases: vec!["openai-compatible-deepseek-v4-pro".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-v4-flash".to_string(), provider: ProviderKind::Openai, aliases: vec!["openai-compatible-deepseek-v4-flash".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/deepseek-v4-flash".to_string(), provider: ProviderKind::Atlascloud, aliases: vec![ "deepseek-v4-flash".to_string(), "atlascloud-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/deepseek-v4-pro".to_string(), provider: ProviderKind::Atlascloud, aliases: vec![ "deepseek-v4-pro".to_string(), "atlascloud-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-reasoner".to_string(), provider: ProviderKind::WanjieArk, aliases: vec![ "wanjie-deepseek-reasoner".to_string(), "ark-wanjie-deepseek-reasoner".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "DeepSeek-V4-Pro".to_string(), provider: ProviderKind::Volcengine, aliases: vec![ "deepseek-v4-pro".to_string(), "volcengine-deepseek-v4-pro".to_string(), "ark-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "DeepSeek-V4-Flash".to_string(), provider: ProviderKind::Volcengine, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "volcengine-deepseek-v4-flash".to_string(), "ark-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "trinity-large-thinking".to_string(), provider: ProviderKind::Arcee, aliases: vec![ "trinity".to_string(), "arcee-trinity".to_string(), "arcee-trinity-large-thinking".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek/deepseek-v4-pro".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "deepseek-v4-pro".to_string(), "openrouter-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek/deepseek-v4-flash".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "openrouter-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "arcee-ai/trinity-large-thinking".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "trinity".to_string(), "trinity-large-thinking".to_string(), "arcee-trinity-large-thinking".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "xiaomi/mimo-v2.5-pro".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "openrouter-mimo-v2.5-pro".to_string(), "openrouter-xiaomi-mimo-v2.5-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "xiaomi/mimo-v2.5".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "openrouter-mimo-v2.5".to_string(), "openrouter-xiaomi-mimo-v2.5".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "qwen/qwen3.6-flash".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["qwen3.6-flash".to_string(), "qwen-3.6-flash".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "qwen/qwen3.6-35b-a3b".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "qwen3.6-35b-a3b".to_string(), "qwen-3.6-35b-a3b".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "qwen/qwen3.6-max-preview".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "qwen3.6-max-preview".to_string(), "qwen-3.6-max-preview".to_string(), "qwen-max-preview".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "qwen/qwen3.6-27b".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "qwen/qwen3.6-plus".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["qwen3.6-plus".to_string(), "qwen-3.6-plus".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "moonshotai/kimi-k2.6".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["openrouter-kimi-k2.6".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "minimax/minimax-m3".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "minimax-m3".to_string(), "minimax-m-3".to_string(), "openrouter-minimax-m3".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "z-ai/glm-5.1".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "tencent/hy3-preview".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "google/gemma-4-31b-it".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "google/gemma-4-26b-a4b-it".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "gemma-4-26b-a4b".to_string(), "gemma-4-26b-a4b-it".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "nemotron-3-nano-omni".to_string(), "nemotron-3-nano-omni-reasoning".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "mimo-v2.5-pro".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec![ "mimo".to_string(), "pro".to_string(), "xiaomi-mimo-v2.5-pro".to_string(), "xiaomi-mimo-v2-5-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "mimo-v2.5".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec![ "omni".to_string(), "mimo-omni".to_string(), "v2.5-omni".to_string(), "mimo-v2.5-omni".to_string(), "xiaomi-mimo-v2.5".to_string(), "xiaomi-mimo-v2.5-omni".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "mimo-v2.5-asr".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec![ "asr".to_string(), "speech-to-text".to_string(), "transcribe".to_string(), ], supports_tools: false, supports_reasoning: false, }, ModelInfo { id: "mimo-v2.5-tts".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec![ "tts".to_string(), "speech".to_string(), "mimo-tts".to_string(), ], supports_tools: false, supports_reasoning: false, }, ModelInfo { id: "mimo-v2.5-tts-voicedesign".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec![ "voicedesign".to_string(), "voice-design".to_string(), "mimo-voice-design".to_string(), ], supports_tools: false, supports_reasoning: false, }, ModelInfo { id: "mimo-v2.5-tts-voiceclone".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec![ "voiceclone".to_string(), "voice-clone".to_string(), "mimo-voice-clone".to_string(), ], supports_tools: false, supports_reasoning: false, }, ModelInfo { id: "mimo-v2-tts".to_string(), provider: ProviderKind::XiaomiMimo, aliases: vec!["mimo-v2-speech".to_string()], supports_tools: false, supports_reasoning: false, }, ModelInfo { id: "deepseek/deepseek-v4-pro".to_string(), provider: ProviderKind::Novita, aliases: vec![ "deepseek-v4-pro".to_string(), "novita-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek/deepseek-v4-flash".to_string(), provider: ProviderKind::Novita, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "novita-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "accounts/fireworks/models/deepseek-v4-pro".to_string(), provider: ProviderKind::Fireworks, aliases: vec![ "deepseek-v4-pro".to_string(), "fireworks-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Pro".to_string(), provider: ProviderKind::Siliconflow, aliases: vec![ "deepseek-v4-pro".to_string(), "deepseek-reasoner".to_string(), "deepseek-r1".to_string(), "siliconflow-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Flash".to_string(), provider: ProviderKind::Siliconflow, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-v3".to_string(), "siliconflow-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "trinity-large-preview".to_string(), provider: ProviderKind::Arcee, aliases: vec!["arcee-trinity-large-preview".to_string()], supports_tools: true, supports_reasoning: false, }, ModelInfo { id: "kimi-k2.6".to_string(), provider: ProviderKind::Moonshot, aliases: vec![ "kimi".to_string(), "kimi-k2".to_string(), "moonshot-kimi-k2.6".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Pro".to_string(), provider: ProviderKind::Sglang, aliases: vec![ "deepseek-v4-pro".to_string(), "sglang-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Flash".to_string(), provider: ProviderKind::Sglang, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "sglang-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Pro".to_string(), provider: ProviderKind::Vllm, aliases: vec![ "deepseek-v4-pro".to_string(), "vllm-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Flash".to_string(), provider: ProviderKind::Vllm, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "vllm-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-coder:1.3b".to_string(), provider: ProviderKind::Ollama, aliases: vec![], supports_tools: true, supports_reasoning: false, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Pro".to_string(), provider: ProviderKind::Huggingface, aliases: vec![ "deepseek-v4-pro".to_string(), "hf-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Flash".to_string(), provider: ProviderKind::Huggingface, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "deepseek-reasoner".to_string(), "hf-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, // Together AI provider models ModelInfo { id: "deepseek-ai/DeepSeek-V4-Pro".to_string(), provider: ProviderKind::Together, aliases: vec![ "deepseek-v4-pro".to_string(), "together-deepseek-v4-pro".to_string(), ], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "deepseek-ai/DeepSeek-V4-Flash".to_string(), provider: ProviderKind::Together, aliases: vec![ "deepseek-v4-flash".to_string(), "deepseek-chat".to_string(), "together-deepseek-v4-flash".to_string(), ], supports_tools: true, supports_reasoning: true, }, // Qwen 3.7 Max (OpenRouter) ModelInfo { id: "qwen/qwen3.7-max".to_string(), provider: ProviderKind::Openrouter, aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()], supports_tools: true, supports_reasoning: true, }, // OpenAI Codex (ChatGPT OAuth) models ModelInfo { id: "gpt-5.5".to_string(), provider: ProviderKind::OpenaiCodex, aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()], supports_tools: true, supports_reasoning: true, }, // Anthropic native Messages API models (#3014) ModelInfo { id: "claude-opus-4-8".to_string(), provider: ProviderKind::Anthropic, aliases: vec!["opus".to_string(), "claude-opus".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "claude-sonnet-4-6".to_string(), provider: ProviderKind::Anthropic, aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()], supports_tools: true, supports_reasoning: true, }, ModelInfo { id: "claude-haiku-4-5".to_string(), provider: ProviderKind::Anthropic, aliases: vec!["haiku".to_string(), "claude-haiku".to_string()], supports_tools: true, supports_reasoning: false, }, // MiniMax 2.7 (OpenRouter) ModelInfo { id: "minimax/minimax-2.7".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "minimax-2.7".to_string(), "minimax-2-7".to_string(), "openrouter-minimax-2.7".to_string(), ], supports_tools: true, supports_reasoning: true, }, // NVIDIA Nemotron 3 Ultra (OpenRouter) ModelInfo { id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(), provider: ProviderKind::Openrouter, aliases: vec![ "nvidia/nemotron-3-ultra".to_string(), "nemotron-3-ultra".to_string(), "nemotron-3-ultra-550b-a55b".to_string(), "nvidia-nemotron-3-ultra".to_string(), "nvidia-nemotron-3-ultra-550b-a55b".to_string(), ], supports_tools: true, supports_reasoning: true, }, ]; Self::new(models) } } impl ModelRegistry { /// Creates a new registry from a list of [`ModelInfo`] entries. /// /// Builds an internal alias map for fast lookup by model id or alias. /// If multiple models share the same id or alias, the first one registered /// takes priority. #[must_use] pub fn new(models: Vec) -> Self { let mut alias_map = HashMap::new(); for (idx, model) in models.iter().enumerate() { alias_map.entry(normalize(&model.id)).or_insert(idx); for alias in &model.aliases { alias_map.entry(normalize(alias)).or_insert(idx); } } Self { models, alias_map } } /// Returns a clone of all models in the registry. #[must_use] pub fn list(&self) -> Vec { self.models.clone() } /// Resolves a user-requested model name to a concrete [`ModelInfo`]. /// /// Resolution follows this priority order: /// 1. If the provider is Ollama, the requested name is used as-is (to /// support arbitrary local model tags like `qwen2.5-coder:7b`). /// 2. If a `provider_hint` is given, search for a model matching that /// provider whose id or alias matches the request (case-insensitive). /// 3. Look up the alias map for a case-insensitive match. /// 4. Fall back to the first model belonging to the hinted provider /// (or DeepSeek if no hint was given). /// 5. As a last resort, fall back to the first model in the registry. #[must_use] pub fn resolve( &self, requested: Option<&str>, provider_hint: Option, ) -> ModelResolution { let mut fallback_chain = Vec::new(); if let Some(name) = requested { fallback_chain.push(format!("requested:{name}")); if provider_hint == Some(ProviderKind::Ollama) { return ModelResolution { requested: Some(name.to_string()), resolved: ModelInfo { id: name.trim().to_string(), provider: ProviderKind::Ollama, aliases: Vec::new(), supports_tools: true, supports_reasoning: false, }, used_fallback: false, fallback_chain, }; } if let Some(provider) = provider_hint && let Some(model) = self .models .iter() .find(|m| m.provider == provider && model_matches(m, name)) .cloned() { return ModelResolution { requested: Some(name.to_string()), resolved: model, used_fallback: false, fallback_chain, }; } if provider_hint == Some(ProviderKind::Atlascloud) && let Some(model) = atlascloud_passthrough_model(name) { return ModelResolution { requested: Some(name.to_string()), resolved: model, used_fallback: false, fallback_chain, }; } if provider_hint == Some(ProviderKind::Arcee) && let Some(model) = arcee_passthrough_model(name) { return ModelResolution { requested: Some(name.to_string()), resolved: model, used_fallback: false, fallback_chain, }; } if provider_hint == Some(ProviderKind::XiaomiMimo) && let Some(model) = xiaomi_mimo_passthrough_model(name) { return ModelResolution { requested: Some(name.to_string()), resolved: model, used_fallback: false, fallback_chain, }; } if let Some(idx) = self.alias_map.get(&normalize(name)) { return ModelResolution { requested: Some(name.to_string()), resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name), 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-v4-pro".to_string(), provider: ProviderKind::Deepseek, aliases: Vec::new(), supports_tools: true, supports_reasoning: true, }); fallback_chain.push("global_default:deepseek-v4-pro".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() } #[must_use] /// Classify a model identifier by its underlying model family. pub fn model_family(model_id: &str) -> ModelFamily { let normalized = normalize(model_id); if normalized.is_empty() { return ModelFamily::Inferencer; } if normalized.contains("deepseek") { return ModelFamily::DeepSeek; } if normalized.contains("claude") || normalized.contains("anthropic") { return ModelFamily::Anthropic; } if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") { return ModelFamily::GptOss; } if normalized.starts_with("gpt-") || normalized.contains("/gpt-") || normalized.contains("openai/") { return ModelFamily::OpenAI; } if normalized.contains("gemini") || normalized.contains("gemma") || normalized.contains("google/") { return ModelFamily::Google; } if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/") { return ModelFamily::Meta; } if normalized.contains("mistral") || normalized.contains("mixtral") || normalized.contains("codestral") { return ModelFamily::Mistral; } if normalized.contains("qwen") { return ModelFamily::Qwen; } if normalized.contains("grok") { return ModelFamily::Grok; } if normalized.contains("cohere") || normalized.contains("command-r") { return ModelFamily::Cohere; } ModelFamily::Inferencer } fn model_matches(model: &ModelInfo, requested: &str) -> bool { let requested = normalize(requested); normalize(&model.id) == requested || model .aliases .iter() .any(|alias| normalize(alias) == requested) } fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo { let requested = requested.trim(); if model.id.eq_ignore_ascii_case(requested) { model.id = requested.to_string(); } model } fn atlascloud_passthrough_model(requested: &str) -> Option { let requested = requested.trim(); if requested.is_empty() || !requested.contains('/') { return None; } Some(ModelInfo { id: requested.to_string(), provider: ProviderKind::Atlascloud, aliases: Vec::new(), supports_tools: true, supports_reasoning: true, }) } fn arcee_passthrough_model(requested: &str) -> Option { let requested = requested.trim(); if requested.is_empty() { return None; } let supports_reasoning = requested.to_ascii_lowercase().contains("thinking"); Some(ModelInfo { id: requested.to_string(), provider: ProviderKind::Arcee, aliases: Vec::new(), supports_tools: true, supports_reasoning, }) } fn xiaomi_mimo_passthrough_model(requested: &str) -> Option { let requested = requested.trim(); if requested.is_empty() || requested.chars().any(char::is_control) { return None; } Some(ModelInfo { id: requested.to_string(), provider: ProviderKind::XiaomiMimo, aliases: Vec::new(), supports_tools: true, supports_reasoning: true, }) } #[cfg(test)] mod tests { use super::*; #[test] fn deepseek_v4_pro_alias_stays_deepseek_by_default() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-pro"), None); assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); assert_eq!(resolved.resolved.id, "deepseek-v4-pro"); } #[test] fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim)); assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro"); } #[test] fn nvidia_nim_default_uses_catalog_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim)); assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro"); } #[test] fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim)); assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash"); } #[test] fn atlascloud_default_uses_namespaced_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud)); assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash"); assert!(resolved.resolved.supports_reasoning); } #[test] fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud)); assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash"); } #[test] fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud)); assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro"); } #[test] fn atlascloud_provider_hint_passes_through_explicit_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud)); assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud); assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat"); assert!(resolved.resolved.supports_tools); assert!(resolved.resolved.supports_reasoning); assert!(!resolved.used_fallback); } #[test] fn atlascloud_provider_hint_preserves_explicit_model_id_case() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud)); assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud); assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder"); assert!(!resolved.used_fallback); } #[test] fn atlascloud_plain_unknown_model_still_uses_provider_default() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud)); assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud); assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash"); assert!(resolved.used_fallback); } #[test] fn openrouter_default_uses_namespaced_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Openrouter)); assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter); assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro"); } #[test] fn xiaomi_mimo_default_uses_canonical_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo)); assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo); assert_eq!(resolved.resolved.id, "mimo-v2.5-pro"); assert!(resolved.resolved.supports_reasoning); } #[test] fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo)); assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo); assert_eq!(resolved.resolved.id, "mimo-v2.5-tts"); assert!(!resolved.resolved.supports_tools); assert!(!resolved.resolved.supports_reasoning); let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo)); assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign"); let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo)); assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone"); } #[test] fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo)); assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo); assert_eq!(resolved.resolved.id, "mimo-v2.5"); assert!(resolved.resolved.supports_tools); } #[test] fn xiaomi_mimo_provider_hint_preserves_custom_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo)); assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo); assert_eq!(resolved.resolved.id, "account-custom-mimo"); assert!(!resolved.used_fallback); } #[test] fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve( Some("deepseek/deepseek-v4-pro"), Some(ProviderKind::XiaomiMimo), ); assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo); assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro"); assert!(!resolved.used_fallback); } #[test] fn wanjie_ark_default_uses_reasoner_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk)); assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk); assert_eq!(resolved.resolved.id, "deepseek-reasoner"); assert!(resolved.resolved.supports_reasoning); } #[test] fn novita_default_uses_namespaced_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Novita)); assert_eq!(resolved.resolved.provider, ProviderKind::Novita); assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro"); } #[test] fn fireworks_default_uses_canonical_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Fireworks)); assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks); assert_eq!( resolved.resolved.id, "accounts/fireworks/models/deepseek-v4-pro" ); } #[test] fn siliconflow_default_uses_canonical_pro_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow)); assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro"); assert!(resolved.resolved.supports_reasoning); } #[test] fn arcee_default_uses_direct_trinity_large_thinking_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Arcee)); assert_eq!(resolved.resolved.provider, ProviderKind::Arcee); assert_eq!(resolved.resolved.id, "trinity-large-thinking"); assert!(resolved.resolved.supports_reasoning); } #[test] fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee)); assert_eq!(resolved.resolved.provider, ProviderKind::Arcee); assert_eq!(resolved.resolved.id, "trinity-large-thinking"); assert!(resolved.resolved.supports_reasoning); } #[test] fn arcee_trinity_mini_remains_explicit_compatibility_model() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee)); assert_eq!(resolved.resolved.provider, ProviderKind::Arcee); assert_eq!(resolved.resolved.id, "trinity-mini"); assert!(!resolved.resolved.supports_reasoning); } #[test] fn arcee_provider_hint_preserves_explicit_future_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee)); assert_eq!(resolved.resolved.provider, ProviderKind::Arcee); assert_eq!(resolved.resolved.id, "trinity-large-next"); assert!(!resolved.resolved.supports_reasoning); assert!(!resolved.used_fallback); } #[test] fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow)); assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro"); } #[test] fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow)); assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash"); } #[test] fn sglang_default_uses_canonical_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Sglang)); assert_eq!(resolved.resolved.provider, ProviderKind::Sglang); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro"); } #[test] fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter)); assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter); assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash"); } #[test] fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() { let registry = ModelRegistry::default(); for (alias, expected) in [ ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"), ("qwen3.6-flash", "qwen/qwen3.6-flash"), ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"), ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"), ("qwen3.6-plus", "qwen/qwen3.6-plus"), ("gemma-4-31b-it", "google/gemma-4-31b-it"), ("glm-5.1", "z-ai/glm-5.1"), ("minimax-m3", "minimax/minimax-m3"), ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"), ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"), ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"), ( "nvidia/nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b", ), ] { let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter)); assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter); assert_eq!(resolved.resolved.id, expected); assert!(resolved.resolved.supports_tools); assert!(resolved.resolved.supports_reasoning); } } #[test] fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita)); assert_eq!(resolved.resolved.provider, ProviderKind::Novita); assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash"); } #[test] fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang)); assert_eq!(resolved.resolved.provider, ProviderKind::Sglang); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash"); } #[test] fn vllm_default_uses_canonical_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Vllm)); assert_eq!(resolved.resolved.provider, ProviderKind::Vllm); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro"); } #[test] fn ollama_default_uses_small_local_model_id() { let registry = ModelRegistry::default(); let resolved = registry.resolve(None, Some(ProviderKind::Ollama)); assert_eq!(resolved.resolved.provider, ProviderKind::Ollama); assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b"); assert!(!resolved.resolved.supports_reasoning); } #[test] fn ollama_requested_model_tag_is_preserved() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama)); assert_eq!(resolved.resolved.provider, ProviderKind::Ollama); assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b"); assert!(!resolved.used_fallback); } #[test] fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm)); assert_eq!(resolved.resolved.provider, ProviderKind::Vllm); assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash"); } #[test] fn preserves_requested_model_casing_for_third_party_providers() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None); assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro"); } #[test] fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek)); assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); // Registry's canonical id is used even when user provides different casing assert_eq!(resolved.resolved.id, "deepseek-v4-pro"); } #[test] fn preserves_requested_model_casing_without_surrounding_whitespace() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None); assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro"); } #[test] fn alias_match_does_not_override_requested_casing() { let registry = ModelRegistry::default(); let resolved = registry.resolve(Some("deepseek-reasoner"), None); assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek); assert_eq!(resolved.resolved.id, "deepseek-v4-flash"); } #[test] fn model_family_classifies_known_model_ids() { assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek); assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI); assert_eq!( model_family("anthropic/claude-opus-4-7"), ModelFamily::Anthropic ); assert_eq!( model_family("meta-llama/llama-3.3-70b-instruct"), ModelFamily::Meta ); assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen); } #[test] fn model_family_uses_underlying_model_for_router_ids() { assert_eq!( model_family("groq/llama-3.3-70b-versatile"), ModelFamily::Meta ); assert_eq!( model_family("openrouter/openai/gpt-5.4"), ModelFamily::OpenAI ); assert_eq!( model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"), ModelFamily::DeepSeek ); } #[test] fn model_family_covers_prominent_google_and_mistral_model_names() { assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google); assert_eq!( model_family("mistralai/mixtral-8x22b"), ModelFamily::Mistral ); assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral); } #[test] fn model_family_falls_back_to_inferencer_for_unknown_models() { assert_eq!( model_family("custom-gateway/my-private-model"), ModelFamily::Inferencer ); assert_eq!(model_family(""), ModelFamily::Inferencer); } }