1use std::collections::HashMap;
2
3use codewhale_config::ProviderKind;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ModelFamily {
9 DeepSeek,
10 Anthropic,
11 OpenAI,
12 Google,
13 Meta,
14 Mistral,
15 Qwen,
16 Grok,
17 Cohere,
18 GptOss,
19 Inferencer,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ModelInfo {
29 pub id: String,
31 pub provider: ProviderKind,
33 pub aliases: Vec<String>,
35 pub supports_tools: bool,
37 pub supports_reasoning: bool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelResolution {
47 pub requested: Option<String>,
49 pub resolved: ModelInfo,
51 pub used_fallback: bool,
53 pub fallback_chain: Vec<String>,
55}
56
57#[derive(Debug, Clone)]
63pub struct ModelRegistry {
64 models: Vec<ModelInfo>,
65 alias_map: HashMap<String, usize>,
66}
67
68impl Default for ModelRegistry {
70 fn default() -> Self {
71 let models = vec![
72 ModelInfo {
73 id: "deepseek-v4-pro".to_string(),
74 provider: ProviderKind::Deepseek,
75 aliases: vec![],
76 supports_tools: true,
77 supports_reasoning: true,
78 },
79 ModelInfo {
80 id: "deepseek-v4-flash".to_string(),
81 provider: ProviderKind::Deepseek,
82 aliases: vec![
83 "deepseek-chat".to_string(),
84 "deepseek-reasoner".to_string(),
85 "deepseek-r1".to_string(),
86 "deepseek-v3".to_string(),
87 "deepseek-v3.2".to_string(),
88 ],
89 supports_tools: true,
90 supports_reasoning: true,
91 },
92 ModelInfo {
93 id: "deepseek-ai/deepseek-v4-pro".to_string(),
94 provider: ProviderKind::NvidiaNim,
95 aliases: vec![
96 "deepseek-v4-pro".to_string(),
97 "nvidia-deepseek-v4-pro".to_string(),
98 "nim-deepseek-v4-pro".to_string(),
99 ],
100 supports_tools: true,
101 supports_reasoning: true,
102 },
103 ModelInfo {
104 id: "deepseek-ai/deepseek-v4-flash".to_string(),
105 provider: ProviderKind::NvidiaNim,
106 aliases: vec![
107 "deepseek-v4-flash".to_string(),
108 "deepseek-chat".to_string(),
109 "deepseek-reasoner".to_string(),
110 "nvidia-deepseek-v4-flash".to_string(),
111 "nim-deepseek-v4-flash".to_string(),
112 ],
113 supports_tools: true,
114 supports_reasoning: true,
115 },
116 ModelInfo {
117 id: "deepseek-v4-pro".to_string(),
118 provider: ProviderKind::Openai,
119 aliases: vec!["openai-compatible-deepseek-v4-pro".to_string()],
120 supports_tools: true,
121 supports_reasoning: true,
122 },
123 ModelInfo {
124 id: "deepseek-v4-flash".to_string(),
125 provider: ProviderKind::Openai,
126 aliases: vec!["openai-compatible-deepseek-v4-flash".to_string()],
127 supports_tools: true,
128 supports_reasoning: true,
129 },
130 ModelInfo {
131 id: "deepseek-ai/deepseek-v4-flash".to_string(),
132 provider: ProviderKind::Atlascloud,
133 aliases: vec![
134 "deepseek-v4-flash".to_string(),
135 "atlascloud-deepseek-v4-flash".to_string(),
136 ],
137 supports_tools: true,
138 supports_reasoning: true,
139 },
140 ModelInfo {
141 id: "deepseek-ai/deepseek-v4-pro".to_string(),
142 provider: ProviderKind::Atlascloud,
143 aliases: vec![
144 "deepseek-v4-pro".to_string(),
145 "atlascloud-deepseek-v4-pro".to_string(),
146 ],
147 supports_tools: true,
148 supports_reasoning: true,
149 },
150 ModelInfo {
151 id: "deepseek-reasoner".to_string(),
152 provider: ProviderKind::WanjieArk,
153 aliases: vec![
154 "wanjie-deepseek-reasoner".to_string(),
155 "ark-wanjie-deepseek-reasoner".to_string(),
156 ],
157 supports_tools: true,
158 supports_reasoning: true,
159 },
160 ModelInfo {
161 id: "DeepSeek-V4-Pro".to_string(),
162 provider: ProviderKind::Volcengine,
163 aliases: vec![
164 "deepseek-v4-pro".to_string(),
165 "volcengine-deepseek-v4-pro".to_string(),
166 "ark-deepseek-v4-pro".to_string(),
167 ],
168 supports_tools: true,
169 supports_reasoning: true,
170 },
171 ModelInfo {
172 id: "DeepSeek-V4-Flash".to_string(),
173 provider: ProviderKind::Volcengine,
174 aliases: vec![
175 "deepseek-v4-flash".to_string(),
176 "deepseek-chat".to_string(),
177 "volcengine-deepseek-v4-flash".to_string(),
178 "ark-deepseek-v4-flash".to_string(),
179 ],
180 supports_tools: true,
181 supports_reasoning: true,
182 },
183 ModelInfo {
184 id: "trinity-large-thinking".to_string(),
185 provider: ProviderKind::Arcee,
186 aliases: vec![
187 "trinity".to_string(),
188 "arcee-trinity".to_string(),
189 "arcee-trinity-large-thinking".to_string(),
190 ],
191 supports_tools: true,
192 supports_reasoning: true,
193 },
194 ModelInfo {
195 id: "deepseek/deepseek-v4-pro".to_string(),
196 provider: ProviderKind::Openrouter,
197 aliases: vec![
198 "deepseek-v4-pro".to_string(),
199 "openrouter-deepseek-v4-pro".to_string(),
200 ],
201 supports_tools: true,
202 supports_reasoning: true,
203 },
204 ModelInfo {
205 id: "deepseek/deepseek-v4-flash".to_string(),
206 provider: ProviderKind::Openrouter,
207 aliases: vec![
208 "deepseek-v4-flash".to_string(),
209 "deepseek-chat".to_string(),
210 "deepseek-reasoner".to_string(),
211 "openrouter-deepseek-v4-flash".to_string(),
212 ],
213 supports_tools: true,
214 supports_reasoning: true,
215 },
216 ModelInfo {
217 id: "arcee-ai/trinity-large-thinking".to_string(),
218 provider: ProviderKind::Openrouter,
219 aliases: vec![
220 "trinity".to_string(),
221 "trinity-large-thinking".to_string(),
222 "arcee-trinity-large-thinking".to_string(),
223 ],
224 supports_tools: true,
225 supports_reasoning: true,
226 },
227 ModelInfo {
228 id: "xiaomi/mimo-v2.5-pro".to_string(),
229 provider: ProviderKind::Openrouter,
230 aliases: vec![
231 "openrouter-mimo-v2.5-pro".to_string(),
232 "openrouter-xiaomi-mimo-v2.5-pro".to_string(),
233 ],
234 supports_tools: true,
235 supports_reasoning: true,
236 },
237 ModelInfo {
238 id: "xiaomi/mimo-v2.5".to_string(),
239 provider: ProviderKind::Openrouter,
240 aliases: vec![
241 "openrouter-mimo-v2.5".to_string(),
242 "openrouter-xiaomi-mimo-v2.5".to_string(),
243 ],
244 supports_tools: true,
245 supports_reasoning: true,
246 },
247 ModelInfo {
248 id: "qwen/qwen3.6-flash".to_string(),
249 provider: ProviderKind::Openrouter,
250 aliases: vec!["qwen3.6-flash".to_string(), "qwen-3.6-flash".to_string()],
251 supports_tools: true,
252 supports_reasoning: true,
253 },
254 ModelInfo {
255 id: "qwen/qwen3.6-35b-a3b".to_string(),
256 provider: ProviderKind::Openrouter,
257 aliases: vec![
258 "qwen3.6-35b-a3b".to_string(),
259 "qwen-3.6-35b-a3b".to_string(),
260 ],
261 supports_tools: true,
262 supports_reasoning: true,
263 },
264 ModelInfo {
265 id: "qwen/qwen3.6-max-preview".to_string(),
266 provider: ProviderKind::Openrouter,
267 aliases: vec![
268 "qwen3.6-max-preview".to_string(),
269 "qwen-3.6-max-preview".to_string(),
270 "qwen-max-preview".to_string(),
271 ],
272 supports_tools: true,
273 supports_reasoning: true,
274 },
275 ModelInfo {
276 id: "qwen/qwen3.6-27b".to_string(),
277 provider: ProviderKind::Openrouter,
278 aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()],
279 supports_tools: true,
280 supports_reasoning: true,
281 },
282 ModelInfo {
283 id: "qwen/qwen3.6-plus".to_string(),
284 provider: ProviderKind::Openrouter,
285 aliases: vec!["qwen3.6-plus".to_string(), "qwen-3.6-plus".to_string()],
286 supports_tools: true,
287 supports_reasoning: true,
288 },
289 ModelInfo {
290 id: "moonshotai/kimi-k2.7-code".to_string(),
291 provider: ProviderKind::Openrouter,
292 aliases: vec![
293 "kimi-k2.7-code".to_string(),
294 "openrouter-kimi-k2.7-code".to_string(),
295 ],
296 supports_tools: true,
297 supports_reasoning: true,
298 },
299 ModelInfo {
300 id: "moonshotai/kimi-k2.6".to_string(),
301 provider: ProviderKind::Openrouter,
302 aliases: vec!["openrouter-kimi-k2.6".to_string()],
303 supports_tools: true,
304 supports_reasoning: true,
305 },
306 ModelInfo {
307 id: "minimax/minimax-m3".to_string(),
308 provider: ProviderKind::Openrouter,
309 aliases: vec![
310 "minimax-m3".to_string(),
311 "minimax-m-3".to_string(),
312 "openrouter-minimax-m3".to_string(),
313 ],
314 supports_tools: true,
315 supports_reasoning: true,
316 },
317 ModelInfo {
318 id: "z-ai/glm-5.1".to_string(),
319 provider: ProviderKind::Openrouter,
320 aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
321 supports_tools: true,
322 supports_reasoning: true,
323 },
324 ModelInfo {
325 id: "tencent/hy3-preview".to_string(),
326 provider: ProviderKind::Openrouter,
327 aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
328 supports_tools: true,
329 supports_reasoning: true,
330 },
331 ModelInfo {
332 id: "google/gemma-4-31b-it".to_string(),
333 provider: ProviderKind::Openrouter,
334 aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
335 supports_tools: true,
336 supports_reasoning: true,
337 },
338 ModelInfo {
339 id: "google/gemma-4-26b-a4b-it".to_string(),
340 provider: ProviderKind::Openrouter,
341 aliases: vec![
342 "gemma-4-26b-a4b".to_string(),
343 "gemma-4-26b-a4b-it".to_string(),
344 ],
345 supports_tools: true,
346 supports_reasoning: true,
347 },
348 ModelInfo {
349 id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
350 provider: ProviderKind::Openrouter,
351 aliases: vec![
352 "nemotron-3-nano-omni".to_string(),
353 "nemotron-3-nano-omni-reasoning".to_string(),
354 ],
355 supports_tools: true,
356 supports_reasoning: true,
357 },
358 ModelInfo {
359 id: "mimo-v2.5-pro".to_string(),
360 provider: ProviderKind::XiaomiMimo,
361 aliases: vec![
362 "mimo".to_string(),
363 "pro".to_string(),
364 "xiaomi-mimo-v2.5-pro".to_string(),
365 "xiaomi-mimo-v2-5-pro".to_string(),
366 ],
367 supports_tools: true,
368 supports_reasoning: true,
369 },
370 ModelInfo {
371 id: "mimo-v2.5".to_string(),
372 provider: ProviderKind::XiaomiMimo,
373 aliases: vec![
374 "omni".to_string(),
375 "mimo-omni".to_string(),
376 "v2.5-omni".to_string(),
377 "mimo-v2.5-omni".to_string(),
378 "xiaomi-mimo-v2.5".to_string(),
379 "xiaomi-mimo-v2.5-omni".to_string(),
380 ],
381 supports_tools: true,
382 supports_reasoning: true,
383 },
384 ModelInfo {
385 id: "mimo-v2.5-asr".to_string(),
386 provider: ProviderKind::XiaomiMimo,
387 aliases: vec![
388 "asr".to_string(),
389 "speech-to-text".to_string(),
390 "transcribe".to_string(),
391 ],
392 supports_tools: false,
393 supports_reasoning: false,
394 },
395 ModelInfo {
396 id: "mimo-v2.5-tts".to_string(),
397 provider: ProviderKind::XiaomiMimo,
398 aliases: vec![
399 "tts".to_string(),
400 "speech".to_string(),
401 "mimo-tts".to_string(),
402 ],
403 supports_tools: false,
404 supports_reasoning: false,
405 },
406 ModelInfo {
407 id: "mimo-v2.5-tts-voicedesign".to_string(),
408 provider: ProviderKind::XiaomiMimo,
409 aliases: vec![
410 "voicedesign".to_string(),
411 "voice-design".to_string(),
412 "mimo-voice-design".to_string(),
413 ],
414 supports_tools: false,
415 supports_reasoning: false,
416 },
417 ModelInfo {
418 id: "mimo-v2.5-tts-voiceclone".to_string(),
419 provider: ProviderKind::XiaomiMimo,
420 aliases: vec![
421 "voiceclone".to_string(),
422 "voice-clone".to_string(),
423 "mimo-voice-clone".to_string(),
424 ],
425 supports_tools: false,
426 supports_reasoning: false,
427 },
428 ModelInfo {
429 id: "mimo-v2-tts".to_string(),
430 provider: ProviderKind::XiaomiMimo,
431 aliases: vec!["mimo-v2-speech".to_string()],
432 supports_tools: false,
433 supports_reasoning: false,
434 },
435 ModelInfo {
436 id: "deepseek/deepseek-v4-pro".to_string(),
437 provider: ProviderKind::Novita,
438 aliases: vec![
439 "deepseek-v4-pro".to_string(),
440 "novita-deepseek-v4-pro".to_string(),
441 ],
442 supports_tools: true,
443 supports_reasoning: true,
444 },
445 ModelInfo {
446 id: "deepseek/deepseek-v4-flash".to_string(),
447 provider: ProviderKind::Novita,
448 aliases: vec![
449 "deepseek-v4-flash".to_string(),
450 "deepseek-chat".to_string(),
451 "deepseek-reasoner".to_string(),
452 "novita-deepseek-v4-flash".to_string(),
453 ],
454 supports_tools: true,
455 supports_reasoning: true,
456 },
457 ModelInfo {
458 id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
459 provider: ProviderKind::Fireworks,
460 aliases: vec![
461 "deepseek-v4-pro".to_string(),
462 "fireworks-deepseek-v4-pro".to_string(),
463 ],
464 supports_tools: true,
465 supports_reasoning: true,
466 },
467 ModelInfo {
468 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
469 provider: ProviderKind::Siliconflow,
470 aliases: vec![
471 "deepseek-v4-pro".to_string(),
472 "deepseek-reasoner".to_string(),
473 "deepseek-r1".to_string(),
474 "siliconflow-deepseek-v4-pro".to_string(),
475 ],
476 supports_tools: true,
477 supports_reasoning: true,
478 },
479 ModelInfo {
480 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
481 provider: ProviderKind::Siliconflow,
482 aliases: vec![
483 "deepseek-v4-flash".to_string(),
484 "deepseek-chat".to_string(),
485 "deepseek-v3".to_string(),
486 "siliconflow-deepseek-v4-flash".to_string(),
487 ],
488 supports_tools: true,
489 supports_reasoning: true,
490 },
491 ModelInfo {
492 id: "trinity-large-preview".to_string(),
493 provider: ProviderKind::Arcee,
494 aliases: vec!["arcee-trinity-large-preview".to_string()],
495 supports_tools: true,
496 supports_reasoning: false,
497 },
498 ModelInfo {
499 id: "kimi-k2.7-code".to_string(),
500 provider: ProviderKind::Moonshot,
501 aliases: vec![
502 "kimi".to_string(),
503 "kimi-k2".to_string(),
504 "kimi-k2.7".to_string(),
505 "kimi-code".to_string(),
506 "moonshot-kimi-k2.7-code".to_string(),
507 ],
508 supports_tools: true,
509 supports_reasoning: true,
510 },
511 ModelInfo {
512 id: "kimi-k2.6".to_string(),
513 provider: ProviderKind::Moonshot,
514 aliases: vec!["kimi-k2.6".to_string(), "moonshot-kimi-k2.6".to_string()],
515 supports_tools: true,
516 supports_reasoning: true,
517 },
518 ModelInfo {
519 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
520 provider: ProviderKind::Sglang,
521 aliases: vec![
522 "deepseek-v4-pro".to_string(),
523 "sglang-deepseek-v4-pro".to_string(),
524 ],
525 supports_tools: true,
526 supports_reasoning: true,
527 },
528 ModelInfo {
529 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
530 provider: ProviderKind::Sglang,
531 aliases: vec![
532 "deepseek-v4-flash".to_string(),
533 "deepseek-chat".to_string(),
534 "deepseek-reasoner".to_string(),
535 "sglang-deepseek-v4-flash".to_string(),
536 ],
537 supports_tools: true,
538 supports_reasoning: true,
539 },
540 ModelInfo {
541 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
542 provider: ProviderKind::Vllm,
543 aliases: vec![
544 "deepseek-v4-pro".to_string(),
545 "vllm-deepseek-v4-pro".to_string(),
546 ],
547 supports_tools: true,
548 supports_reasoning: true,
549 },
550 ModelInfo {
551 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
552 provider: ProviderKind::Vllm,
553 aliases: vec![
554 "deepseek-v4-flash".to_string(),
555 "deepseek-chat".to_string(),
556 "deepseek-reasoner".to_string(),
557 "vllm-deepseek-v4-flash".to_string(),
558 ],
559 supports_tools: true,
560 supports_reasoning: true,
561 },
562 ModelInfo {
563 id: "deepseek-coder:1.3b".to_string(),
564 provider: ProviderKind::Ollama,
565 aliases: vec![],
566 supports_tools: true,
567 supports_reasoning: false,
568 },
569 ModelInfo {
570 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
571 provider: ProviderKind::Huggingface,
572 aliases: vec![
573 "deepseek-v4-pro".to_string(),
574 "hf-deepseek-v4-pro".to_string(),
575 ],
576 supports_tools: true,
577 supports_reasoning: true,
578 },
579 ModelInfo {
580 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
581 provider: ProviderKind::Huggingface,
582 aliases: vec![
583 "deepseek-v4-flash".to_string(),
584 "deepseek-chat".to_string(),
585 "deepseek-reasoner".to_string(),
586 "hf-deepseek-v4-flash".to_string(),
587 ],
588 supports_tools: true,
589 supports_reasoning: true,
590 },
591 ModelInfo {
593 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
594 provider: ProviderKind::Together,
595 aliases: vec![
596 "deepseek-v4-pro".to_string(),
597 "together-deepseek-v4-pro".to_string(),
598 ],
599 supports_tools: true,
600 supports_reasoning: true,
601 },
602 ModelInfo {
603 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
604 provider: ProviderKind::Together,
605 aliases: vec![
606 "deepseek-v4-flash".to_string(),
607 "deepseek-chat".to_string(),
608 "together-deepseek-v4-flash".to_string(),
609 ],
610 supports_tools: true,
611 supports_reasoning: true,
612 },
613 ModelInfo {
615 id: "qwen/qwen3.7-max".to_string(),
616 provider: ProviderKind::Openrouter,
617 aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
618 supports_tools: true,
619 supports_reasoning: true,
620 },
621 ModelInfo {
623 id: "gpt-5.5".to_string(),
624 provider: ProviderKind::OpenaiCodex,
625 aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
626 supports_tools: true,
627 supports_reasoning: true,
628 },
629 ModelInfo {
631 id: "claude-opus-4-8".to_string(),
632 provider: ProviderKind::Anthropic,
633 aliases: vec!["opus".to_string(), "claude-opus".to_string()],
634 supports_tools: true,
635 supports_reasoning: true,
636 },
637 ModelInfo {
638 id: "claude-sonnet-4-6".to_string(),
639 provider: ProviderKind::Anthropic,
640 aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
641 supports_tools: true,
642 supports_reasoning: true,
643 },
644 ModelInfo {
645 id: "claude-haiku-4-5".to_string(),
646 provider: ProviderKind::Anthropic,
647 aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
648 supports_tools: true,
649 supports_reasoning: false,
650 },
651 ModelInfo {
653 id: "minimax/minimax-2.7".to_string(),
654 provider: ProviderKind::Openrouter,
655 aliases: vec![
656 "minimax-2.7".to_string(),
657 "minimax-2-7".to_string(),
658 "openrouter-minimax-2.7".to_string(),
659 ],
660 supports_tools: true,
661 supports_reasoning: true,
662 },
663 ModelInfo {
665 id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
666 provider: ProviderKind::Openrouter,
667 aliases: vec![
668 "nvidia/nemotron-3-ultra".to_string(),
669 "nemotron-3-ultra".to_string(),
670 "nemotron-3-ultra-550b-a55b".to_string(),
671 "nvidia-nemotron-3-ultra".to_string(),
672 "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
673 ],
674 supports_tools: true,
675 supports_reasoning: true,
676 },
677 ];
678 Self::new(models)
679 }
680}
681
682impl ModelRegistry {
683 #[must_use]
689 pub fn new(models: Vec<ModelInfo>) -> Self {
690 let mut alias_map = HashMap::new();
691 for (idx, model) in models.iter().enumerate() {
692 alias_map.entry(normalize(&model.id)).or_insert(idx);
693 for alias in &model.aliases {
694 alias_map.entry(normalize(alias)).or_insert(idx);
695 }
696 }
697 Self { models, alias_map }
698 }
699
700 #[must_use]
702 pub fn list(&self) -> Vec<ModelInfo> {
703 self.models.clone()
704 }
705
706 #[must_use]
718 pub fn resolve(
719 &self,
720 requested: Option<&str>,
721 provider_hint: Option<ProviderKind>,
722 ) -> ModelResolution {
723 let mut fallback_chain = Vec::new();
724
725 if let Some(name) = requested {
726 fallback_chain.push(format!("requested:{name}"));
727 if provider_hint == Some(ProviderKind::Ollama) {
728 return ModelResolution {
729 requested: Some(name.to_string()),
730 resolved: ModelInfo {
731 id: name.trim().to_string(),
732 provider: ProviderKind::Ollama,
733 aliases: Vec::new(),
734 supports_tools: true,
735 supports_reasoning: false,
736 },
737 used_fallback: false,
738 fallback_chain,
739 };
740 }
741 if let Some(provider) = provider_hint
742 && let Some(model) = self
743 .models
744 .iter()
745 .find(|m| m.provider == provider && model_matches(m, name))
746 .cloned()
747 {
748 return ModelResolution {
749 requested: Some(name.to_string()),
750 resolved: model,
751 used_fallback: false,
752 fallback_chain,
753 };
754 }
755 if provider_hint == Some(ProviderKind::Atlascloud)
756 && let Some(model) = atlascloud_passthrough_model(name)
757 {
758 return ModelResolution {
759 requested: Some(name.to_string()),
760 resolved: model,
761 used_fallback: false,
762 fallback_chain,
763 };
764 }
765 if provider_hint == Some(ProviderKind::Arcee)
766 && let Some(model) = arcee_passthrough_model(name)
767 {
768 return ModelResolution {
769 requested: Some(name.to_string()),
770 resolved: model,
771 used_fallback: false,
772 fallback_chain,
773 };
774 }
775 if provider_hint == Some(ProviderKind::XiaomiMimo)
776 && let Some(model) = xiaomi_mimo_passthrough_model(name)
777 {
778 return ModelResolution {
779 requested: Some(name.to_string()),
780 resolved: model,
781 used_fallback: false,
782 fallback_chain,
783 };
784 }
785 if let Some(idx) = self.alias_map.get(&normalize(name)) {
786 return ModelResolution {
787 requested: Some(name.to_string()),
788 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
789 used_fallback: false,
790 fallback_chain,
791 };
792 }
793 }
794
795 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
796 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
797 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
798 return ModelResolution {
799 requested: requested.map(ToOwned::to_owned),
800 resolved: model,
801 used_fallback: true,
802 fallback_chain,
803 };
804 }
805
806 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
807 id: "deepseek-v4-pro".to_string(),
808 provider: ProviderKind::Deepseek,
809 aliases: Vec::new(),
810 supports_tools: true,
811 supports_reasoning: true,
812 });
813 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
814 ModelResolution {
815 requested: requested.map(ToOwned::to_owned),
816 resolved: final_fallback,
817 used_fallback: true,
818 fallback_chain,
819 }
820 }
821}
822
823fn normalize(value: &str) -> String {
824 value.trim().to_ascii_lowercase()
825}
826
827#[must_use]
828pub fn model_family(model_id: &str) -> ModelFamily {
830 let normalized = normalize(model_id);
831 if normalized.is_empty() {
832 return ModelFamily::Inferencer;
833 }
834
835 if normalized.contains("deepseek") {
836 return ModelFamily::DeepSeek;
837 }
838 if normalized.contains("claude") || normalized.contains("anthropic") {
839 return ModelFamily::Anthropic;
840 }
841 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
842 return ModelFamily::GptOss;
843 }
844 if normalized.starts_with("gpt-")
845 || normalized.contains("/gpt-")
846 || normalized.contains("openai/")
847 {
848 return ModelFamily::OpenAI;
849 }
850 if normalized.contains("gemini")
851 || normalized.contains("gemma")
852 || normalized.contains("google/")
853 {
854 return ModelFamily::Google;
855 }
856 if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/")
857 {
858 return ModelFamily::Meta;
859 }
860 if normalized.contains("mistral")
861 || normalized.contains("mixtral")
862 || normalized.contains("codestral")
863 {
864 return ModelFamily::Mistral;
865 }
866 if normalized.contains("qwen") {
867 return ModelFamily::Qwen;
868 }
869 if normalized.contains("grok") {
870 return ModelFamily::Grok;
871 }
872 if normalized.contains("cohere") || normalized.contains("command-r") {
873 return ModelFamily::Cohere;
874 }
875
876 ModelFamily::Inferencer
877}
878
879fn model_matches(model: &ModelInfo, requested: &str) -> bool {
880 let requested = normalize(requested);
881 normalize(&model.id) == requested
882 || model
883 .aliases
884 .iter()
885 .any(|alias| normalize(alias) == requested)
886}
887
888fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
889 let requested = requested.trim();
890 if model.id.eq_ignore_ascii_case(requested) {
891 model.id = requested.to_string();
892 }
893 model
894}
895
896fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
897 let requested = requested.trim();
898 if requested.is_empty() || !requested.contains('/') {
899 return None;
900 }
901
902 Some(ModelInfo {
903 id: requested.to_string(),
904 provider: ProviderKind::Atlascloud,
905 aliases: Vec::new(),
906 supports_tools: true,
907 supports_reasoning: true,
908 })
909}
910
911fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
912 let requested = requested.trim();
913 if requested.is_empty() {
914 return None;
915 }
916 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
917
918 Some(ModelInfo {
919 id: requested.to_string(),
920 provider: ProviderKind::Arcee,
921 aliases: Vec::new(),
922 supports_tools: true,
923 supports_reasoning,
924 })
925}
926
927fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
928 let requested = requested.trim();
929 if requested.is_empty() || requested.chars().any(char::is_control) {
930 return None;
931 }
932
933 Some(ModelInfo {
934 id: requested.to_string(),
935 provider: ProviderKind::XiaomiMimo,
936 aliases: Vec::new(),
937 supports_tools: true,
938 supports_reasoning: true,
939 })
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945
946 #[test]
947 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
948 let registry = ModelRegistry::default();
949 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
950
951 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
952 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
953 }
954
955 #[test]
956 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
957 let registry = ModelRegistry::default();
958 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
959
960 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
961 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
962 }
963
964 #[test]
965 fn nvidia_nim_default_uses_catalog_model_id() {
966 let registry = ModelRegistry::default();
967 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
968
969 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
970 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
971 }
972
973 #[test]
974 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
975 let registry = ModelRegistry::default();
976 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
977
978 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
979 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
980 }
981
982 #[test]
983 fn atlascloud_default_uses_namespaced_model_id() {
984 let registry = ModelRegistry::default();
985 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
986
987 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
988 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
989 assert!(resolved.resolved.supports_reasoning);
990 }
991
992 #[test]
993 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
994 let registry = ModelRegistry::default();
995 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
996
997 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
998 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
999 }
1000
1001 #[test]
1002 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1003 let registry = ModelRegistry::default();
1004 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1005
1006 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1007 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1008 }
1009
1010 #[test]
1011 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1012 let registry = ModelRegistry::default();
1013 let resolved =
1014 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1015
1016 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1017 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1018 assert!(resolved.resolved.supports_tools);
1019 assert!(resolved.resolved.supports_reasoning);
1020 assert!(!resolved.used_fallback);
1021 }
1022
1023 #[test]
1024 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1025 let registry = ModelRegistry::default();
1026 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1027
1028 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1029 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1030 assert!(!resolved.used_fallback);
1031 }
1032
1033 #[test]
1034 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1035 let registry = ModelRegistry::default();
1036 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1037
1038 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1039 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1040 assert!(resolved.used_fallback);
1041 }
1042
1043 #[test]
1044 fn openrouter_default_uses_namespaced_model_id() {
1045 let registry = ModelRegistry::default();
1046 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1047
1048 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1049 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1050 }
1051
1052 #[test]
1053 fn xiaomi_mimo_default_uses_canonical_model_id() {
1054 let registry = ModelRegistry::default();
1055 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1056
1057 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1058 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1059 assert!(resolved.resolved.supports_reasoning);
1060 }
1061
1062 #[test]
1063 fn moonshot_default_and_aliases_use_kimi_k27_code() {
1064 let registry = ModelRegistry::default();
1065
1066 for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1067 let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1068
1069 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1070 assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1071 assert!(resolved.resolved.supports_tools);
1072 assert!(resolved.resolved.supports_reasoning);
1073 }
1074 }
1075
1076 #[test]
1077 fn moonshot_explicit_kimi_k26_remains_available() {
1078 let registry = ModelRegistry::default();
1079 let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1080
1081 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1082 assert_eq!(resolved.resolved.id, "kimi-k2.6");
1083 assert!(resolved.resolved.supports_reasoning);
1084 }
1085
1086 #[test]
1087 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1088 let registry = ModelRegistry::default();
1089 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1090 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1091 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1092 assert!(!resolved.resolved.supports_tools);
1093 assert!(!resolved.resolved.supports_reasoning);
1094
1095 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1096 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1097
1098 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1099 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1100 }
1101
1102 #[test]
1103 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1104 let registry = ModelRegistry::default();
1105
1106 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1107 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1108 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1109 assert!(resolved.resolved.supports_tools);
1110 }
1111
1112 #[test]
1113 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1114 let registry = ModelRegistry::default();
1115 let resolved =
1116 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1117
1118 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1119 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1120 assert!(!resolved.used_fallback);
1121 }
1122
1123 #[test]
1124 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1125 let registry = ModelRegistry::default();
1126 let resolved = registry.resolve(
1127 Some("deepseek/deepseek-v4-pro"),
1128 Some(ProviderKind::XiaomiMimo),
1129 );
1130
1131 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1132 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1133 assert!(!resolved.used_fallback);
1134 }
1135
1136 #[test]
1137 fn wanjie_ark_default_uses_reasoner_model_id() {
1138 let registry = ModelRegistry::default();
1139 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1140
1141 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1142 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1143 assert!(resolved.resolved.supports_reasoning);
1144 }
1145
1146 #[test]
1147 fn novita_default_uses_namespaced_model_id() {
1148 let registry = ModelRegistry::default();
1149 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1150
1151 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1152 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1153 }
1154
1155 #[test]
1156 fn fireworks_default_uses_canonical_model_id() {
1157 let registry = ModelRegistry::default();
1158 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1159
1160 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1161 assert_eq!(
1162 resolved.resolved.id,
1163 "accounts/fireworks/models/deepseek-v4-pro"
1164 );
1165 }
1166
1167 #[test]
1168 fn siliconflow_default_uses_canonical_pro_model_id() {
1169 let registry = ModelRegistry::default();
1170 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1171
1172 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1173 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1174 assert!(resolved.resolved.supports_reasoning);
1175 }
1176
1177 #[test]
1178 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1179 let registry = ModelRegistry::default();
1180 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1181
1182 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1183 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1184 assert!(resolved.resolved.supports_reasoning);
1185 }
1186
1187 #[test]
1188 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1189 let registry = ModelRegistry::default();
1190 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1191
1192 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1193 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1194 assert!(resolved.resolved.supports_reasoning);
1195 }
1196
1197 #[test]
1198 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1199 let registry = ModelRegistry::default();
1200 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1201
1202 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1203 assert_eq!(resolved.resolved.id, "trinity-mini");
1204 assert!(!resolved.resolved.supports_reasoning);
1205 }
1206
1207 #[test]
1208 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1209 let registry = ModelRegistry::default();
1210 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1211
1212 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1213 assert_eq!(resolved.resolved.id, "trinity-large-next");
1214 assert!(!resolved.resolved.supports_reasoning);
1215 assert!(!resolved.used_fallback);
1216 }
1217
1218 #[test]
1219 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1220 let registry = ModelRegistry::default();
1221 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1222
1223 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1224 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1225 }
1226
1227 #[test]
1228 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1229 let registry = ModelRegistry::default();
1230 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1231
1232 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1233 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1234 }
1235
1236 #[test]
1237 fn sglang_default_uses_canonical_model_id() {
1238 let registry = ModelRegistry::default();
1239 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1240
1241 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1242 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1243 }
1244
1245 #[test]
1246 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1247 let registry = ModelRegistry::default();
1248 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1249
1250 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1251 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1252 }
1253
1254 #[test]
1255 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1256 let registry = ModelRegistry::default();
1257
1258 for (alias, expected) in [
1259 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1260 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1261 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1262 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1263 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1264 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1265 ("glm-5.1", "z-ai/glm-5.1"),
1266 ("minimax-m3", "minimax/minimax-m3"),
1267 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1268 ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
1269 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1270 ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
1271 (
1272 "nvidia/nemotron-3-ultra",
1273 "nvidia/nemotron-3-ultra-550b-a55b",
1274 ),
1275 ] {
1276 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1277
1278 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1279 assert_eq!(resolved.resolved.id, expected);
1280 assert!(resolved.resolved.supports_tools);
1281 assert!(resolved.resolved.supports_reasoning);
1282 }
1283 }
1284
1285 #[test]
1286 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
1287 let registry = ModelRegistry::default();
1288 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
1289
1290 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1291 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1292 }
1293
1294 #[test]
1295 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
1296 let registry = ModelRegistry::default();
1297 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
1298
1299 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1300 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1301 }
1302
1303 #[test]
1304 fn vllm_default_uses_canonical_model_id() {
1305 let registry = ModelRegistry::default();
1306 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
1307
1308 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1309 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1310 }
1311
1312 #[test]
1313 fn ollama_default_uses_small_local_model_id() {
1314 let registry = ModelRegistry::default();
1315 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
1316
1317 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1318 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
1319 assert!(!resolved.resolved.supports_reasoning);
1320 }
1321
1322 #[test]
1323 fn ollama_requested_model_tag_is_preserved() {
1324 let registry = ModelRegistry::default();
1325 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
1326
1327 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1328 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
1329 assert!(!resolved.used_fallback);
1330 }
1331
1332 #[test]
1333 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
1334 let registry = ModelRegistry::default();
1335 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
1336
1337 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1338 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1339 }
1340
1341 #[test]
1342 fn preserves_requested_model_casing_for_third_party_providers() {
1343 let registry = ModelRegistry::default();
1344 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
1345
1346 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1347 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1348 }
1349
1350 #[test]
1351 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
1352 let registry = ModelRegistry::default();
1353 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
1354
1355 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1356 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1358 }
1359
1360 #[test]
1361 fn preserves_requested_model_casing_without_surrounding_whitespace() {
1362 let registry = ModelRegistry::default();
1363 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
1364
1365 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1366 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1367 }
1368
1369 #[test]
1370 fn alias_match_does_not_override_requested_casing() {
1371 let registry = ModelRegistry::default();
1372 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
1373
1374 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1375 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1376 }
1377
1378 #[test]
1379 fn model_family_classifies_known_model_ids() {
1380 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
1381 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
1382 assert_eq!(
1383 model_family("anthropic/claude-opus-4-7"),
1384 ModelFamily::Anthropic
1385 );
1386 assert_eq!(
1387 model_family("meta-llama/llama-3.3-70b-instruct"),
1388 ModelFamily::Meta
1389 );
1390 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
1391 }
1392
1393 #[test]
1394 fn model_family_uses_underlying_model_for_router_ids() {
1395 assert_eq!(
1396 model_family("groq/llama-3.3-70b-versatile"),
1397 ModelFamily::Meta
1398 );
1399 assert_eq!(
1400 model_family("openrouter/openai/gpt-5.4"),
1401 ModelFamily::OpenAI
1402 );
1403 assert_eq!(
1404 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
1405 ModelFamily::DeepSeek
1406 );
1407 }
1408
1409 #[test]
1410 fn model_family_covers_prominent_google_and_mistral_model_names() {
1411 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
1412 assert_eq!(
1413 model_family("mistralai/mixtral-8x22b"),
1414 ModelFamily::Mistral
1415 );
1416 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
1417 }
1418
1419 #[test]
1420 fn model_family_falls_back_to_inferencer_for_unknown_models() {
1421 assert_eq!(
1422 model_family("custom-gateway/my-private-model"),
1423 ModelFamily::Inferencer
1424 );
1425 assert_eq!(model_family(""), ModelFamily::Inferencer);
1426 }
1427}