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: "z-ai/glm-5.2".to_string(),
326 provider: ProviderKind::Openrouter,
327 aliases: vec!["glm-5.2".to_string(), "zai-glm-5.2".to_string()],
328 supports_tools: true,
329 supports_reasoning: true,
330 },
331 ModelInfo {
332 id: "GLM-5.1".to_string(),
333 provider: ProviderKind::Zai,
334 aliases: vec![
335 "glm-5.1".to_string(),
336 "glm-5-1".to_string(),
337 "zai-glm-5.1".to_string(),
338 "zai-glm-5-1".to_string(),
339 ],
340 supports_tools: true,
341 supports_reasoning: true,
342 },
343 ModelInfo {
344 id: "GLM-5.2".to_string(),
345 provider: ProviderKind::Zai,
346 aliases: vec![
347 "glm-5.2".to_string(),
348 "glm-5-2".to_string(),
349 "zai-glm-5.2".to_string(),
350 "zai-glm-5-2".to_string(),
351 ],
352 supports_tools: true,
353 supports_reasoning: true,
354 },
355 ModelInfo {
356 id: "tencent/hy3-preview".to_string(),
357 provider: ProviderKind::Openrouter,
358 aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
359 supports_tools: true,
360 supports_reasoning: true,
361 },
362 ModelInfo {
363 id: "google/gemma-4-31b-it".to_string(),
364 provider: ProviderKind::Openrouter,
365 aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
366 supports_tools: true,
367 supports_reasoning: true,
368 },
369 ModelInfo {
370 id: "google/gemma-4-26b-a4b-it".to_string(),
371 provider: ProviderKind::Openrouter,
372 aliases: vec![
373 "gemma-4-26b-a4b".to_string(),
374 "gemma-4-26b-a4b-it".to_string(),
375 ],
376 supports_tools: true,
377 supports_reasoning: true,
378 },
379 ModelInfo {
380 id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
381 provider: ProviderKind::Openrouter,
382 aliases: vec![
383 "nemotron-3-nano-omni".to_string(),
384 "nemotron-3-nano-omni-reasoning".to_string(),
385 ],
386 supports_tools: true,
387 supports_reasoning: true,
388 },
389 ModelInfo {
390 id: "mimo-v2.5-pro".to_string(),
391 provider: ProviderKind::XiaomiMimo,
392 aliases: vec![
393 "mimo".to_string(),
394 "pro".to_string(),
395 "xiaomi-mimo-v2.5-pro".to_string(),
396 "xiaomi-mimo-v2-5-pro".to_string(),
397 ],
398 supports_tools: true,
399 supports_reasoning: true,
400 },
401 ModelInfo {
402 id: "mimo-v2.5".to_string(),
403 provider: ProviderKind::XiaomiMimo,
404 aliases: vec![
405 "omni".to_string(),
406 "mimo-omni".to_string(),
407 "v2.5-omni".to_string(),
408 "mimo-v2.5-omni".to_string(),
409 "xiaomi-mimo-v2.5".to_string(),
410 "xiaomi-mimo-v2.5-omni".to_string(),
411 ],
412 supports_tools: true,
413 supports_reasoning: true,
414 },
415 ModelInfo {
416 id: "mimo-v2.5-asr".to_string(),
417 provider: ProviderKind::XiaomiMimo,
418 aliases: vec![
419 "asr".to_string(),
420 "speech-to-text".to_string(),
421 "transcribe".to_string(),
422 ],
423 supports_tools: false,
424 supports_reasoning: false,
425 },
426 ModelInfo {
427 id: "mimo-v2.5-tts".to_string(),
428 provider: ProviderKind::XiaomiMimo,
429 aliases: vec![
430 "tts".to_string(),
431 "speech".to_string(),
432 "mimo-tts".to_string(),
433 ],
434 supports_tools: false,
435 supports_reasoning: false,
436 },
437 ModelInfo {
438 id: "mimo-v2.5-tts-voicedesign".to_string(),
439 provider: ProviderKind::XiaomiMimo,
440 aliases: vec![
441 "voicedesign".to_string(),
442 "voice-design".to_string(),
443 "mimo-voice-design".to_string(),
444 ],
445 supports_tools: false,
446 supports_reasoning: false,
447 },
448 ModelInfo {
449 id: "mimo-v2.5-tts-voiceclone".to_string(),
450 provider: ProviderKind::XiaomiMimo,
451 aliases: vec![
452 "voiceclone".to_string(),
453 "voice-clone".to_string(),
454 "mimo-voice-clone".to_string(),
455 ],
456 supports_tools: false,
457 supports_reasoning: false,
458 },
459 ModelInfo {
460 id: "mimo-v2-tts".to_string(),
461 provider: ProviderKind::XiaomiMimo,
462 aliases: vec!["mimo-v2-speech".to_string()],
463 supports_tools: false,
464 supports_reasoning: false,
465 },
466 ModelInfo {
467 id: "deepseek/deepseek-v4-pro".to_string(),
468 provider: ProviderKind::Novita,
469 aliases: vec![
470 "deepseek-v4-pro".to_string(),
471 "novita-deepseek-v4-pro".to_string(),
472 ],
473 supports_tools: true,
474 supports_reasoning: true,
475 },
476 ModelInfo {
477 id: "deepseek/deepseek-v4-flash".to_string(),
478 provider: ProviderKind::Novita,
479 aliases: vec![
480 "deepseek-v4-flash".to_string(),
481 "deepseek-chat".to_string(),
482 "deepseek-reasoner".to_string(),
483 "novita-deepseek-v4-flash".to_string(),
484 ],
485 supports_tools: true,
486 supports_reasoning: true,
487 },
488 ModelInfo {
489 id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
490 provider: ProviderKind::Fireworks,
491 aliases: vec![
492 "deepseek-v4-pro".to_string(),
493 "fireworks-deepseek-v4-pro".to_string(),
494 ],
495 supports_tools: true,
496 supports_reasoning: true,
497 },
498 ModelInfo {
499 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
500 provider: ProviderKind::Siliconflow,
501 aliases: vec![
502 "deepseek-v4-pro".to_string(),
503 "deepseek-reasoner".to_string(),
504 "deepseek-r1".to_string(),
505 "siliconflow-deepseek-v4-pro".to_string(),
506 ],
507 supports_tools: true,
508 supports_reasoning: true,
509 },
510 ModelInfo {
511 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
512 provider: ProviderKind::Siliconflow,
513 aliases: vec![
514 "deepseek-v4-flash".to_string(),
515 "deepseek-chat".to_string(),
516 "deepseek-v3".to_string(),
517 "siliconflow-deepseek-v4-flash".to_string(),
518 ],
519 supports_tools: true,
520 supports_reasoning: true,
521 },
522 ModelInfo {
523 id: "trinity-large-preview".to_string(),
524 provider: ProviderKind::Arcee,
525 aliases: vec!["arcee-trinity-large-preview".to_string()],
526 supports_tools: true,
527 supports_reasoning: false,
528 },
529 ModelInfo {
530 id: "kimi-k2.7-code".to_string(),
531 provider: ProviderKind::Moonshot,
532 aliases: vec![
533 "kimi".to_string(),
534 "kimi-k2".to_string(),
535 "kimi-k2.7".to_string(),
536 "kimi-code".to_string(),
537 "moonshot-kimi-k2.7-code".to_string(),
538 ],
539 supports_tools: true,
540 supports_reasoning: true,
541 },
542 ModelInfo {
543 id: "kimi-k2.6".to_string(),
544 provider: ProviderKind::Moonshot,
545 aliases: vec!["kimi-k2.6".to_string(), "moonshot-kimi-k2.6".to_string()],
546 supports_tools: true,
547 supports_reasoning: true,
548 },
549 ModelInfo {
550 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
551 provider: ProviderKind::Sglang,
552 aliases: vec![
553 "deepseek-v4-pro".to_string(),
554 "sglang-deepseek-v4-pro".to_string(),
555 ],
556 supports_tools: true,
557 supports_reasoning: true,
558 },
559 ModelInfo {
560 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
561 provider: ProviderKind::Sglang,
562 aliases: vec![
563 "deepseek-v4-flash".to_string(),
564 "deepseek-chat".to_string(),
565 "deepseek-reasoner".to_string(),
566 "sglang-deepseek-v4-flash".to_string(),
567 ],
568 supports_tools: true,
569 supports_reasoning: true,
570 },
571 ModelInfo {
572 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
573 provider: ProviderKind::Vllm,
574 aliases: vec![
575 "deepseek-v4-pro".to_string(),
576 "vllm-deepseek-v4-pro".to_string(),
577 ],
578 supports_tools: true,
579 supports_reasoning: true,
580 },
581 ModelInfo {
582 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
583 provider: ProviderKind::Vllm,
584 aliases: vec![
585 "deepseek-v4-flash".to_string(),
586 "deepseek-chat".to_string(),
587 "deepseek-reasoner".to_string(),
588 "vllm-deepseek-v4-flash".to_string(),
589 ],
590 supports_tools: true,
591 supports_reasoning: true,
592 },
593 ModelInfo {
594 id: "deepseek-coder:1.3b".to_string(),
595 provider: ProviderKind::Ollama,
596 aliases: vec![],
597 supports_tools: true,
598 supports_reasoning: false,
599 },
600 ModelInfo {
601 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
602 provider: ProviderKind::Huggingface,
603 aliases: vec![
604 "deepseek-v4-pro".to_string(),
605 "hf-deepseek-v4-pro".to_string(),
606 ],
607 supports_tools: true,
608 supports_reasoning: true,
609 },
610 ModelInfo {
611 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
612 provider: ProviderKind::Huggingface,
613 aliases: vec![
614 "deepseek-v4-flash".to_string(),
615 "deepseek-chat".to_string(),
616 "deepseek-reasoner".to_string(),
617 "hf-deepseek-v4-flash".to_string(),
618 ],
619 supports_tools: true,
620 supports_reasoning: true,
621 },
622 ModelInfo {
624 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
625 provider: ProviderKind::Together,
626 aliases: vec![
627 "deepseek-v4-pro".to_string(),
628 "together-deepseek-v4-pro".to_string(),
629 ],
630 supports_tools: true,
631 supports_reasoning: true,
632 },
633 ModelInfo {
634 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
635 provider: ProviderKind::Together,
636 aliases: vec![
637 "deepseek-v4-flash".to_string(),
638 "deepseek-chat".to_string(),
639 "together-deepseek-v4-flash".to_string(),
640 ],
641 supports_tools: true,
642 supports_reasoning: true,
643 },
644 ModelInfo {
646 id: "qwen/qwen3.7-max".to_string(),
647 provider: ProviderKind::Openrouter,
648 aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
649 supports_tools: true,
650 supports_reasoning: true,
651 },
652 ModelInfo {
654 id: "gpt-5.5".to_string(),
655 provider: ProviderKind::OpenaiCodex,
656 aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
657 supports_tools: true,
658 supports_reasoning: true,
659 },
660 ModelInfo {
662 id: "claude-opus-4-8".to_string(),
663 provider: ProviderKind::Anthropic,
664 aliases: vec!["opus".to_string(), "claude-opus".to_string()],
665 supports_tools: true,
666 supports_reasoning: true,
667 },
668 ModelInfo {
669 id: "claude-sonnet-4-6".to_string(),
670 provider: ProviderKind::Anthropic,
671 aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
672 supports_tools: true,
673 supports_reasoning: true,
674 },
675 ModelInfo {
676 id: "claude-haiku-4-5".to_string(),
677 provider: ProviderKind::Anthropic,
678 aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
679 supports_tools: true,
680 supports_reasoning: false,
681 },
682 ModelInfo {
684 id: "minimax/minimax-2.7".to_string(),
685 provider: ProviderKind::Openrouter,
686 aliases: vec![
687 "minimax-2.7".to_string(),
688 "minimax-2-7".to_string(),
689 "openrouter-minimax-2.7".to_string(),
690 ],
691 supports_tools: true,
692 supports_reasoning: true,
693 },
694 ModelInfo {
695 id: "step-3.7-flash".to_string(),
696 provider: ProviderKind::Stepfun,
697 aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
698 supports_tools: true,
699 supports_reasoning: false,
700 },
701 ModelInfo {
702 id: "MiniMax-M3".to_string(),
703 provider: ProviderKind::Minimax,
704 aliases: vec![
705 "minimax".to_string(),
706 "minimax-m3".to_string(),
707 "minimax-m-3".to_string(),
708 ],
709 supports_tools: true,
710 supports_reasoning: true,
711 },
712 ModelInfo {
713 id: "MiniMax-M2.7".to_string(),
714 provider: ProviderKind::Minimax,
715 aliases: vec![
716 "minimax-m2.7".to_string(),
717 "minimax-m2-7".to_string(),
718 "minimax-m-2.7".to_string(),
719 "minimax-m-2-7".to_string(),
720 ],
721 supports_tools: true,
722 supports_reasoning: true,
723 },
724 ModelInfo {
725 id: "MiniMax-M2.7-highspeed".to_string(),
726 provider: ProviderKind::Minimax,
727 aliases: vec![
728 "minimax-m2.7-highspeed".to_string(),
729 "minimax-m2-7-highspeed".to_string(),
730 "minimax-m-2.7-highspeed".to_string(),
731 "minimax-m-2-7-highspeed".to_string(),
732 ],
733 supports_tools: true,
734 supports_reasoning: true,
735 },
736 ModelInfo {
737 id: "MiniMax-M2.5".to_string(),
738 provider: ProviderKind::Minimax,
739 aliases: vec![
740 "minimax-m2.5".to_string(),
741 "minimax-m2-5".to_string(),
742 "minimax-m-2.5".to_string(),
743 "minimax-m-2-5".to_string(),
744 ],
745 supports_tools: true,
746 supports_reasoning: true,
747 },
748 ModelInfo {
749 id: "MiniMax-M2.5-highspeed".to_string(),
750 provider: ProviderKind::Minimax,
751 aliases: vec![
752 "minimax-m2.5-highspeed".to_string(),
753 "minimax-m2-5-highspeed".to_string(),
754 "minimax-m-2.5-highspeed".to_string(),
755 "minimax-m-2-5-highspeed".to_string(),
756 ],
757 supports_tools: true,
758 supports_reasoning: true,
759 },
760 ModelInfo {
761 id: "MiniMax-M2.1".to_string(),
762 provider: ProviderKind::Minimax,
763 aliases: vec![
764 "minimax-m2.1".to_string(),
765 "minimax-m2-1".to_string(),
766 "minimax-m-2.1".to_string(),
767 "minimax-m-2-1".to_string(),
768 ],
769 supports_tools: true,
770 supports_reasoning: true,
771 },
772 ModelInfo {
773 id: "MiniMax-M2.1-highspeed".to_string(),
774 provider: ProviderKind::Minimax,
775 aliases: vec![
776 "minimax-m2.1-highspeed".to_string(),
777 "minimax-m2-1-highspeed".to_string(),
778 "minimax-m-2.1-highspeed".to_string(),
779 "minimax-m-2-1-highspeed".to_string(),
780 ],
781 supports_tools: true,
782 supports_reasoning: true,
783 },
784 ModelInfo {
785 id: "MiniMax-M2".to_string(),
786 provider: ProviderKind::Minimax,
787 aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
788 supports_tools: true,
789 supports_reasoning: true,
790 },
791 ModelInfo {
793 id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
794 provider: ProviderKind::Openrouter,
795 aliases: vec![
796 "nvidia/nemotron-3-ultra".to_string(),
797 "nemotron-3-ultra".to_string(),
798 "nemotron-3-ultra-550b-a55b".to_string(),
799 "nvidia-nemotron-3-ultra".to_string(),
800 "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
801 ],
802 supports_tools: true,
803 supports_reasoning: true,
804 },
805 ModelInfo {
807 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
808 provider: ProviderKind::Deepinfra,
809 aliases: vec![
810 "deepseek-v4-pro".to_string(),
811 "di-deepseek-v4-pro".to_string(),
812 ],
813 supports_tools: true,
814 supports_reasoning: true,
815 },
816 ModelInfo {
817 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
818 provider: ProviderKind::Deepinfra,
819 aliases: vec![
820 "deepseek-v4-flash".to_string(),
821 "di-deepseek-v4-flash".to_string(),
822 ],
823 supports_tools: true,
824 supports_reasoning: true,
825 },
826 ];
827 Self::new(models)
828 }
829}
830
831impl ModelRegistry {
832 #[must_use]
838 pub fn new(models: Vec<ModelInfo>) -> Self {
839 let mut alias_map = HashMap::new();
840 for (idx, model) in models.iter().enumerate() {
841 alias_map.entry(normalize(&model.id)).or_insert(idx);
842 for alias in &model.aliases {
843 alias_map.entry(normalize(alias)).or_insert(idx);
844 }
845 }
846 Self { models, alias_map }
847 }
848
849 #[must_use]
851 pub fn list(&self) -> Vec<ModelInfo> {
852 self.models.clone()
853 }
854
855 #[must_use]
867 pub fn resolve(
868 &self,
869 requested: Option<&str>,
870 provider_hint: Option<ProviderKind>,
871 ) -> ModelResolution {
872 let mut fallback_chain = Vec::new();
873
874 if let Some(name) = requested {
875 fallback_chain.push(format!("requested:{name}"));
876 if provider_hint == Some(ProviderKind::Ollama) {
877 return ModelResolution {
878 requested: Some(name.to_string()),
879 resolved: ModelInfo {
880 id: name.trim().to_string(),
881 provider: ProviderKind::Ollama,
882 aliases: Vec::new(),
883 supports_tools: true,
884 supports_reasoning: false,
885 },
886 used_fallback: false,
887 fallback_chain,
888 };
889 }
890 if let Some(provider) = provider_hint
891 && let Some(model) = self
892 .models
893 .iter()
894 .find(|m| m.provider == provider && model_matches(m, name))
895 .cloned()
896 {
897 return ModelResolution {
898 requested: Some(name.to_string()),
899 resolved: model,
900 used_fallback: false,
901 fallback_chain,
902 };
903 }
904 if provider_hint == Some(ProviderKind::Atlascloud)
905 && let Some(model) = atlascloud_passthrough_model(name)
906 {
907 return ModelResolution {
908 requested: Some(name.to_string()),
909 resolved: model,
910 used_fallback: false,
911 fallback_chain,
912 };
913 }
914 if provider_hint == Some(ProviderKind::Arcee)
915 && let Some(model) = arcee_passthrough_model(name)
916 {
917 return ModelResolution {
918 requested: Some(name.to_string()),
919 resolved: model,
920 used_fallback: false,
921 fallback_chain,
922 };
923 }
924 if provider_hint == Some(ProviderKind::XiaomiMimo)
925 && let Some(model) = xiaomi_mimo_passthrough_model(name)
926 {
927 return ModelResolution {
928 requested: Some(name.to_string()),
929 resolved: model,
930 used_fallback: false,
931 fallback_chain,
932 };
933 }
934 if let Some(idx) = self.alias_map.get(&normalize(name)) {
935 return ModelResolution {
936 requested: Some(name.to_string()),
937 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
938 used_fallback: false,
939 fallback_chain,
940 };
941 }
942 }
943
944 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
945 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
946 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
947 return ModelResolution {
948 requested: requested.map(ToOwned::to_owned),
949 resolved: model,
950 used_fallback: true,
951 fallback_chain,
952 };
953 }
954
955 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
956 id: "deepseek-v4-pro".to_string(),
957 provider: ProviderKind::Deepseek,
958 aliases: Vec::new(),
959 supports_tools: true,
960 supports_reasoning: true,
961 });
962 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
963 ModelResolution {
964 requested: requested.map(ToOwned::to_owned),
965 resolved: final_fallback,
966 used_fallback: true,
967 fallback_chain,
968 }
969 }
970}
971
972fn normalize(value: &str) -> String {
973 value.trim().to_ascii_lowercase()
974}
975
976#[must_use]
977pub fn model_family(model_id: &str) -> ModelFamily {
979 let normalized = normalize(model_id);
980 if normalized.is_empty() {
981 return ModelFamily::Inferencer;
982 }
983
984 if normalized.contains("deepseek") {
985 return ModelFamily::DeepSeek;
986 }
987 if normalized.contains("claude") || normalized.contains("anthropic") {
988 return ModelFamily::Anthropic;
989 }
990 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
991 return ModelFamily::GptOss;
992 }
993 if normalized.starts_with("gpt-")
994 || normalized.contains("/gpt-")
995 || normalized.contains("openai/")
996 {
997 return ModelFamily::OpenAI;
998 }
999 if normalized.contains("gemini")
1000 || normalized.contains("gemma")
1001 || normalized.contains("google/")
1002 {
1003 return ModelFamily::Google;
1004 }
1005 if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/")
1006 {
1007 return ModelFamily::Meta;
1008 }
1009 if normalized.contains("mistral")
1010 || normalized.contains("mixtral")
1011 || normalized.contains("codestral")
1012 {
1013 return ModelFamily::Mistral;
1014 }
1015 if normalized.contains("qwen") {
1016 return ModelFamily::Qwen;
1017 }
1018 if normalized.contains("grok") {
1019 return ModelFamily::Grok;
1020 }
1021 if normalized.contains("cohere") || normalized.contains("command-r") {
1022 return ModelFamily::Cohere;
1023 }
1024
1025 ModelFamily::Inferencer
1026}
1027
1028fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1029 let requested = normalize(requested);
1030 normalize(&model.id) == requested
1031 || model
1032 .aliases
1033 .iter()
1034 .any(|alias| normalize(alias) == requested)
1035}
1036
1037fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1038 let requested = requested.trim();
1039 if model.id.eq_ignore_ascii_case(requested) {
1040 model.id = requested.to_string();
1041 }
1042 model
1043}
1044
1045fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1046 let requested = requested.trim();
1047 if requested.is_empty() || !requested.contains('/') {
1048 return None;
1049 }
1050
1051 Some(ModelInfo {
1052 id: requested.to_string(),
1053 provider: ProviderKind::Atlascloud,
1054 aliases: Vec::new(),
1055 supports_tools: true,
1056 supports_reasoning: true,
1057 })
1058}
1059
1060fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1061 let requested = requested.trim();
1062 if requested.is_empty() {
1063 return None;
1064 }
1065 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1066
1067 Some(ModelInfo {
1068 id: requested.to_string(),
1069 provider: ProviderKind::Arcee,
1070 aliases: Vec::new(),
1071 supports_tools: true,
1072 supports_reasoning,
1073 })
1074}
1075
1076fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1077 let requested = requested.trim();
1078 if requested.is_empty() || requested.chars().any(char::is_control) {
1079 return None;
1080 }
1081
1082 Some(ModelInfo {
1083 id: requested.to_string(),
1084 provider: ProviderKind::XiaomiMimo,
1085 aliases: Vec::new(),
1086 supports_tools: true,
1087 supports_reasoning: true,
1088 })
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094
1095 #[test]
1096 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1097 let registry = ModelRegistry::default();
1098 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1099
1100 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1101 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1102 }
1103
1104 #[test]
1105 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1106 let registry = ModelRegistry::default();
1107 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1108
1109 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1110 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1111 }
1112
1113 #[test]
1114 fn nvidia_nim_default_uses_catalog_model_id() {
1115 let registry = ModelRegistry::default();
1116 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1117
1118 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1119 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1120 }
1121
1122 #[test]
1123 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1124 let registry = ModelRegistry::default();
1125 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1126
1127 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1128 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1129 }
1130
1131 #[test]
1132 fn atlascloud_default_uses_namespaced_model_id() {
1133 let registry = ModelRegistry::default();
1134 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1135
1136 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1137 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1138 assert!(resolved.resolved.supports_reasoning);
1139 }
1140
1141 #[test]
1142 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1143 let registry = ModelRegistry::default();
1144 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1145
1146 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1147 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1148 }
1149
1150 #[test]
1151 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1152 let registry = ModelRegistry::default();
1153 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1154
1155 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1156 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1157 }
1158
1159 #[test]
1160 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1161 let registry = ModelRegistry::default();
1162 let resolved =
1163 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1164
1165 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1166 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1167 assert!(resolved.resolved.supports_tools);
1168 assert!(resolved.resolved.supports_reasoning);
1169 assert!(!resolved.used_fallback);
1170 }
1171
1172 #[test]
1173 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1174 let registry = ModelRegistry::default();
1175 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1176
1177 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1178 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1179 assert!(!resolved.used_fallback);
1180 }
1181
1182 #[test]
1183 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1184 let registry = ModelRegistry::default();
1185 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1186
1187 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1188 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1189 assert!(resolved.used_fallback);
1190 }
1191
1192 #[test]
1193 fn openrouter_default_uses_namespaced_model_id() {
1194 let registry = ModelRegistry::default();
1195 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1196
1197 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1198 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1199 }
1200
1201 #[test]
1202 fn xiaomi_mimo_default_uses_canonical_model_id() {
1203 let registry = ModelRegistry::default();
1204 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1205
1206 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1207 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1208 assert!(resolved.resolved.supports_reasoning);
1209 }
1210
1211 #[test]
1212 fn moonshot_default_and_aliases_use_kimi_k27_code() {
1213 let registry = ModelRegistry::default();
1214
1215 for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1216 let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1217
1218 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1219 assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1220 assert!(resolved.resolved.supports_tools);
1221 assert!(resolved.resolved.supports_reasoning);
1222 }
1223 }
1224
1225 #[test]
1226 fn moonshot_explicit_kimi_k26_remains_available() {
1227 let registry = ModelRegistry::default();
1228 let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1229
1230 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1231 assert_eq!(resolved.resolved.id, "kimi-k2.6");
1232 assert!(resolved.resolved.supports_reasoning);
1233 }
1234
1235 #[test]
1236 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1237 let registry = ModelRegistry::default();
1238 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1239 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1240 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1241 assert!(!resolved.resolved.supports_tools);
1242 assert!(!resolved.resolved.supports_reasoning);
1243
1244 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1245 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1246
1247 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1248 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1249 }
1250
1251 #[test]
1252 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1253 let registry = ModelRegistry::default();
1254
1255 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1256 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1257 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1258 assert!(resolved.resolved.supports_tools);
1259 }
1260
1261 #[test]
1262 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1263 let registry = ModelRegistry::default();
1264 let resolved =
1265 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1266
1267 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1268 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1269 assert!(!resolved.used_fallback);
1270 }
1271
1272 #[test]
1273 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1274 let registry = ModelRegistry::default();
1275 let resolved = registry.resolve(
1276 Some("deepseek/deepseek-v4-pro"),
1277 Some(ProviderKind::XiaomiMimo),
1278 );
1279
1280 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1281 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1282 assert!(!resolved.used_fallback);
1283 }
1284
1285 #[test]
1286 fn wanjie_ark_default_uses_reasoner_model_id() {
1287 let registry = ModelRegistry::default();
1288 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1289
1290 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1291 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1292 assert!(resolved.resolved.supports_reasoning);
1293 }
1294
1295 #[test]
1296 fn novita_default_uses_namespaced_model_id() {
1297 let registry = ModelRegistry::default();
1298 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1299
1300 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1301 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1302 }
1303
1304 #[test]
1305 fn fireworks_default_uses_canonical_model_id() {
1306 let registry = ModelRegistry::default();
1307 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1308
1309 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1310 assert_eq!(
1311 resolved.resolved.id,
1312 "accounts/fireworks/models/deepseek-v4-pro"
1313 );
1314 }
1315
1316 #[test]
1317 fn siliconflow_default_uses_canonical_pro_model_id() {
1318 let registry = ModelRegistry::default();
1319 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1320
1321 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1322 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1323 assert!(resolved.resolved.supports_reasoning);
1324 }
1325
1326 #[test]
1327 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1328 let registry = ModelRegistry::default();
1329 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1330
1331 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1332 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1333 assert!(resolved.resolved.supports_reasoning);
1334 }
1335
1336 #[test]
1337 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1338 let registry = ModelRegistry::default();
1339 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1340
1341 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1342 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1343 assert!(resolved.resolved.supports_reasoning);
1344 }
1345
1346 #[test]
1347 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1348 let registry = ModelRegistry::default();
1349 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1350
1351 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1352 assert_eq!(resolved.resolved.id, "trinity-mini");
1353 assert!(!resolved.resolved.supports_reasoning);
1354 }
1355
1356 #[test]
1357 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1358 let registry = ModelRegistry::default();
1359 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1360
1361 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1362 assert_eq!(resolved.resolved.id, "trinity-large-next");
1363 assert!(!resolved.resolved.supports_reasoning);
1364 assert!(!resolved.used_fallback);
1365 }
1366
1367 #[test]
1368 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1369 let registry = ModelRegistry::default();
1370 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1371
1372 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1373 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1374 }
1375
1376 #[test]
1377 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1378 let registry = ModelRegistry::default();
1379 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1380
1381 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1382 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1383 }
1384
1385 #[test]
1386 fn sglang_default_uses_canonical_model_id() {
1387 let registry = ModelRegistry::default();
1388 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1389
1390 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1391 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1392 }
1393
1394 #[test]
1395 fn zai_direct_models_resolve_when_provider_hinted() {
1396 let registry = ModelRegistry::default();
1397
1398 let default = registry.resolve(None, Some(ProviderKind::Zai));
1399 assert_eq!(default.resolved.provider, ProviderKind::Zai);
1400 assert_eq!(default.resolved.id, "GLM-5.1");
1401
1402 for (alias, expected) in [
1403 ("GLM-5.1", "GLM-5.1"),
1404 ("glm-5-1", "GLM-5.1"),
1405 ("GLM-5.2", "GLM-5.2"),
1406 ("glm-5.2", "GLM-5.2"),
1407 ("zai-glm-5-2", "GLM-5.2"),
1408 ] {
1409 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
1410
1411 assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
1412 assert_eq!(resolved.resolved.id, expected);
1413 assert!(!resolved.used_fallback);
1414 assert!(resolved.resolved.supports_tools);
1415 assert!(resolved.resolved.supports_reasoning);
1416 }
1417 }
1418
1419 #[test]
1420 fn first_party_recent_provider_models_are_listed() {
1421 let registry = ModelRegistry::default();
1422 let models = registry.list();
1423
1424 for (provider, id) in [
1425 (ProviderKind::Zai, "GLM-5.2"),
1426 (ProviderKind::Stepfun, "step-3.7-flash"),
1427 (ProviderKind::Minimax, "MiniMax-M2.1"),
1428 ] {
1429 assert!(
1430 models
1431 .iter()
1432 .any(|model| model.provider == provider && model.id == id),
1433 "expected {provider:?} model {id} in registry"
1434 );
1435 }
1436 }
1437
1438 #[test]
1439 fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
1440 let registry = ModelRegistry::default();
1441
1442 let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
1443 assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
1444 assert_eq!(stepfun.resolved.id, "step-3.7-flash");
1445
1446 for (alias, expected) in [
1447 ("minimax", "MiniMax-M3"),
1448 ("minimax-m3", "MiniMax-M3"),
1449 ("minimax-m2.7", "MiniMax-M2.7"),
1450 ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
1451 ("minimax-m2.1", "MiniMax-M2.1"),
1452 ("minimax-m2", "MiniMax-M2"),
1453 ] {
1454 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
1455
1456 assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
1457 assert_eq!(resolved.resolved.id, expected);
1458 assert!(!resolved.used_fallback);
1459 assert!(resolved.resolved.supports_tools);
1460 assert!(resolved.resolved.supports_reasoning);
1461 }
1462 }
1463
1464 #[test]
1465 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1466 let registry = ModelRegistry::default();
1467 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1468
1469 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1470 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1471 }
1472
1473 #[test]
1474 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1475 let registry = ModelRegistry::default();
1476
1477 for (alias, expected) in [
1478 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1479 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1480 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1481 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1482 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1483 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1484 ("glm-5.1", "z-ai/glm-5.1"),
1485 ("glm-5.2", "z-ai/glm-5.2"),
1486 ("minimax-m3", "minimax/minimax-m3"),
1487 ("minimax-2.7", "minimax/minimax-2.7"),
1488 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1489 ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
1490 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1491 ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
1492 (
1493 "nvidia/nemotron-3-ultra",
1494 "nvidia/nemotron-3-ultra-550b-a55b",
1495 ),
1496 ] {
1497 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1498
1499 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1500 assert_eq!(resolved.resolved.id, expected);
1501 assert!(resolved.resolved.supports_tools);
1502 assert!(resolved.resolved.supports_reasoning);
1503 }
1504 }
1505
1506 #[test]
1507 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
1508 let registry = ModelRegistry::default();
1509 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
1510
1511 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1512 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1513 }
1514
1515 #[test]
1516 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
1517 let registry = ModelRegistry::default();
1518 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
1519
1520 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1521 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1522 }
1523
1524 #[test]
1525 fn vllm_default_uses_canonical_model_id() {
1526 let registry = ModelRegistry::default();
1527 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
1528
1529 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1530 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1531 }
1532
1533 #[test]
1534 fn ollama_default_uses_small_local_model_id() {
1535 let registry = ModelRegistry::default();
1536 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
1537
1538 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1539 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
1540 assert!(!resolved.resolved.supports_reasoning);
1541 }
1542
1543 #[test]
1544 fn ollama_requested_model_tag_is_preserved() {
1545 let registry = ModelRegistry::default();
1546 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
1547
1548 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1549 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
1550 assert!(!resolved.used_fallback);
1551 }
1552
1553 #[test]
1554 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
1555 let registry = ModelRegistry::default();
1556 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
1557
1558 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1559 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1560 }
1561
1562 #[test]
1563 fn preserves_requested_model_casing_for_third_party_providers() {
1564 let registry = ModelRegistry::default();
1565 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
1566
1567 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1568 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1569 }
1570
1571 #[test]
1572 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
1573 let registry = ModelRegistry::default();
1574 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
1575
1576 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1577 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1579 }
1580
1581 #[test]
1582 fn preserves_requested_model_casing_without_surrounding_whitespace() {
1583 let registry = ModelRegistry::default();
1584 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
1585
1586 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1587 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1588 }
1589
1590 #[test]
1591 fn alias_match_does_not_override_requested_casing() {
1592 let registry = ModelRegistry::default();
1593 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
1594
1595 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1596 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1597 }
1598
1599 #[test]
1600 fn model_family_classifies_known_model_ids() {
1601 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
1602 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
1603 assert_eq!(
1604 model_family("anthropic/claude-opus-4-7"),
1605 ModelFamily::Anthropic
1606 );
1607 assert_eq!(
1608 model_family("meta-llama/llama-3.3-70b-instruct"),
1609 ModelFamily::Meta
1610 );
1611 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
1612 }
1613
1614 #[test]
1615 fn model_family_uses_underlying_model_for_router_ids() {
1616 assert_eq!(
1617 model_family("groq/llama-3.3-70b-versatile"),
1618 ModelFamily::Meta
1619 );
1620 assert_eq!(
1621 model_family("openrouter/openai/gpt-5.4"),
1622 ModelFamily::OpenAI
1623 );
1624 assert_eq!(
1625 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
1626 ModelFamily::DeepSeek
1627 );
1628 }
1629
1630 #[test]
1631 fn model_family_covers_prominent_google_and_mistral_model_names() {
1632 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
1633 assert_eq!(
1634 model_family("mistralai/mixtral-8x22b"),
1635 ModelFamily::Mistral
1636 );
1637 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
1638 }
1639
1640 #[test]
1641 fn model_family_falls_back_to_inferencer_for_unknown_models() {
1642 assert_eq!(
1643 model_family("custom-gateway/my-private-model"),
1644 ModelFamily::Inferencer
1645 );
1646 assert_eq!(model_family(""), ModelFamily::Inferencer);
1647 }
1648}