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.6".to_string(),
291 provider: ProviderKind::Openrouter,
292 aliases: vec!["openrouter-kimi-k2.6".to_string()],
293 supports_tools: true,
294 supports_reasoning: true,
295 },
296 ModelInfo {
297 id: "minimax/minimax-m3".to_string(),
298 provider: ProviderKind::Openrouter,
299 aliases: vec![
300 "minimax-m3".to_string(),
301 "minimax-m-3".to_string(),
302 "openrouter-minimax-m3".to_string(),
303 ],
304 supports_tools: true,
305 supports_reasoning: true,
306 },
307 ModelInfo {
308 id: "z-ai/glm-5.1".to_string(),
309 provider: ProviderKind::Openrouter,
310 aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
311 supports_tools: true,
312 supports_reasoning: true,
313 },
314 ModelInfo {
315 id: "tencent/hy3-preview".to_string(),
316 provider: ProviderKind::Openrouter,
317 aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
318 supports_tools: true,
319 supports_reasoning: true,
320 },
321 ModelInfo {
322 id: "google/gemma-4-31b-it".to_string(),
323 provider: ProviderKind::Openrouter,
324 aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
325 supports_tools: true,
326 supports_reasoning: true,
327 },
328 ModelInfo {
329 id: "google/gemma-4-26b-a4b-it".to_string(),
330 provider: ProviderKind::Openrouter,
331 aliases: vec![
332 "gemma-4-26b-a4b".to_string(),
333 "gemma-4-26b-a4b-it".to_string(),
334 ],
335 supports_tools: true,
336 supports_reasoning: true,
337 },
338 ModelInfo {
339 id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
340 provider: ProviderKind::Openrouter,
341 aliases: vec![
342 "nemotron-3-nano-omni".to_string(),
343 "nemotron-3-nano-omni-reasoning".to_string(),
344 ],
345 supports_tools: true,
346 supports_reasoning: true,
347 },
348 ModelInfo {
349 id: "mimo-v2.5-pro".to_string(),
350 provider: ProviderKind::XiaomiMimo,
351 aliases: vec![
352 "mimo".to_string(),
353 "pro".to_string(),
354 "xiaomi-mimo-v2.5-pro".to_string(),
355 "xiaomi-mimo-v2-5-pro".to_string(),
356 ],
357 supports_tools: true,
358 supports_reasoning: true,
359 },
360 ModelInfo {
361 id: "mimo-v2.5".to_string(),
362 provider: ProviderKind::XiaomiMimo,
363 aliases: vec![
364 "omni".to_string(),
365 "mimo-omni".to_string(),
366 "v2.5-omni".to_string(),
367 "mimo-v2.5-omni".to_string(),
368 "xiaomi-mimo-v2.5".to_string(),
369 "xiaomi-mimo-v2.5-omni".to_string(),
370 ],
371 supports_tools: true,
372 supports_reasoning: true,
373 },
374 ModelInfo {
375 id: "mimo-v2.5-asr".to_string(),
376 provider: ProviderKind::XiaomiMimo,
377 aliases: vec![
378 "asr".to_string(),
379 "speech-to-text".to_string(),
380 "transcribe".to_string(),
381 ],
382 supports_tools: false,
383 supports_reasoning: false,
384 },
385 ModelInfo {
386 id: "mimo-v2.5-tts".to_string(),
387 provider: ProviderKind::XiaomiMimo,
388 aliases: vec![
389 "tts".to_string(),
390 "speech".to_string(),
391 "mimo-tts".to_string(),
392 ],
393 supports_tools: false,
394 supports_reasoning: false,
395 },
396 ModelInfo {
397 id: "mimo-v2.5-tts-voicedesign".to_string(),
398 provider: ProviderKind::XiaomiMimo,
399 aliases: vec![
400 "voicedesign".to_string(),
401 "voice-design".to_string(),
402 "mimo-voice-design".to_string(),
403 ],
404 supports_tools: false,
405 supports_reasoning: false,
406 },
407 ModelInfo {
408 id: "mimo-v2.5-tts-voiceclone".to_string(),
409 provider: ProviderKind::XiaomiMimo,
410 aliases: vec![
411 "voiceclone".to_string(),
412 "voice-clone".to_string(),
413 "mimo-voice-clone".to_string(),
414 ],
415 supports_tools: false,
416 supports_reasoning: false,
417 },
418 ModelInfo {
419 id: "mimo-v2-tts".to_string(),
420 provider: ProviderKind::XiaomiMimo,
421 aliases: vec!["mimo-v2-speech".to_string()],
422 supports_tools: false,
423 supports_reasoning: false,
424 },
425 ModelInfo {
426 id: "deepseek/deepseek-v4-pro".to_string(),
427 provider: ProviderKind::Novita,
428 aliases: vec![
429 "deepseek-v4-pro".to_string(),
430 "novita-deepseek-v4-pro".to_string(),
431 ],
432 supports_tools: true,
433 supports_reasoning: true,
434 },
435 ModelInfo {
436 id: "deepseek/deepseek-v4-flash".to_string(),
437 provider: ProviderKind::Novita,
438 aliases: vec![
439 "deepseek-v4-flash".to_string(),
440 "deepseek-chat".to_string(),
441 "deepseek-reasoner".to_string(),
442 "novita-deepseek-v4-flash".to_string(),
443 ],
444 supports_tools: true,
445 supports_reasoning: true,
446 },
447 ModelInfo {
448 id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
449 provider: ProviderKind::Fireworks,
450 aliases: vec![
451 "deepseek-v4-pro".to_string(),
452 "fireworks-deepseek-v4-pro".to_string(),
453 ],
454 supports_tools: true,
455 supports_reasoning: true,
456 },
457 ModelInfo {
458 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
459 provider: ProviderKind::Siliconflow,
460 aliases: vec![
461 "deepseek-v4-pro".to_string(),
462 "deepseek-reasoner".to_string(),
463 "deepseek-r1".to_string(),
464 "siliconflow-deepseek-v4-pro".to_string(),
465 ],
466 supports_tools: true,
467 supports_reasoning: true,
468 },
469 ModelInfo {
470 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
471 provider: ProviderKind::Siliconflow,
472 aliases: vec![
473 "deepseek-v4-flash".to_string(),
474 "deepseek-chat".to_string(),
475 "deepseek-v3".to_string(),
476 "siliconflow-deepseek-v4-flash".to_string(),
477 ],
478 supports_tools: true,
479 supports_reasoning: true,
480 },
481 ModelInfo {
482 id: "trinity-large-preview".to_string(),
483 provider: ProviderKind::Arcee,
484 aliases: vec!["arcee-trinity-large-preview".to_string()],
485 supports_tools: true,
486 supports_reasoning: false,
487 },
488 ModelInfo {
489 id: "kimi-k2.6".to_string(),
490 provider: ProviderKind::Moonshot,
491 aliases: vec![
492 "kimi".to_string(),
493 "kimi-k2".to_string(),
494 "moonshot-kimi-k2.6".to_string(),
495 ],
496 supports_tools: true,
497 supports_reasoning: true,
498 },
499 ModelInfo {
500 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
501 provider: ProviderKind::Sglang,
502 aliases: vec![
503 "deepseek-v4-pro".to_string(),
504 "sglang-deepseek-v4-pro".to_string(),
505 ],
506 supports_tools: true,
507 supports_reasoning: true,
508 },
509 ModelInfo {
510 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
511 provider: ProviderKind::Sglang,
512 aliases: vec![
513 "deepseek-v4-flash".to_string(),
514 "deepseek-chat".to_string(),
515 "deepseek-reasoner".to_string(),
516 "sglang-deepseek-v4-flash".to_string(),
517 ],
518 supports_tools: true,
519 supports_reasoning: true,
520 },
521 ModelInfo {
522 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
523 provider: ProviderKind::Vllm,
524 aliases: vec![
525 "deepseek-v4-pro".to_string(),
526 "vllm-deepseek-v4-pro".to_string(),
527 ],
528 supports_tools: true,
529 supports_reasoning: true,
530 },
531 ModelInfo {
532 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
533 provider: ProviderKind::Vllm,
534 aliases: vec![
535 "deepseek-v4-flash".to_string(),
536 "deepseek-chat".to_string(),
537 "deepseek-reasoner".to_string(),
538 "vllm-deepseek-v4-flash".to_string(),
539 ],
540 supports_tools: true,
541 supports_reasoning: true,
542 },
543 ModelInfo {
544 id: "deepseek-coder:1.3b".to_string(),
545 provider: ProviderKind::Ollama,
546 aliases: vec![],
547 supports_tools: true,
548 supports_reasoning: false,
549 },
550 ModelInfo {
551 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
552 provider: ProviderKind::Huggingface,
553 aliases: vec![
554 "deepseek-v4-pro".to_string(),
555 "hf-deepseek-v4-pro".to_string(),
556 ],
557 supports_tools: true,
558 supports_reasoning: true,
559 },
560 ModelInfo {
561 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
562 provider: ProviderKind::Huggingface,
563 aliases: vec![
564 "deepseek-v4-flash".to_string(),
565 "deepseek-chat".to_string(),
566 "deepseek-reasoner".to_string(),
567 "hf-deepseek-v4-flash".to_string(),
568 ],
569 supports_tools: true,
570 supports_reasoning: true,
571 },
572 ModelInfo {
574 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
575 provider: ProviderKind::Together,
576 aliases: vec![
577 "deepseek-v4-pro".to_string(),
578 "together-deepseek-v4-pro".to_string(),
579 ],
580 supports_tools: true,
581 supports_reasoning: true,
582 },
583 ModelInfo {
584 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
585 provider: ProviderKind::Together,
586 aliases: vec![
587 "deepseek-v4-flash".to_string(),
588 "deepseek-chat".to_string(),
589 "together-deepseek-v4-flash".to_string(),
590 ],
591 supports_tools: true,
592 supports_reasoning: true,
593 },
594 ModelInfo {
596 id: "qwen/qwen3.7-max".to_string(),
597 provider: ProviderKind::Openrouter,
598 aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
599 supports_tools: true,
600 supports_reasoning: true,
601 },
602 ModelInfo {
604 id: "gpt-5.5".to_string(),
605 provider: ProviderKind::OpenaiCodex,
606 aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
607 supports_tools: true,
608 supports_reasoning: true,
609 },
610 ModelInfo {
612 id: "claude-opus-4-8".to_string(),
613 provider: ProviderKind::Anthropic,
614 aliases: vec!["opus".to_string(), "claude-opus".to_string()],
615 supports_tools: true,
616 supports_reasoning: true,
617 },
618 ModelInfo {
619 id: "claude-sonnet-4-6".to_string(),
620 provider: ProviderKind::Anthropic,
621 aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
622 supports_tools: true,
623 supports_reasoning: true,
624 },
625 ModelInfo {
626 id: "claude-haiku-4-5".to_string(),
627 provider: ProviderKind::Anthropic,
628 aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
629 supports_tools: true,
630 supports_reasoning: false,
631 },
632 ModelInfo {
634 id: "minimax/minimax-2.7".to_string(),
635 provider: ProviderKind::Openrouter,
636 aliases: vec![
637 "minimax-2.7".to_string(),
638 "minimax-2-7".to_string(),
639 "openrouter-minimax-2.7".to_string(),
640 ],
641 supports_tools: true,
642 supports_reasoning: true,
643 },
644 ModelInfo {
646 id: "nvidia/nemotron-3-ultra".to_string(),
647 provider: ProviderKind::Openrouter,
648 aliases: vec![
649 "nemotron-3-ultra".to_string(),
650 "nvidia-nemotron-3-ultra".to_string(),
651 ],
652 supports_tools: true,
653 supports_reasoning: true,
654 },
655 ];
656 Self::new(models)
657 }
658}
659
660impl ModelRegistry {
661 #[must_use]
667 pub fn new(models: Vec<ModelInfo>) -> Self {
668 let mut alias_map = HashMap::new();
669 for (idx, model) in models.iter().enumerate() {
670 alias_map.entry(normalize(&model.id)).or_insert(idx);
671 for alias in &model.aliases {
672 alias_map.entry(normalize(alias)).or_insert(idx);
673 }
674 }
675 Self { models, alias_map }
676 }
677
678 #[must_use]
680 pub fn list(&self) -> Vec<ModelInfo> {
681 self.models.clone()
682 }
683
684 #[must_use]
696 pub fn resolve(
697 &self,
698 requested: Option<&str>,
699 provider_hint: Option<ProviderKind>,
700 ) -> ModelResolution {
701 let mut fallback_chain = Vec::new();
702
703 if let Some(name) = requested {
704 fallback_chain.push(format!("requested:{name}"));
705 if provider_hint == Some(ProviderKind::Ollama) {
706 return ModelResolution {
707 requested: Some(name.to_string()),
708 resolved: ModelInfo {
709 id: name.trim().to_string(),
710 provider: ProviderKind::Ollama,
711 aliases: Vec::new(),
712 supports_tools: true,
713 supports_reasoning: false,
714 },
715 used_fallback: false,
716 fallback_chain,
717 };
718 }
719 if let Some(provider) = provider_hint
720 && let Some(model) = self
721 .models
722 .iter()
723 .find(|m| m.provider == provider && model_matches(m, name))
724 .cloned()
725 {
726 return ModelResolution {
727 requested: Some(name.to_string()),
728 resolved: model,
729 used_fallback: false,
730 fallback_chain,
731 };
732 }
733 if provider_hint == Some(ProviderKind::Atlascloud)
734 && let Some(model) = atlascloud_passthrough_model(name)
735 {
736 return ModelResolution {
737 requested: Some(name.to_string()),
738 resolved: model,
739 used_fallback: false,
740 fallback_chain,
741 };
742 }
743 if provider_hint == Some(ProviderKind::Arcee)
744 && let Some(model) = arcee_passthrough_model(name)
745 {
746 return ModelResolution {
747 requested: Some(name.to_string()),
748 resolved: model,
749 used_fallback: false,
750 fallback_chain,
751 };
752 }
753 if provider_hint == Some(ProviderKind::XiaomiMimo)
754 && let Some(model) = xiaomi_mimo_passthrough_model(name)
755 {
756 return ModelResolution {
757 requested: Some(name.to_string()),
758 resolved: model,
759 used_fallback: false,
760 fallback_chain,
761 };
762 }
763 if let Some(idx) = self.alias_map.get(&normalize(name)) {
764 return ModelResolution {
765 requested: Some(name.to_string()),
766 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
767 used_fallback: false,
768 fallback_chain,
769 };
770 }
771 }
772
773 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
774 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
775 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
776 return ModelResolution {
777 requested: requested.map(ToOwned::to_owned),
778 resolved: model,
779 used_fallback: true,
780 fallback_chain,
781 };
782 }
783
784 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
785 id: "deepseek-v4-pro".to_string(),
786 provider: ProviderKind::Deepseek,
787 aliases: Vec::new(),
788 supports_tools: true,
789 supports_reasoning: true,
790 });
791 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
792 ModelResolution {
793 requested: requested.map(ToOwned::to_owned),
794 resolved: final_fallback,
795 used_fallback: true,
796 fallback_chain,
797 }
798 }
799}
800
801fn normalize(value: &str) -> String {
802 value.trim().to_ascii_lowercase()
803}
804
805#[must_use]
806pub fn model_family(model_id: &str) -> ModelFamily {
808 let normalized = normalize(model_id);
809 if normalized.is_empty() {
810 return ModelFamily::Inferencer;
811 }
812
813 if normalized.contains("deepseek") {
814 return ModelFamily::DeepSeek;
815 }
816 if normalized.contains("claude") || normalized.contains("anthropic") {
817 return ModelFamily::Anthropic;
818 }
819 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
820 return ModelFamily::GptOss;
821 }
822 if normalized.starts_with("gpt-")
823 || normalized.contains("/gpt-")
824 || normalized.contains("openai/")
825 {
826 return ModelFamily::OpenAI;
827 }
828 if normalized.contains("gemini")
829 || normalized.contains("gemma")
830 || normalized.contains("google/")
831 {
832 return ModelFamily::Google;
833 }
834 if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/")
835 {
836 return ModelFamily::Meta;
837 }
838 if normalized.contains("mistral")
839 || normalized.contains("mixtral")
840 || normalized.contains("codestral")
841 {
842 return ModelFamily::Mistral;
843 }
844 if normalized.contains("qwen") {
845 return ModelFamily::Qwen;
846 }
847 if normalized.contains("grok") {
848 return ModelFamily::Grok;
849 }
850 if normalized.contains("cohere") || normalized.contains("command-r") {
851 return ModelFamily::Cohere;
852 }
853
854 ModelFamily::Inferencer
855}
856
857fn model_matches(model: &ModelInfo, requested: &str) -> bool {
858 let requested = normalize(requested);
859 normalize(&model.id) == requested
860 || model
861 .aliases
862 .iter()
863 .any(|alias| normalize(alias) == requested)
864}
865
866fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
867 let requested = requested.trim();
868 if model.id.eq_ignore_ascii_case(requested) {
869 model.id = requested.to_string();
870 }
871 model
872}
873
874fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
875 let requested = requested.trim();
876 if requested.is_empty() || !requested.contains('/') {
877 return None;
878 }
879
880 Some(ModelInfo {
881 id: requested.to_string(),
882 provider: ProviderKind::Atlascloud,
883 aliases: Vec::new(),
884 supports_tools: true,
885 supports_reasoning: true,
886 })
887}
888
889fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
890 let requested = requested.trim();
891 if requested.is_empty() {
892 return None;
893 }
894 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
895
896 Some(ModelInfo {
897 id: requested.to_string(),
898 provider: ProviderKind::Arcee,
899 aliases: Vec::new(),
900 supports_tools: true,
901 supports_reasoning,
902 })
903}
904
905fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
906 let requested = requested.trim();
907 if requested.is_empty() || requested.chars().any(char::is_control) {
908 return None;
909 }
910
911 Some(ModelInfo {
912 id: requested.to_string(),
913 provider: ProviderKind::XiaomiMimo,
914 aliases: Vec::new(),
915 supports_tools: true,
916 supports_reasoning: true,
917 })
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923
924 #[test]
925 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
926 let registry = ModelRegistry::default();
927 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
928
929 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
930 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
931 }
932
933 #[test]
934 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
935 let registry = ModelRegistry::default();
936 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
937
938 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
939 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
940 }
941
942 #[test]
943 fn nvidia_nim_default_uses_catalog_model_id() {
944 let registry = ModelRegistry::default();
945 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
946
947 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
948 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
949 }
950
951 #[test]
952 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
953 let registry = ModelRegistry::default();
954 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
955
956 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
957 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
958 }
959
960 #[test]
961 fn atlascloud_default_uses_namespaced_model_id() {
962 let registry = ModelRegistry::default();
963 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
964
965 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
966 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
967 assert!(resolved.resolved.supports_reasoning);
968 }
969
970 #[test]
971 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
972 let registry = ModelRegistry::default();
973 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
974
975 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
976 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
977 }
978
979 #[test]
980 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
981 let registry = ModelRegistry::default();
982 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
983
984 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
985 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
986 }
987
988 #[test]
989 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
990 let registry = ModelRegistry::default();
991 let resolved =
992 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
993
994 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
995 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
996 assert!(resolved.resolved.supports_tools);
997 assert!(resolved.resolved.supports_reasoning);
998 assert!(!resolved.used_fallback);
999 }
1000
1001 #[test]
1002 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1003 let registry = ModelRegistry::default();
1004 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1005
1006 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1007 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1008 assert!(!resolved.used_fallback);
1009 }
1010
1011 #[test]
1012 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1013 let registry = ModelRegistry::default();
1014 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1015
1016 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1017 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1018 assert!(resolved.used_fallback);
1019 }
1020
1021 #[test]
1022 fn openrouter_default_uses_namespaced_model_id() {
1023 let registry = ModelRegistry::default();
1024 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1025
1026 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1027 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1028 }
1029
1030 #[test]
1031 fn xiaomi_mimo_default_uses_canonical_model_id() {
1032 let registry = ModelRegistry::default();
1033 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1034
1035 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1036 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1037 assert!(resolved.resolved.supports_reasoning);
1038 }
1039
1040 #[test]
1041 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1042 let registry = ModelRegistry::default();
1043 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1044 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1045 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1046 assert!(!resolved.resolved.supports_tools);
1047 assert!(!resolved.resolved.supports_reasoning);
1048
1049 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1050 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1051
1052 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1053 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1054 }
1055
1056 #[test]
1057 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1058 let registry = ModelRegistry::default();
1059
1060 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1061 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1062 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1063 assert!(resolved.resolved.supports_tools);
1064 }
1065
1066 #[test]
1067 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1068 let registry = ModelRegistry::default();
1069 let resolved =
1070 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1071
1072 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1073 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1074 assert!(!resolved.used_fallback);
1075 }
1076
1077 #[test]
1078 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1079 let registry = ModelRegistry::default();
1080 let resolved = registry.resolve(
1081 Some("deepseek/deepseek-v4-pro"),
1082 Some(ProviderKind::XiaomiMimo),
1083 );
1084
1085 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1086 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1087 assert!(!resolved.used_fallback);
1088 }
1089
1090 #[test]
1091 fn wanjie_ark_default_uses_reasoner_model_id() {
1092 let registry = ModelRegistry::default();
1093 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1094
1095 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1096 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1097 assert!(resolved.resolved.supports_reasoning);
1098 }
1099
1100 #[test]
1101 fn novita_default_uses_namespaced_model_id() {
1102 let registry = ModelRegistry::default();
1103 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1104
1105 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1106 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1107 }
1108
1109 #[test]
1110 fn fireworks_default_uses_canonical_model_id() {
1111 let registry = ModelRegistry::default();
1112 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1113
1114 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1115 assert_eq!(
1116 resolved.resolved.id,
1117 "accounts/fireworks/models/deepseek-v4-pro"
1118 );
1119 }
1120
1121 #[test]
1122 fn siliconflow_default_uses_canonical_pro_model_id() {
1123 let registry = ModelRegistry::default();
1124 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1125
1126 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1127 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1128 assert!(resolved.resolved.supports_reasoning);
1129 }
1130
1131 #[test]
1132 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1133 let registry = ModelRegistry::default();
1134 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1135
1136 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1137 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1138 assert!(resolved.resolved.supports_reasoning);
1139 }
1140
1141 #[test]
1142 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1143 let registry = ModelRegistry::default();
1144 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1145
1146 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1147 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1148 assert!(resolved.resolved.supports_reasoning);
1149 }
1150
1151 #[test]
1152 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1153 let registry = ModelRegistry::default();
1154 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1155
1156 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1157 assert_eq!(resolved.resolved.id, "trinity-mini");
1158 assert!(!resolved.resolved.supports_reasoning);
1159 }
1160
1161 #[test]
1162 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1163 let registry = ModelRegistry::default();
1164 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1165
1166 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1167 assert_eq!(resolved.resolved.id, "trinity-large-next");
1168 assert!(!resolved.resolved.supports_reasoning);
1169 assert!(!resolved.used_fallback);
1170 }
1171
1172 #[test]
1173 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1174 let registry = ModelRegistry::default();
1175 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1176
1177 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1178 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1179 }
1180
1181 #[test]
1182 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1183 let registry = ModelRegistry::default();
1184 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1185
1186 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1187 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1188 }
1189
1190 #[test]
1191 fn sglang_default_uses_canonical_model_id() {
1192 let registry = ModelRegistry::default();
1193 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1194
1195 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1196 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1197 }
1198
1199 #[test]
1200 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1201 let registry = ModelRegistry::default();
1202 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1203
1204 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1205 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1206 }
1207
1208 #[test]
1209 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1210 let registry = ModelRegistry::default();
1211
1212 for (alias, expected) in [
1213 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1214 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1215 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1216 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1217 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1218 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1219 ("glm-5.1", "z-ai/glm-5.1"),
1220 ("minimax-m3", "minimax/minimax-m3"),
1221 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1222 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1223 ] {
1224 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1225
1226 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1227 assert_eq!(resolved.resolved.id, expected);
1228 assert!(resolved.resolved.supports_tools);
1229 assert!(resolved.resolved.supports_reasoning);
1230 }
1231 }
1232
1233 #[test]
1234 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
1235 let registry = ModelRegistry::default();
1236 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
1237
1238 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1239 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1240 }
1241
1242 #[test]
1243 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
1244 let registry = ModelRegistry::default();
1245 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
1246
1247 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1248 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1249 }
1250
1251 #[test]
1252 fn vllm_default_uses_canonical_model_id() {
1253 let registry = ModelRegistry::default();
1254 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
1255
1256 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1257 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1258 }
1259
1260 #[test]
1261 fn ollama_default_uses_small_local_model_id() {
1262 let registry = ModelRegistry::default();
1263 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
1264
1265 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1266 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
1267 assert!(!resolved.resolved.supports_reasoning);
1268 }
1269
1270 #[test]
1271 fn ollama_requested_model_tag_is_preserved() {
1272 let registry = ModelRegistry::default();
1273 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
1274
1275 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1276 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
1277 assert!(!resolved.used_fallback);
1278 }
1279
1280 #[test]
1281 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
1282 let registry = ModelRegistry::default();
1283 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
1284
1285 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1286 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1287 }
1288
1289 #[test]
1290 fn preserves_requested_model_casing_for_third_party_providers() {
1291 let registry = ModelRegistry::default();
1292 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
1293
1294 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1295 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1296 }
1297
1298 #[test]
1299 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
1300 let registry = ModelRegistry::default();
1301 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
1302
1303 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1304 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1306 }
1307
1308 #[test]
1309 fn preserves_requested_model_casing_without_surrounding_whitespace() {
1310 let registry = ModelRegistry::default();
1311 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
1312
1313 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1314 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1315 }
1316
1317 #[test]
1318 fn alias_match_does_not_override_requested_casing() {
1319 let registry = ModelRegistry::default();
1320 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
1321
1322 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1323 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1324 }
1325
1326 #[test]
1327 fn model_family_classifies_known_model_ids() {
1328 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
1329 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
1330 assert_eq!(
1331 model_family("anthropic/claude-opus-4-7"),
1332 ModelFamily::Anthropic
1333 );
1334 assert_eq!(
1335 model_family("meta-llama/llama-3.3-70b-instruct"),
1336 ModelFamily::Meta
1337 );
1338 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
1339 }
1340
1341 #[test]
1342 fn model_family_uses_underlying_model_for_router_ids() {
1343 assert_eq!(
1344 model_family("groq/llama-3.3-70b-versatile"),
1345 ModelFamily::Meta
1346 );
1347 assert_eq!(
1348 model_family("openrouter/openai/gpt-5.4"),
1349 ModelFamily::OpenAI
1350 );
1351 assert_eq!(
1352 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
1353 ModelFamily::DeepSeek
1354 );
1355 }
1356
1357 #[test]
1358 fn model_family_covers_prominent_google_and_mistral_model_names() {
1359 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
1360 assert_eq!(
1361 model_family("mistralai/mixtral-8x22b"),
1362 ModelFamily::Mistral
1363 );
1364 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
1365 }
1366
1367 #[test]
1368 fn model_family_falls_back_to_inferencer_for_unknown_models() {
1369 assert_eq!(
1370 model_family("custom-gateway/my-private-model"),
1371 ModelFamily::Inferencer
1372 );
1373 assert_eq!(model_family(""), ModelFamily::Inferencer);
1374 }
1375}