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: "minimax/minimax-2.7".to_string(),
613 provider: ProviderKind::Openrouter,
614 aliases: vec![
615 "minimax-2.7".to_string(),
616 "minimax-2-7".to_string(),
617 "openrouter-minimax-2.7".to_string(),
618 ],
619 supports_tools: true,
620 supports_reasoning: true,
621 },
622 ModelInfo {
624 id: "nvidia/nemotron-3-ultra".to_string(),
625 provider: ProviderKind::Openrouter,
626 aliases: vec![
627 "nemotron-3-ultra".to_string(),
628 "nvidia-nemotron-3-ultra".to_string(),
629 ],
630 supports_tools: true,
631 supports_reasoning: true,
632 },
633 ];
634 Self::new(models)
635 }
636}
637
638impl ModelRegistry {
639 #[must_use]
645 pub fn new(models: Vec<ModelInfo>) -> Self {
646 let mut alias_map = HashMap::new();
647 for (idx, model) in models.iter().enumerate() {
648 alias_map.entry(normalize(&model.id)).or_insert(idx);
649 for alias in &model.aliases {
650 alias_map.entry(normalize(alias)).or_insert(idx);
651 }
652 }
653 Self { models, alias_map }
654 }
655
656 #[must_use]
658 pub fn list(&self) -> Vec<ModelInfo> {
659 self.models.clone()
660 }
661
662 #[must_use]
674 pub fn resolve(
675 &self,
676 requested: Option<&str>,
677 provider_hint: Option<ProviderKind>,
678 ) -> ModelResolution {
679 let mut fallback_chain = Vec::new();
680
681 if let Some(name) = requested {
682 fallback_chain.push(format!("requested:{name}"));
683 if provider_hint == Some(ProviderKind::Ollama) {
684 return ModelResolution {
685 requested: Some(name.to_string()),
686 resolved: ModelInfo {
687 id: name.trim().to_string(),
688 provider: ProviderKind::Ollama,
689 aliases: Vec::new(),
690 supports_tools: true,
691 supports_reasoning: false,
692 },
693 used_fallback: false,
694 fallback_chain,
695 };
696 }
697 if let Some(provider) = provider_hint
698 && let Some(model) = self
699 .models
700 .iter()
701 .find(|m| m.provider == provider && model_matches(m, name))
702 .cloned()
703 {
704 return ModelResolution {
705 requested: Some(name.to_string()),
706 resolved: model,
707 used_fallback: false,
708 fallback_chain,
709 };
710 }
711 if provider_hint == Some(ProviderKind::Atlascloud)
712 && let Some(model) = atlascloud_passthrough_model(name)
713 {
714 return ModelResolution {
715 requested: Some(name.to_string()),
716 resolved: model,
717 used_fallback: false,
718 fallback_chain,
719 };
720 }
721 if provider_hint == Some(ProviderKind::Arcee)
722 && let Some(model) = arcee_passthrough_model(name)
723 {
724 return ModelResolution {
725 requested: Some(name.to_string()),
726 resolved: model,
727 used_fallback: false,
728 fallback_chain,
729 };
730 }
731 if provider_hint == Some(ProviderKind::XiaomiMimo)
732 && let Some(model) = xiaomi_mimo_passthrough_model(name)
733 {
734 return ModelResolution {
735 requested: Some(name.to_string()),
736 resolved: model,
737 used_fallback: false,
738 fallback_chain,
739 };
740 }
741 if let Some(idx) = self.alias_map.get(&normalize(name)) {
742 return ModelResolution {
743 requested: Some(name.to_string()),
744 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
745 used_fallback: false,
746 fallback_chain,
747 };
748 }
749 }
750
751 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
752 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
753 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
754 return ModelResolution {
755 requested: requested.map(ToOwned::to_owned),
756 resolved: model,
757 used_fallback: true,
758 fallback_chain,
759 };
760 }
761
762 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
763 id: "deepseek-v4-pro".to_string(),
764 provider: ProviderKind::Deepseek,
765 aliases: Vec::new(),
766 supports_tools: true,
767 supports_reasoning: true,
768 });
769 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
770 ModelResolution {
771 requested: requested.map(ToOwned::to_owned),
772 resolved: final_fallback,
773 used_fallback: true,
774 fallback_chain,
775 }
776 }
777}
778
779fn normalize(value: &str) -> String {
780 value.trim().to_ascii_lowercase()
781}
782
783#[must_use]
784pub fn model_family(model_id: &str) -> ModelFamily {
786 let normalized = normalize(model_id);
787 if normalized.is_empty() {
788 return ModelFamily::Inferencer;
789 }
790
791 if normalized.contains("deepseek") {
792 return ModelFamily::DeepSeek;
793 }
794 if normalized.contains("claude") || normalized.contains("anthropic") {
795 return ModelFamily::Anthropic;
796 }
797 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
798 return ModelFamily::GptOss;
799 }
800 if normalized.starts_with("gpt-")
801 || normalized.contains("/gpt-")
802 || normalized.contains("openai/")
803 {
804 return ModelFamily::OpenAI;
805 }
806 if normalized.contains("gemini")
807 || normalized.contains("gemma")
808 || normalized.contains("google/")
809 {
810 return ModelFamily::Google;
811 }
812 if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/")
813 {
814 return ModelFamily::Meta;
815 }
816 if normalized.contains("mistral")
817 || normalized.contains("mixtral")
818 || normalized.contains("codestral")
819 {
820 return ModelFamily::Mistral;
821 }
822 if normalized.contains("qwen") {
823 return ModelFamily::Qwen;
824 }
825 if normalized.contains("grok") {
826 return ModelFamily::Grok;
827 }
828 if normalized.contains("cohere") || normalized.contains("command-r") {
829 return ModelFamily::Cohere;
830 }
831
832 ModelFamily::Inferencer
833}
834
835fn model_matches(model: &ModelInfo, requested: &str) -> bool {
836 let requested = normalize(requested);
837 normalize(&model.id) == requested
838 || model
839 .aliases
840 .iter()
841 .any(|alias| normalize(alias) == requested)
842}
843
844fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
845 let requested = requested.trim();
846 if model.id.eq_ignore_ascii_case(requested) {
847 model.id = requested.to_string();
848 }
849 model
850}
851
852fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
853 let requested = requested.trim();
854 if requested.is_empty() || !requested.contains('/') {
855 return None;
856 }
857
858 Some(ModelInfo {
859 id: requested.to_string(),
860 provider: ProviderKind::Atlascloud,
861 aliases: Vec::new(),
862 supports_tools: true,
863 supports_reasoning: true,
864 })
865}
866
867fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
868 let requested = requested.trim();
869 if requested.is_empty() {
870 return None;
871 }
872 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
873
874 Some(ModelInfo {
875 id: requested.to_string(),
876 provider: ProviderKind::Arcee,
877 aliases: Vec::new(),
878 supports_tools: true,
879 supports_reasoning,
880 })
881}
882
883fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
884 let requested = requested.trim();
885 if requested.is_empty() || requested.chars().any(char::is_control) {
886 return None;
887 }
888
889 Some(ModelInfo {
890 id: requested.to_string(),
891 provider: ProviderKind::XiaomiMimo,
892 aliases: Vec::new(),
893 supports_tools: true,
894 supports_reasoning: true,
895 })
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
904 let registry = ModelRegistry::default();
905 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
906
907 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
908 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
909 }
910
911 #[test]
912 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
913 let registry = ModelRegistry::default();
914 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
915
916 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
917 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
918 }
919
920 #[test]
921 fn nvidia_nim_default_uses_catalog_model_id() {
922 let registry = ModelRegistry::default();
923 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
924
925 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
926 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
927 }
928
929 #[test]
930 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
931 let registry = ModelRegistry::default();
932 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
933
934 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
935 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
936 }
937
938 #[test]
939 fn atlascloud_default_uses_namespaced_model_id() {
940 let registry = ModelRegistry::default();
941 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
942
943 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
944 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
945 assert!(resolved.resolved.supports_reasoning);
946 }
947
948 #[test]
949 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
950 let registry = ModelRegistry::default();
951 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
952
953 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
954 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
955 }
956
957 #[test]
958 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
959 let registry = ModelRegistry::default();
960 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
961
962 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
963 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
964 }
965
966 #[test]
967 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
968 let registry = ModelRegistry::default();
969 let resolved =
970 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
971
972 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
973 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
974 assert!(resolved.resolved.supports_tools);
975 assert!(resolved.resolved.supports_reasoning);
976 assert!(!resolved.used_fallback);
977 }
978
979 #[test]
980 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
981 let registry = ModelRegistry::default();
982 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
983
984 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
985 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
986 assert!(!resolved.used_fallback);
987 }
988
989 #[test]
990 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
991 let registry = ModelRegistry::default();
992 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
993
994 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
995 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
996 assert!(resolved.used_fallback);
997 }
998
999 #[test]
1000 fn openrouter_default_uses_namespaced_model_id() {
1001 let registry = ModelRegistry::default();
1002 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1003
1004 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1005 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1006 }
1007
1008 #[test]
1009 fn xiaomi_mimo_default_uses_canonical_model_id() {
1010 let registry = ModelRegistry::default();
1011 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1012
1013 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1014 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1015 assert!(resolved.resolved.supports_reasoning);
1016 }
1017
1018 #[test]
1019 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1020 let registry = ModelRegistry::default();
1021 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1022 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1023 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1024 assert!(!resolved.resolved.supports_tools);
1025 assert!(!resolved.resolved.supports_reasoning);
1026
1027 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1028 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1029
1030 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1031 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1032 }
1033
1034 #[test]
1035 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1036 let registry = ModelRegistry::default();
1037
1038 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1039 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1040 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1041 assert!(resolved.resolved.supports_tools);
1042 }
1043
1044 #[test]
1045 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1046 let registry = ModelRegistry::default();
1047 let resolved =
1048 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1049
1050 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1051 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1052 assert!(!resolved.used_fallback);
1053 }
1054
1055 #[test]
1056 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1057 let registry = ModelRegistry::default();
1058 let resolved = registry.resolve(
1059 Some("deepseek/deepseek-v4-pro"),
1060 Some(ProviderKind::XiaomiMimo),
1061 );
1062
1063 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1064 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1065 assert!(!resolved.used_fallback);
1066 }
1067
1068 #[test]
1069 fn wanjie_ark_default_uses_reasoner_model_id() {
1070 let registry = ModelRegistry::default();
1071 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1072
1073 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1074 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1075 assert!(resolved.resolved.supports_reasoning);
1076 }
1077
1078 #[test]
1079 fn novita_default_uses_namespaced_model_id() {
1080 let registry = ModelRegistry::default();
1081 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1082
1083 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1084 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1085 }
1086
1087 #[test]
1088 fn fireworks_default_uses_canonical_model_id() {
1089 let registry = ModelRegistry::default();
1090 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1091
1092 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1093 assert_eq!(
1094 resolved.resolved.id,
1095 "accounts/fireworks/models/deepseek-v4-pro"
1096 );
1097 }
1098
1099 #[test]
1100 fn siliconflow_default_uses_canonical_pro_model_id() {
1101 let registry = ModelRegistry::default();
1102 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1103
1104 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1105 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1106 assert!(resolved.resolved.supports_reasoning);
1107 }
1108
1109 #[test]
1110 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1111 let registry = ModelRegistry::default();
1112 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1113
1114 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1115 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1116 assert!(resolved.resolved.supports_reasoning);
1117 }
1118
1119 #[test]
1120 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1121 let registry = ModelRegistry::default();
1122 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1123
1124 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1125 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1126 assert!(resolved.resolved.supports_reasoning);
1127 }
1128
1129 #[test]
1130 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1131 let registry = ModelRegistry::default();
1132 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1133
1134 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1135 assert_eq!(resolved.resolved.id, "trinity-mini");
1136 assert!(!resolved.resolved.supports_reasoning);
1137 }
1138
1139 #[test]
1140 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1141 let registry = ModelRegistry::default();
1142 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1143
1144 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1145 assert_eq!(resolved.resolved.id, "trinity-large-next");
1146 assert!(!resolved.resolved.supports_reasoning);
1147 assert!(!resolved.used_fallback);
1148 }
1149
1150 #[test]
1151 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1152 let registry = ModelRegistry::default();
1153 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1154
1155 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1156 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1157 }
1158
1159 #[test]
1160 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1161 let registry = ModelRegistry::default();
1162 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1163
1164 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1165 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1166 }
1167
1168 #[test]
1169 fn sglang_default_uses_canonical_model_id() {
1170 let registry = ModelRegistry::default();
1171 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1172
1173 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1174 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1175 }
1176
1177 #[test]
1178 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1179 let registry = ModelRegistry::default();
1180 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1181
1182 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1183 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1184 }
1185
1186 #[test]
1187 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1188 let registry = ModelRegistry::default();
1189
1190 for (alias, expected) in [
1191 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1192 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1193 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1194 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1195 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1196 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1197 ("glm-5.1", "z-ai/glm-5.1"),
1198 ("minimax-m3", "minimax/minimax-m3"),
1199 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1200 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1201 ] {
1202 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1203
1204 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1205 assert_eq!(resolved.resolved.id, expected);
1206 assert!(resolved.resolved.supports_tools);
1207 assert!(resolved.resolved.supports_reasoning);
1208 }
1209 }
1210
1211 #[test]
1212 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
1213 let registry = ModelRegistry::default();
1214 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
1215
1216 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1217 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1218 }
1219
1220 #[test]
1221 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
1222 let registry = ModelRegistry::default();
1223 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
1224
1225 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1226 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1227 }
1228
1229 #[test]
1230 fn vllm_default_uses_canonical_model_id() {
1231 let registry = ModelRegistry::default();
1232 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
1233
1234 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1235 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1236 }
1237
1238 #[test]
1239 fn ollama_default_uses_small_local_model_id() {
1240 let registry = ModelRegistry::default();
1241 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
1242
1243 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1244 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
1245 assert!(!resolved.resolved.supports_reasoning);
1246 }
1247
1248 #[test]
1249 fn ollama_requested_model_tag_is_preserved() {
1250 let registry = ModelRegistry::default();
1251 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
1252
1253 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1254 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
1255 assert!(!resolved.used_fallback);
1256 }
1257
1258 #[test]
1259 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
1260 let registry = ModelRegistry::default();
1261 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
1262
1263 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1264 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1265 }
1266
1267 #[test]
1268 fn preserves_requested_model_casing_for_third_party_providers() {
1269 let registry = ModelRegistry::default();
1270 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
1271
1272 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1273 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1274 }
1275
1276 #[test]
1277 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
1278 let registry = ModelRegistry::default();
1279 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
1280
1281 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1282 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1284 }
1285
1286 #[test]
1287 fn preserves_requested_model_casing_without_surrounding_whitespace() {
1288 let registry = ModelRegistry::default();
1289 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
1290
1291 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1292 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1293 }
1294
1295 #[test]
1296 fn alias_match_does_not_override_requested_casing() {
1297 let registry = ModelRegistry::default();
1298 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
1299
1300 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1301 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1302 }
1303
1304 #[test]
1305 fn model_family_classifies_known_model_ids() {
1306 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
1307 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
1308 assert_eq!(
1309 model_family("anthropic/claude-opus-4-7"),
1310 ModelFamily::Anthropic
1311 );
1312 assert_eq!(
1313 model_family("meta-llama/llama-3.3-70b-instruct"),
1314 ModelFamily::Meta
1315 );
1316 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
1317 }
1318
1319 #[test]
1320 fn model_family_uses_underlying_model_for_router_ids() {
1321 assert_eq!(
1322 model_family("groq/llama-3.3-70b-versatile"),
1323 ModelFamily::Meta
1324 );
1325 assert_eq!(
1326 model_family("openrouter/openai/gpt-5.4"),
1327 ModelFamily::OpenAI
1328 );
1329 assert_eq!(
1330 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
1331 ModelFamily::DeepSeek
1332 );
1333 }
1334
1335 #[test]
1336 fn model_family_covers_prominent_google_and_mistral_model_names() {
1337 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
1338 assert_eq!(
1339 model_family("mistralai/mixtral-8x22b"),
1340 ModelFamily::Mistral
1341 );
1342 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
1343 }
1344
1345 #[test]
1346 fn model_family_falls_back_to_inferencer_for_unknown_models() {
1347 assert_eq!(
1348 model_family("custom-gateway/my-private-model"),
1349 ModelFamily::Inferencer
1350 );
1351 assert_eq!(model_family(""), ModelFamily::Inferencer);
1352 }
1353}