1use std::collections::HashMap;
2
3use codewhale_config::{ProviderKind, opencode_go_chat_model_id};
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 {
132 id: "gpt-5.3-codex".to_string(),
133 provider: ProviderKind::Openai,
134 aliases: vec!["gpt53-codex".to_string()],
135 supports_tools: true,
136 supports_reasoning: true,
137 },
138 ModelInfo {
139 id: "gpt-5.5".to_string(),
140 provider: ProviderKind::Openai,
141 aliases: vec!["openai-gpt-5.5".to_string()],
142 supports_tools: true,
143 supports_reasoning: true,
144 },
145 ModelInfo {
146 id: "gpt-5.5-pro".to_string(),
147 provider: ProviderKind::Openai,
148 aliases: vec!["openai-gpt-5.5-pro".to_string()],
149 supports_tools: true,
150 supports_reasoning: true,
151 },
152 ModelInfo {
154 id: "gpt-5.6".to_string(),
155 provider: ProviderKind::Openai,
156 aliases: vec!["gpt56".to_string()],
157 supports_tools: true,
158 supports_reasoning: true,
159 },
160 ModelInfo {
161 id: "gpt-5.6-sol".to_string(),
162 provider: ProviderKind::Openai,
163 aliases: vec!["gpt56-sol".to_string()],
164 supports_tools: true,
165 supports_reasoning: true,
166 },
167 ModelInfo {
168 id: "gpt-5.6-terra".to_string(),
169 provider: ProviderKind::Openai,
170 aliases: vec!["gpt56-terra".to_string()],
171 supports_tools: true,
172 supports_reasoning: true,
173 },
174 ModelInfo {
175 id: "gpt-5.6-luna".to_string(),
176 provider: ProviderKind::Openai,
177 aliases: vec!["gpt56-luna".to_string()],
178 supports_tools: true,
179 supports_reasoning: true,
180 },
181 ModelInfo {
182 id: "deepseek-ai/deepseek-v4-flash".to_string(),
183 provider: ProviderKind::Atlascloud,
184 aliases: vec![
185 "deepseek-v4-flash".to_string(),
186 "atlascloud-deepseek-v4-flash".to_string(),
187 ],
188 supports_tools: true,
189 supports_reasoning: true,
190 },
191 ModelInfo {
192 id: "deepseek-ai/deepseek-v4-pro".to_string(),
193 provider: ProviderKind::Atlascloud,
194 aliases: vec![
195 "deepseek-v4-pro".to_string(),
196 "atlascloud-deepseek-v4-pro".to_string(),
197 ],
198 supports_tools: true,
199 supports_reasoning: true,
200 },
201 ModelInfo {
202 id: "deepseek-reasoner".to_string(),
203 provider: ProviderKind::WanjieArk,
204 aliases: vec![
205 "wanjie-deepseek-reasoner".to_string(),
206 "ark-wanjie-deepseek-reasoner".to_string(),
207 ],
208 supports_tools: true,
209 supports_reasoning: true,
210 },
211 ModelInfo {
212 id: "DeepSeek-V4-Pro".to_string(),
213 provider: ProviderKind::Volcengine,
214 aliases: vec![
215 "deepseek-v4-pro".to_string(),
216 "volcengine-deepseek-v4-pro".to_string(),
217 "ark-deepseek-v4-pro".to_string(),
218 ],
219 supports_tools: true,
220 supports_reasoning: true,
221 },
222 ModelInfo {
223 id: "DeepSeek-V4-Flash".to_string(),
224 provider: ProviderKind::Volcengine,
225 aliases: vec![
226 "deepseek-v4-flash".to_string(),
227 "deepseek-chat".to_string(),
228 "volcengine-deepseek-v4-flash".to_string(),
229 "ark-deepseek-v4-flash".to_string(),
230 ],
231 supports_tools: true,
232 supports_reasoning: true,
233 },
234 ModelInfo {
235 id: "trinity-large-thinking".to_string(),
236 provider: ProviderKind::Arcee,
237 aliases: vec![
238 "trinity".to_string(),
239 "arcee-trinity".to_string(),
240 "arcee-trinity-large-thinking".to_string(),
241 ],
242 supports_tools: true,
243 supports_reasoning: true,
244 },
245 ModelInfo {
246 id: "trinity-mini".to_string(),
247 provider: ProviderKind::Arcee,
248 aliases: vec!["arcee-trinity-mini".to_string()],
249 supports_tools: true,
250 supports_reasoning: true,
251 },
252 ModelInfo {
253 id: "deepseek/deepseek-v4-pro".to_string(),
254 provider: ProviderKind::Openrouter,
255 aliases: vec![
256 "deepseek-v4-pro".to_string(),
257 "openrouter-deepseek-v4-pro".to_string(),
258 ],
259 supports_tools: true,
260 supports_reasoning: true,
261 },
262 ModelInfo {
263 id: "deepseek/deepseek-v4-flash".to_string(),
264 provider: ProviderKind::Openrouter,
265 aliases: vec![
266 "deepseek-v4-flash".to_string(),
267 "deepseek-chat".to_string(),
268 "deepseek-reasoner".to_string(),
269 "openrouter-deepseek-v4-flash".to_string(),
270 ],
271 supports_tools: true,
272 supports_reasoning: true,
273 },
274 ModelInfo {
275 id: "arcee-ai/trinity-large-thinking".to_string(),
276 provider: ProviderKind::Openrouter,
277 aliases: vec![
278 "trinity".to_string(),
279 "trinity-large-thinking".to_string(),
280 "arcee-trinity-large-thinking".to_string(),
281 ],
282 supports_tools: true,
283 supports_reasoning: true,
284 },
285 ModelInfo {
286 id: "xiaomi/mimo-v2.5-pro".to_string(),
287 provider: ProviderKind::Openrouter,
288 aliases: vec![
289 "openrouter-mimo-v2.5-pro".to_string(),
290 "openrouter-xiaomi-mimo-v2.5-pro".to_string(),
291 ],
292 supports_tools: true,
293 supports_reasoning: true,
294 },
295 ModelInfo {
296 id: "xiaomi/mimo-v2.5".to_string(),
297 provider: ProviderKind::Openrouter,
298 aliases: vec![
299 "openrouter-mimo-v2.5".to_string(),
300 "openrouter-xiaomi-mimo-v2.5".to_string(),
301 ],
302 supports_tools: true,
303 supports_reasoning: true,
304 },
305 ModelInfo {
306 id: "qwen/qwen3.6-flash".to_string(),
307 provider: ProviderKind::Openrouter,
308 aliases: vec!["qwen3.6-flash".to_string(), "qwen-3.6-flash".to_string()],
309 supports_tools: true,
310 supports_reasoning: true,
311 },
312 ModelInfo {
313 id: "qwen/qwen3.6-35b-a3b".to_string(),
314 provider: ProviderKind::Openrouter,
315 aliases: vec![
316 "qwen3.6-35b-a3b".to_string(),
317 "qwen-3.6-35b-a3b".to_string(),
318 ],
319 supports_tools: true,
320 supports_reasoning: true,
321 },
322 ModelInfo {
323 id: "qwen/qwen3.6-max-preview".to_string(),
324 provider: ProviderKind::Openrouter,
325 aliases: vec![
326 "qwen3.6-max-preview".to_string(),
327 "qwen-3.6-max-preview".to_string(),
328 "qwen-max-preview".to_string(),
329 ],
330 supports_tools: true,
331 supports_reasoning: true,
332 },
333 ModelInfo {
334 id: "qwen/qwen3.6-27b".to_string(),
335 provider: ProviderKind::Openrouter,
336 aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()],
337 supports_tools: true,
338 supports_reasoning: true,
339 },
340 ModelInfo {
341 id: "qwen/qwen3.6-plus".to_string(),
342 provider: ProviderKind::Openrouter,
343 aliases: vec!["qwen3.6-plus".to_string(), "qwen-3.6-plus".to_string()],
344 supports_tools: true,
345 supports_reasoning: true,
346 },
347 ModelInfo {
348 id: "qwen/qwen3.7-plus".to_string(),
349 provider: ProviderKind::Openrouter,
350 aliases: vec!["qwen3.7-plus".to_string(), "qwen-3.7-plus".to_string()],
351 supports_tools: true,
352 supports_reasoning: true,
353 },
354 ModelInfo {
355 id: "moonshotai/kimi-k2.7-code".to_string(),
356 provider: ProviderKind::Openrouter,
357 aliases: vec![
358 "kimi-k2.7-code".to_string(),
359 "openrouter-kimi-k2.7-code".to_string(),
360 ],
361 supports_tools: true,
362 supports_reasoning: true,
363 },
364 ModelInfo {
365 id: "moonshotai/kimi-k2.6".to_string(),
366 provider: ProviderKind::Openrouter,
367 aliases: vec!["openrouter-kimi-k2.6".to_string()],
368 supports_tools: true,
369 supports_reasoning: true,
370 },
371 ModelInfo {
372 id: "minimax/minimax-m3".to_string(),
373 provider: ProviderKind::Openrouter,
374 aliases: vec![
375 "minimax-m3".to_string(),
376 "minimax-m-3".to_string(),
377 "openrouter-minimax-m3".to_string(),
378 ],
379 supports_tools: true,
380 supports_reasoning: true,
381 },
382 ModelInfo {
383 id: "z-ai/glm-5.1".to_string(),
384 provider: ProviderKind::Openrouter,
385 aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
386 supports_tools: true,
387 supports_reasoning: true,
388 },
389 ModelInfo {
390 id: "z-ai/glm-5.2".to_string(),
391 provider: ProviderKind::Openrouter,
392 aliases: vec!["glm-5.2".to_string(), "zai-glm-5.2".to_string()],
393 supports_tools: true,
394 supports_reasoning: true,
395 },
396 ModelInfo {
397 id: "z-ai/glm-5-turbo".to_string(),
398 provider: ProviderKind::Openrouter,
399 aliases: vec!["glm-5-turbo".to_string(), "zai-glm-5-turbo".to_string()],
400 supports_tools: true,
401 supports_reasoning: true,
402 },
403 ModelInfo {
404 id: "GLM-5.2".to_string(),
405 provider: ProviderKind::Zai,
406 aliases: vec![
407 "glm-5.2".to_string(),
408 "glm-5-2".to_string(),
409 "zai-glm-5.2".to_string(),
410 "zai-glm-5-2".to_string(),
411 ],
412 supports_tools: true,
413 supports_reasoning: true,
414 },
415 ModelInfo {
416 id: "GLM-5.1".to_string(),
417 provider: ProviderKind::Zai,
418 aliases: vec![
419 "glm-5.1".to_string(),
420 "glm-5-1".to_string(),
421 "zai-glm-5.1".to_string(),
422 "zai-glm-5-1".to_string(),
423 ],
424 supports_tools: true,
425 supports_reasoning: true,
426 },
427 ModelInfo {
428 id: "GLM-5-Turbo".to_string(),
429 provider: ProviderKind::Zai,
430 aliases: vec![
431 "glm-5-turbo".to_string(),
432 "glm-5turbo".to_string(),
433 "zai-glm-5-turbo".to_string(),
434 ],
435 supports_tools: true,
436 supports_reasoning: true,
437 },
438 ModelInfo {
439 id: "tencent/hy3-preview".to_string(),
440 provider: ProviderKind::Openrouter,
441 aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
442 supports_tools: true,
443 supports_reasoning: true,
444 },
445 ModelInfo {
446 id: "google/gemma-4-31b-it".to_string(),
447 provider: ProviderKind::Openrouter,
448 aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
449 supports_tools: true,
450 supports_reasoning: true,
451 },
452 ModelInfo {
453 id: "google/gemma-4-26b-a4b-it".to_string(),
454 provider: ProviderKind::Openrouter,
455 aliases: vec![
456 "gemma-4-26b-a4b".to_string(),
457 "gemma-4-26b-a4b-it".to_string(),
458 ],
459 supports_tools: true,
460 supports_reasoning: true,
461 },
462 ModelInfo {
463 id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
464 provider: ProviderKind::Openrouter,
465 aliases: vec![
466 "nemotron-3-nano-omni".to_string(),
467 "nemotron-3-nano-omni-reasoning".to_string(),
468 ],
469 supports_tools: true,
470 supports_reasoning: true,
471 },
472 ModelInfo {
473 id: "mimo-v2.5-pro".to_string(),
474 provider: ProviderKind::XiaomiMimo,
475 aliases: vec![
476 "mimo".to_string(),
477 "pro".to_string(),
478 "xiaomi-mimo-v2.5-pro".to_string(),
479 "xiaomi-mimo-v2-5-pro".to_string(),
480 ],
481 supports_tools: true,
482 supports_reasoning: true,
483 },
484 ModelInfo {
485 id: "mimo-v2.5".to_string(),
486 provider: ProviderKind::XiaomiMimo,
487 aliases: vec![
488 "omni".to_string(),
489 "mimo-omni".to_string(),
490 "v2.5-omni".to_string(),
491 "mimo-v2.5-omni".to_string(),
492 "xiaomi-mimo-v2.5".to_string(),
493 "xiaomi-mimo-v2.5-omni".to_string(),
494 ],
495 supports_tools: true,
496 supports_reasoning: true,
497 },
498 ModelInfo {
499 id: "mimo-v2.5-asr".to_string(),
500 provider: ProviderKind::XiaomiMimo,
501 aliases: vec![
502 "asr".to_string(),
503 "speech-to-text".to_string(),
504 "transcribe".to_string(),
505 ],
506 supports_tools: false,
507 supports_reasoning: false,
508 },
509 ModelInfo {
510 id: "mimo-v2.5-tts".to_string(),
511 provider: ProviderKind::XiaomiMimo,
512 aliases: vec![
513 "tts".to_string(),
514 "speech".to_string(),
515 "mimo-tts".to_string(),
516 ],
517 supports_tools: false,
518 supports_reasoning: false,
519 },
520 ModelInfo {
521 id: "mimo-v2.5-tts-voicedesign".to_string(),
522 provider: ProviderKind::XiaomiMimo,
523 aliases: vec![
524 "voicedesign".to_string(),
525 "voice-design".to_string(),
526 "mimo-voice-design".to_string(),
527 ],
528 supports_tools: false,
529 supports_reasoning: false,
530 },
531 ModelInfo {
532 id: "mimo-v2.5-tts-voiceclone".to_string(),
533 provider: ProviderKind::XiaomiMimo,
534 aliases: vec![
535 "voiceclone".to_string(),
536 "voice-clone".to_string(),
537 "mimo-voice-clone".to_string(),
538 ],
539 supports_tools: false,
540 supports_reasoning: false,
541 },
542 ModelInfo {
543 id: "mimo-v2-tts".to_string(),
544 provider: ProviderKind::XiaomiMimo,
545 aliases: vec!["mimo-v2-speech".to_string()],
546 supports_tools: false,
547 supports_reasoning: false,
548 },
549 ModelInfo {
550 id: "deepseek/deepseek-v4-pro".to_string(),
551 provider: ProviderKind::Novita,
552 aliases: vec![
553 "deepseek-v4-pro".to_string(),
554 "novita-deepseek-v4-pro".to_string(),
555 ],
556 supports_tools: true,
557 supports_reasoning: true,
558 },
559 ModelInfo {
560 id: "deepseek/deepseek-v4-flash".to_string(),
561 provider: ProviderKind::Novita,
562 aliases: vec![
563 "deepseek-v4-flash".to_string(),
564 "deepseek-chat".to_string(),
565 "deepseek-reasoner".to_string(),
566 "novita-deepseek-v4-flash".to_string(),
567 ],
568 supports_tools: true,
569 supports_reasoning: true,
570 },
571 ModelInfo {
572 id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
573 provider: ProviderKind::Fireworks,
574 aliases: vec![
575 "deepseek-v4-pro".to_string(),
576 "fireworks-deepseek-v4-pro".to_string(),
577 ],
578 supports_tools: true,
579 supports_reasoning: true,
580 },
581 ModelInfo {
582 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
583 provider: ProviderKind::Siliconflow,
584 aliases: vec![
585 "deepseek-v4-pro".to_string(),
586 "deepseek-reasoner".to_string(),
587 "deepseek-r1".to_string(),
588 "siliconflow-deepseek-v4-pro".to_string(),
589 ],
590 supports_tools: true,
591 supports_reasoning: true,
592 },
593 ModelInfo {
594 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
595 provider: ProviderKind::Siliconflow,
596 aliases: vec![
597 "deepseek-v4-flash".to_string(),
598 "deepseek-chat".to_string(),
599 "deepseek-v3".to_string(),
600 "siliconflow-deepseek-v4-flash".to_string(),
601 ],
602 supports_tools: true,
603 supports_reasoning: true,
604 },
605 ModelInfo {
606 id: "trinity-large-preview".to_string(),
607 provider: ProviderKind::Arcee,
608 aliases: vec!["arcee-trinity-large-preview".to_string()],
609 supports_tools: true,
610 supports_reasoning: false,
611 },
612 ModelInfo {
613 id: "kimi-k2.7-code".to_string(),
614 provider: ProviderKind::Moonshot,
615 aliases: vec![
616 "kimi".to_string(),
617 "kimi-k2".to_string(),
618 "kimi-k2.7".to_string(),
619 "kimi-code".to_string(),
620 "moonshot-kimi-k2.7-code".to_string(),
621 ],
622 supports_tools: true,
623 supports_reasoning: true,
624 },
625 ModelInfo {
626 id: "kimi-k2.6".to_string(),
627 provider: ProviderKind::Moonshot,
628 aliases: vec!["kimi-k2.6".to_string(), "moonshot-kimi-k2.6".to_string()],
629 supports_tools: true,
630 supports_reasoning: true,
631 },
632 ModelInfo {
633 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
634 provider: ProviderKind::Sglang,
635 aliases: vec![
636 "deepseek-v4-pro".to_string(),
637 "sglang-deepseek-v4-pro".to_string(),
638 ],
639 supports_tools: true,
640 supports_reasoning: true,
641 },
642 ModelInfo {
643 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
644 provider: ProviderKind::Sglang,
645 aliases: vec![
646 "deepseek-v4-flash".to_string(),
647 "deepseek-chat".to_string(),
648 "deepseek-reasoner".to_string(),
649 "sglang-deepseek-v4-flash".to_string(),
650 ],
651 supports_tools: true,
652 supports_reasoning: true,
653 },
654 ModelInfo {
655 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
656 provider: ProviderKind::Vllm,
657 aliases: vec![
658 "deepseek-v4-pro".to_string(),
659 "vllm-deepseek-v4-pro".to_string(),
660 ],
661 supports_tools: true,
662 supports_reasoning: true,
663 },
664 ModelInfo {
665 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
666 provider: ProviderKind::Vllm,
667 aliases: vec![
668 "deepseek-v4-flash".to_string(),
669 "deepseek-chat".to_string(),
670 "deepseek-reasoner".to_string(),
671 "vllm-deepseek-v4-flash".to_string(),
672 ],
673 supports_tools: true,
674 supports_reasoning: true,
675 },
676 ModelInfo {
677 id: "deepseek-v4-flash".to_string(),
678 provider: ProviderKind::Ollama,
679 aliases: vec![],
680 supports_tools: true,
681 supports_reasoning: true,
682 },
683 ModelInfo {
684 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
685 provider: ProviderKind::Huggingface,
686 aliases: vec![
687 "deepseek-v4-pro".to_string(),
688 "hf-deepseek-v4-pro".to_string(),
689 ],
690 supports_tools: true,
691 supports_reasoning: true,
692 },
693 ModelInfo {
694 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
695 provider: ProviderKind::Huggingface,
696 aliases: vec![
697 "deepseek-v4-flash".to_string(),
698 "deepseek-chat".to_string(),
699 "deepseek-reasoner".to_string(),
700 "hf-deepseek-v4-flash".to_string(),
701 ],
702 supports_tools: true,
703 supports_reasoning: true,
704 },
705 ModelInfo {
707 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
708 provider: ProviderKind::Together,
709 aliases: vec![
710 "deepseek-v4-pro".to_string(),
711 "together-deepseek-v4-pro".to_string(),
712 ],
713 supports_tools: true,
714 supports_reasoning: true,
715 },
716 ModelInfo {
717 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
718 provider: ProviderKind::Together,
719 aliases: vec![
720 "deepseek-v4-flash".to_string(),
721 "deepseek-chat".to_string(),
722 "together-deepseek-v4-flash".to_string(),
723 ],
724 supports_tools: true,
725 supports_reasoning: true,
726 },
727 ModelInfo {
728 id: "thinkingmachines/inkling".to_string(),
731 provider: ProviderKind::Together,
732 aliases: vec!["inkling".to_string(), "together-inkling".to_string()],
733 supports_tools: true,
734 supports_reasoning: true,
735 },
736 ModelInfo {
738 id: "qwen/qwen3.7-max".to_string(),
739 provider: ProviderKind::Openrouter,
740 aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
741 supports_tools: true,
742 supports_reasoning: true,
743 },
744 ModelInfo {
746 id: "gpt-5.5".to_string(),
747 provider: ProviderKind::OpenaiCodex,
748 aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
749 supports_tools: true,
750 supports_reasoning: true,
751 },
752 ModelInfo {
754 id: "claude-opus-4-8".to_string(),
755 provider: ProviderKind::Anthropic,
756 aliases: vec!["opus".to_string(), "claude-opus".to_string()],
757 supports_tools: true,
758 supports_reasoning: true,
759 },
760 ModelInfo {
761 id: "claude-sonnet-4-6".to_string(),
762 provider: ProviderKind::Anthropic,
763 aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
764 supports_tools: true,
765 supports_reasoning: true,
766 },
767 ModelInfo {
768 id: "claude-haiku-4-5".to_string(),
769 provider: ProviderKind::Anthropic,
770 aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
771 supports_tools: true,
772 supports_reasoning: false,
773 },
774 ModelInfo {
775 id: "claude-sonnet-5".to_string(),
776 provider: ProviderKind::Anthropic,
777 aliases: vec!["sonnet-5".to_string()],
778 supports_tools: true,
779 supports_reasoning: true,
780 },
781 ModelInfo {
782 id: "claude-fable-5".to_string(),
783 provider: ProviderKind::Anthropic,
784 aliases: vec!["fable".to_string(), "fable-5".to_string()],
785 supports_tools: true,
786 supports_reasoning: true,
787 },
788 ModelInfo {
790 id: "deepseek-v4-flash".to_string(),
791 provider: ProviderKind::Openmodel,
792 aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
793 supports_tools: true,
794 supports_reasoning: true,
795 },
796 ModelInfo {
798 id: "minimax/minimax-m2.7".to_string(),
799 provider: ProviderKind::Openrouter,
800 aliases: vec![
801 "minimax-2.7".to_string(),
802 "minimax-2-7".to_string(),
803 "openrouter-minimax-2.7".to_string(),
804 ],
805 supports_tools: true,
806 supports_reasoning: true,
807 },
808 ModelInfo {
809 id: "step-3.7-flash".to_string(),
810 provider: ProviderKind::Stepfun,
811 aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
812 supports_tools: true,
813 supports_reasoning: false,
814 },
815 ModelInfo {
816 id: "MiniMax-M3".to_string(),
817 provider: ProviderKind::Minimax,
818 aliases: vec![
819 "minimax".to_string(),
820 "minimax-m3".to_string(),
821 "minimax-m-3".to_string(),
822 ],
823 supports_tools: true,
824 supports_reasoning: true,
825 },
826 ModelInfo {
827 id: "MiniMax-M2.7".to_string(),
828 provider: ProviderKind::Minimax,
829 aliases: vec![
830 "minimax-m2.7".to_string(),
831 "minimax-m2-7".to_string(),
832 "minimax-m-2.7".to_string(),
833 "minimax-m-2-7".to_string(),
834 ],
835 supports_tools: true,
836 supports_reasoning: true,
837 },
838 ModelInfo {
839 id: "MiniMax-M3".to_string(),
840 provider: ProviderKind::MinimaxAnthropic,
841 aliases: vec![
842 "minimax-anthropic".to_string(),
843 "minimax-anthropic-m3".to_string(),
844 "minimax-m3".to_string(),
845 ],
846 supports_tools: true,
847 supports_reasoning: true,
848 },
849 ModelInfo {
850 id: "MiniMax-M2.7".to_string(),
851 provider: ProviderKind::MinimaxAnthropic,
852 aliases: vec![
853 "minimax-anthropic-m2.7".to_string(),
854 "minimax-anthropic-m2-7".to_string(),
855 "minimax-m2.7".to_string(),
856 ],
857 supports_tools: true,
858 supports_reasoning: true,
859 },
860 ModelInfo {
861 id: "MiniMax-M2.7-highspeed".to_string(),
862 provider: ProviderKind::Minimax,
863 aliases: vec![
864 "minimax-m2.7-highspeed".to_string(),
865 "minimax-m2-7-highspeed".to_string(),
866 "minimax-m-2.7-highspeed".to_string(),
867 "minimax-m-2-7-highspeed".to_string(),
868 ],
869 supports_tools: true,
870 supports_reasoning: true,
871 },
872 ModelInfo {
873 id: "MiniMax-M2.5".to_string(),
874 provider: ProviderKind::Minimax,
875 aliases: vec![
876 "minimax-m2.5".to_string(),
877 "minimax-m2-5".to_string(),
878 "minimax-m-2.5".to_string(),
879 "minimax-m-2-5".to_string(),
880 ],
881 supports_tools: true,
882 supports_reasoning: true,
883 },
884 ModelInfo {
885 id: "MiniMax-M2.5-highspeed".to_string(),
886 provider: ProviderKind::Minimax,
887 aliases: vec![
888 "minimax-m2.5-highspeed".to_string(),
889 "minimax-m2-5-highspeed".to_string(),
890 "minimax-m-2.5-highspeed".to_string(),
891 "minimax-m-2-5-highspeed".to_string(),
892 ],
893 supports_tools: true,
894 supports_reasoning: true,
895 },
896 ModelInfo {
897 id: "MiniMax-M2.1".to_string(),
898 provider: ProviderKind::Minimax,
899 aliases: vec![
900 "minimax-m2.1".to_string(),
901 "minimax-m2-1".to_string(),
902 "minimax-m-2.1".to_string(),
903 "minimax-m-2-1".to_string(),
904 ],
905 supports_tools: true,
906 supports_reasoning: true,
907 },
908 ModelInfo {
909 id: "MiniMax-M2.1-highspeed".to_string(),
910 provider: ProviderKind::Minimax,
911 aliases: vec![
912 "minimax-m2.1-highspeed".to_string(),
913 "minimax-m2-1-highspeed".to_string(),
914 "minimax-m-2.1-highspeed".to_string(),
915 "minimax-m-2-1-highspeed".to_string(),
916 ],
917 supports_tools: true,
918 supports_reasoning: true,
919 },
920 ModelInfo {
921 id: "MiniMax-M2".to_string(),
922 provider: ProviderKind::Minimax,
923 aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
924 supports_tools: true,
925 supports_reasoning: true,
926 },
927 ModelInfo {
929 id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
930 provider: ProviderKind::Openrouter,
931 aliases: vec![
932 "nvidia/nemotron-3-ultra".to_string(),
933 "nemotron-3-ultra".to_string(),
934 "nemotron-3-ultra-550b-a55b".to_string(),
935 "nvidia-nemotron-3-ultra".to_string(),
936 "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
937 ],
938 supports_tools: true,
939 supports_reasoning: true,
940 },
941 ModelInfo {
943 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
944 provider: ProviderKind::Deepinfra,
945 aliases: vec![
946 "deepseek-v4-pro".to_string(),
947 "di-deepseek-v4-pro".to_string(),
948 ],
949 supports_tools: true,
950 supports_reasoning: true,
951 },
952 ModelInfo {
953 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
954 provider: ProviderKind::Deepinfra,
955 aliases: vec![
956 "deepseek-v4-flash".to_string(),
957 "di-deepseek-v4-flash".to_string(),
958 ],
959 supports_tools: true,
960 supports_reasoning: true,
961 },
962 ModelInfo {
964 id: "fugu".to_string(),
965 provider: ProviderKind::Sakana,
966 aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
967 supports_tools: true,
968 supports_reasoning: false,
969 },
970 ModelInfo {
971 id: "fugu-ultra-20260615".to_string(),
972 provider: ProviderKind::Sakana,
973 aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
974 supports_tools: true,
975 supports_reasoning: true,
976 },
977 ModelInfo {
979 id: "LongCat-2.0".to_string(),
980 provider: ProviderKind::LongCat,
981 aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
982 supports_tools: true,
983 supports_reasoning: true,
984 },
985 ModelInfo {
989 id: "deepseek-v4-pro".to_string(),
990 provider: ProviderKind::OpencodeGo,
991 aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
992 supports_tools: true,
993 supports_reasoning: true,
994 },
995 ModelInfo {
996 id: "grok-4.5".to_string(),
997 provider: ProviderKind::OpencodeGo,
998 aliases: vec!["opencode-go/grok-4.5".to_string()],
999 supports_tools: true,
1000 supports_reasoning: true,
1001 },
1002 ModelInfo {
1003 id: "glm-5.2".to_string(),
1004 provider: ProviderKind::OpencodeGo,
1005 aliases: vec!["opencode-go/glm-5.2".to_string()],
1006 supports_tools: true,
1007 supports_reasoning: true,
1008 },
1009 ModelInfo {
1010 id: "glm-5.1".to_string(),
1011 provider: ProviderKind::OpencodeGo,
1012 aliases: vec!["opencode-go/glm-5.1".to_string()],
1013 supports_tools: true,
1014 supports_reasoning: true,
1015 },
1016 ModelInfo {
1017 id: "kimi-k3".to_string(),
1018 provider: ProviderKind::OpencodeGo,
1019 aliases: vec!["opencode-go/kimi-k3".to_string()],
1020 supports_tools: true,
1021 supports_reasoning: true,
1022 },
1023 ModelInfo {
1024 id: "kimi-k2.7-code".to_string(),
1025 provider: ProviderKind::OpencodeGo,
1026 aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
1027 supports_tools: true,
1028 supports_reasoning: true,
1029 },
1030 ModelInfo {
1031 id: "kimi-k2.6".to_string(),
1032 provider: ProviderKind::OpencodeGo,
1033 aliases: vec!["opencode-go/kimi-k2.6".to_string()],
1034 supports_tools: true,
1035 supports_reasoning: true,
1036 },
1037 ModelInfo {
1038 id: "deepseek-v4-flash".to_string(),
1039 provider: ProviderKind::OpencodeGo,
1040 aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
1041 supports_tools: true,
1042 supports_reasoning: true,
1043 },
1044 ModelInfo {
1045 id: "mimo-v2.5".to_string(),
1046 provider: ProviderKind::OpencodeGo,
1047 aliases: vec!["opencode-go/mimo-v2.5".to_string()],
1048 supports_tools: true,
1049 supports_reasoning: true,
1050 },
1051 ModelInfo {
1052 id: "mimo-v2.5-pro".to_string(),
1053 provider: ProviderKind::OpencodeGo,
1054 aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
1055 supports_tools: true,
1056 supports_reasoning: true,
1057 },
1058 ModelInfo {
1060 id: "muse-spark-1.1".to_string(),
1061 provider: ProviderKind::Meta,
1062 aliases: vec!["muse-spark".to_string(), "muse".to_string()],
1063 supports_tools: true,
1064 supports_reasoning: true,
1065 },
1066 ModelInfo {
1068 id: "grok-4.5".to_string(),
1069 provider: ProviderKind::Xai,
1070 aliases: vec!["grok".to_string(), "xai-grok-4.5".to_string()],
1071 supports_tools: true,
1072 supports_reasoning: true,
1073 },
1074 ModelInfo {
1075 id: "grok-4.3".to_string(),
1076 provider: ProviderKind::Xai,
1077 aliases: vec!["xai-grok-4.3".to_string()],
1078 supports_tools: true,
1079 supports_reasoning: true,
1080 },
1081 ModelInfo {
1082 id: "grok-build".to_string(),
1083 provider: ProviderKind::Xai,
1084 aliases: vec!["xai-grok-build".to_string()],
1085 supports_tools: true,
1086 supports_reasoning: true,
1087 },
1088 ModelInfo {
1089 id: "grok-composer-2.5-fast".to_string(),
1090 provider: ProviderKind::Xai,
1091 aliases: vec!["xai-grok-composer".to_string()],
1092 supports_tools: true,
1093 supports_reasoning: false,
1094 },
1095 ModelInfo {
1096 id: "grok-4.20-0309-reasoning".to_string(),
1097 provider: ProviderKind::Xai,
1098 aliases: vec!["xai-grok-reasoning".to_string()],
1099 supports_tools: true,
1100 supports_reasoning: true,
1101 },
1102 ModelInfo {
1103 id: "grok-4.20-0309-non-reasoning".to_string(),
1104 provider: ProviderKind::Xai,
1105 aliases: vec!["xai-grok-fast".to_string()],
1106 supports_tools: true,
1107 supports_reasoning: false,
1108 },
1109 ];
1110 Self::new(models)
1111 }
1112}
1113
1114impl ModelRegistry {
1115 #[must_use]
1121 pub fn new(models: Vec<ModelInfo>) -> Self {
1122 let mut alias_map = HashMap::new();
1123 for (idx, model) in models.iter().enumerate() {
1124 alias_map.entry(normalize(&model.id)).or_insert(idx);
1125 for alias in &model.aliases {
1126 alias_map.entry(normalize(alias)).or_insert(idx);
1127 }
1128 }
1129 Self { models, alias_map }
1130 }
1131
1132 #[must_use]
1134 pub fn list(&self) -> Vec<ModelInfo> {
1135 self.models.clone()
1136 }
1137
1138 #[must_use]
1150 pub fn resolve(
1151 &self,
1152 requested: Option<&str>,
1153 provider_hint: Option<ProviderKind>,
1154 ) -> ModelResolution {
1155 let mut fallback_chain = Vec::new();
1156
1157 if let Some(name) = requested {
1158 fallback_chain.push(format!("requested:{name}"));
1159 if provider_hint == Some(ProviderKind::Ollama) {
1160 return ModelResolution {
1161 requested: Some(name.to_string()),
1162 resolved: ModelInfo {
1163 id: name.trim().to_string(),
1164 provider: ProviderKind::Ollama,
1165 aliases: Vec::new(),
1166 supports_tools: true,
1167 supports_reasoning: false,
1168 },
1169 used_fallback: false,
1170 fallback_chain,
1171 };
1172 }
1173 if provider_hint == Some(ProviderKind::OpencodeGo)
1179 && let Some(canonical) = opencode_go_chat_model_id(name)
1180 && let Some(model) = self
1181 .models
1182 .iter()
1183 .find(|model| {
1184 model.provider == ProviderKind::OpencodeGo
1185 && model.id.eq_ignore_ascii_case(canonical)
1186 })
1187 .cloned()
1188 {
1189 return ModelResolution {
1190 requested: Some(name.to_string()),
1191 resolved: model,
1192 used_fallback: false,
1193 fallback_chain,
1194 };
1195 }
1196 if provider_hint != Some(ProviderKind::OpencodeGo)
1197 && let Some(provider) = provider_hint
1198 && let Some(model) = self
1199 .models
1200 .iter()
1201 .find(|m| m.provider == provider && model_matches(m, name))
1202 .cloned()
1203 {
1204 return ModelResolution {
1205 requested: Some(name.to_string()),
1206 resolved: model,
1207 used_fallback: false,
1208 fallback_chain,
1209 };
1210 }
1211 if provider_hint == Some(ProviderKind::Atlascloud)
1212 && let Some(model) = atlascloud_passthrough_model(name)
1213 {
1214 return ModelResolution {
1215 requested: Some(name.to_string()),
1216 resolved: model,
1217 used_fallback: false,
1218 fallback_chain,
1219 };
1220 }
1221 if provider_hint == Some(ProviderKind::Arcee)
1222 && let Some(model) = arcee_passthrough_model(name)
1223 {
1224 return ModelResolution {
1225 requested: Some(name.to_string()),
1226 resolved: model,
1227 used_fallback: false,
1228 fallback_chain,
1229 };
1230 }
1231 if provider_hint == Some(ProviderKind::XiaomiMimo)
1232 && let Some(model) = xiaomi_mimo_passthrough_model(name)
1233 {
1234 return ModelResolution {
1235 requested: Some(name.to_string()),
1236 resolved: model,
1237 used_fallback: false,
1238 fallback_chain,
1239 };
1240 }
1241 if provider_hint != Some(ProviderKind::OpencodeGo)
1242 && let Some(idx) = self.alias_map.get(&normalize(name))
1243 {
1244 return ModelResolution {
1245 requested: Some(name.to_string()),
1246 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1247 used_fallback: false,
1248 fallback_chain,
1249 };
1250 }
1251 }
1252
1253 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1254 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1255 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1256 return ModelResolution {
1257 requested: requested.map(ToOwned::to_owned),
1258 resolved: model,
1259 used_fallback: true,
1260 fallback_chain,
1261 };
1262 }
1263
1264 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1265 id: "deepseek-v4-pro".to_string(),
1266 provider: ProviderKind::Deepseek,
1267 aliases: Vec::new(),
1268 supports_tools: true,
1269 supports_reasoning: true,
1270 });
1271 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1272 ModelResolution {
1273 requested: requested.map(ToOwned::to_owned),
1274 resolved: final_fallback,
1275 used_fallback: true,
1276 fallback_chain,
1277 }
1278 }
1279}
1280
1281fn normalize(value: &str) -> String {
1282 value.trim().to_ascii_lowercase()
1283}
1284
1285#[must_use]
1286pub fn model_family(model_id: &str) -> ModelFamily {
1288 let normalized = normalize(model_id);
1289 if normalized.is_empty() {
1290 return ModelFamily::Inferencer;
1291 }
1292
1293 if normalized.contains("deepseek") {
1294 return ModelFamily::DeepSeek;
1295 }
1296 if normalized.contains("claude") || normalized.contains("anthropic") {
1297 return ModelFamily::Anthropic;
1298 }
1299 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1300 return ModelFamily::GptOss;
1301 }
1302 if normalized.starts_with("gpt-")
1303 || normalized.contains("/gpt-")
1304 || normalized.contains("openai/")
1305 {
1306 return ModelFamily::OpenAI;
1307 }
1308 if normalized.contains("gemini")
1309 || normalized.contains("gemma")
1310 || normalized.contains("google/")
1311 {
1312 return ModelFamily::Google;
1313 }
1314 if normalized.contains("llama")
1315 || normalized.contains("muse-spark")
1316 || normalized.contains("meta-")
1317 || normalized.contains("meta/")
1318 {
1319 return ModelFamily::Meta;
1320 }
1321 if normalized.contains("mistral")
1322 || normalized.contains("mixtral")
1323 || normalized.contains("codestral")
1324 {
1325 return ModelFamily::Mistral;
1326 }
1327 if normalized.contains("qwen") {
1328 return ModelFamily::Qwen;
1329 }
1330 if normalized.contains("grok") {
1331 return ModelFamily::Grok;
1332 }
1333 if normalized.contains("cohere") || normalized.contains("command-r") {
1334 return ModelFamily::Cohere;
1335 }
1336
1337 ModelFamily::Inferencer
1338}
1339
1340fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1341 let requested = normalize(requested);
1342 normalize(&model.id) == requested
1343 || model
1344 .aliases
1345 .iter()
1346 .any(|alias| normalize(alias) == requested)
1347}
1348
1349fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1350 let requested = requested.trim();
1351 if model.id.eq_ignore_ascii_case(requested) {
1352 model.id = requested.to_string();
1353 }
1354 model
1355}
1356
1357fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1358 let requested = requested.trim();
1359 if requested.is_empty() || !requested.contains('/') {
1360 return None;
1361 }
1362
1363 Some(ModelInfo {
1364 id: requested.to_string(),
1365 provider: ProviderKind::Atlascloud,
1366 aliases: Vec::new(),
1367 supports_tools: true,
1368 supports_reasoning: true,
1369 })
1370}
1371
1372fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1373 let requested = requested.trim();
1374 if requested.is_empty() {
1375 return None;
1376 }
1377 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1378
1379 Some(ModelInfo {
1380 id: requested.to_string(),
1381 provider: ProviderKind::Arcee,
1382 aliases: Vec::new(),
1383 supports_tools: true,
1384 supports_reasoning,
1385 })
1386}
1387
1388fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1389 let requested = requested.trim();
1390 if requested.is_empty() || requested.chars().any(char::is_control) {
1391 return None;
1392 }
1393
1394 Some(ModelInfo {
1395 id: requested.to_string(),
1396 provider: ProviderKind::XiaomiMimo,
1397 aliases: Vec::new(),
1398 supports_tools: true,
1399 supports_reasoning: true,
1400 })
1401}
1402
1403#[cfg(test)]
1404mod tests {
1405 use super::*;
1406
1407 #[test]
1408 fn model_registry_new_builds_alias_map_correctly() {
1409 let models = vec![
1410 ModelInfo {
1411 id: "Model-A".to_string(),
1412 provider: ProviderKind::Deepseek,
1413 aliases: vec!["alias-1".to_string(), " ALIAS-2 ".to_string()],
1414 supports_tools: true,
1415 supports_reasoning: false,
1416 },
1417 ModelInfo {
1418 id: "model-b".to_string(),
1419 provider: ProviderKind::Deepseek,
1420 aliases: vec!["alias-1".to_string()], supports_tools: true,
1422 supports_reasoning: true,
1423 },
1424 ];
1425
1426 let registry = ModelRegistry::new(models);
1427
1428 assert_eq!(registry.alias_map.len(), 4); assert_eq!(registry.alias_map.get("model-a"), Some(&0));
1430 assert_eq!(registry.alias_map.get("alias-1"), Some(&0)); assert_eq!(registry.alias_map.get("alias-2"), Some(&0)); assert_eq!(registry.alias_map.get("model-b"), Some(&1));
1433 }
1434
1435 #[test]
1436 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1437 let registry = ModelRegistry::default();
1438 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1439
1440 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1441 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1442 }
1443
1444 #[test]
1445 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1446 let registry = ModelRegistry::default();
1447 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1448
1449 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1450 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1451 }
1452
1453 #[test]
1454 fn nvidia_nim_default_uses_catalog_model_id() {
1455 let registry = ModelRegistry::default();
1456 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1457
1458 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1459 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1460 }
1461
1462 #[test]
1463 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1464 let registry = ModelRegistry::default();
1465 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1466
1467 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1468 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1469 }
1470
1471 #[test]
1472 fn atlascloud_default_uses_namespaced_model_id() {
1473 let registry = ModelRegistry::default();
1474 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1475
1476 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1477 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1478 assert!(resolved.resolved.supports_reasoning);
1479 }
1480
1481 #[test]
1482 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1483 let registry = ModelRegistry::default();
1484 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1485
1486 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1487 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1488 }
1489
1490 #[test]
1491 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1492 let registry = ModelRegistry::default();
1493 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1494
1495 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1496 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1497 }
1498
1499 #[test]
1500 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1501 let registry = ModelRegistry::default();
1502 let resolved =
1503 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1504
1505 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1506 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1507 assert!(resolved.resolved.supports_tools);
1508 assert!(resolved.resolved.supports_reasoning);
1509 assert!(!resolved.used_fallback);
1510 }
1511
1512 #[test]
1513 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1514 let registry = ModelRegistry::default();
1515 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1516
1517 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1518 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1519 assert!(!resolved.used_fallback);
1520 }
1521
1522 #[test]
1523 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1524 let registry = ModelRegistry::default();
1525 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1526
1527 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1528 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1529 assert!(resolved.used_fallback);
1530 }
1531
1532 #[test]
1533 fn openrouter_default_uses_namespaced_model_id() {
1534 let registry = ModelRegistry::default();
1535 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1536
1537 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1538 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1539 }
1540
1541 #[test]
1542 fn xiaomi_mimo_default_uses_canonical_model_id() {
1543 let registry = ModelRegistry::default();
1544 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1545
1546 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1547 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1548 assert!(resolved.resolved.supports_reasoning);
1549 }
1550
1551 #[test]
1552 fn moonshot_default_and_aliases_use_kimi_k27_code() {
1553 let registry = ModelRegistry::default();
1554
1555 for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1556 let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1557
1558 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1559 assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1560 assert!(resolved.resolved.supports_tools);
1561 assert!(resolved.resolved.supports_reasoning);
1562 }
1563 }
1564
1565 #[test]
1566 fn moonshot_explicit_kimi_k26_remains_available() {
1567 let registry = ModelRegistry::default();
1568 let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1569
1570 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1571 assert_eq!(resolved.resolved.id, "kimi-k2.6");
1572 assert!(resolved.resolved.supports_reasoning);
1573 }
1574
1575 #[test]
1576 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1577 let registry = ModelRegistry::default();
1578 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1579 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1580 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1581 assert!(!resolved.resolved.supports_tools);
1582 assert!(!resolved.resolved.supports_reasoning);
1583
1584 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1585 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1586
1587 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1588 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1589 }
1590
1591 #[test]
1592 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1593 let registry = ModelRegistry::default();
1594
1595 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1596 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1597 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1598 assert!(resolved.resolved.supports_tools);
1599 }
1600
1601 #[test]
1602 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1603 let registry = ModelRegistry::default();
1604 let resolved =
1605 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1606
1607 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1608 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1609 assert!(!resolved.used_fallback);
1610 }
1611
1612 #[test]
1613 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1614 let registry = ModelRegistry::default();
1615 let resolved = registry.resolve(
1616 Some("deepseek/deepseek-v4-pro"),
1617 Some(ProviderKind::XiaomiMimo),
1618 );
1619
1620 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1621 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1622 assert!(!resolved.used_fallback);
1623 }
1624
1625 #[test]
1626 fn wanjie_ark_default_uses_reasoner_model_id() {
1627 let registry = ModelRegistry::default();
1628 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1629
1630 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1631 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1632 assert!(resolved.resolved.supports_reasoning);
1633 }
1634
1635 #[test]
1636 fn novita_default_uses_namespaced_model_id() {
1637 let registry = ModelRegistry::default();
1638 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1639
1640 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1641 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1642 }
1643
1644 #[test]
1645 fn fireworks_default_uses_canonical_model_id() {
1646 let registry = ModelRegistry::default();
1647 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1648
1649 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1650 assert_eq!(
1651 resolved.resolved.id,
1652 "accounts/fireworks/models/deepseek-v4-pro"
1653 );
1654 }
1655
1656 #[test]
1657 fn siliconflow_default_uses_canonical_pro_model_id() {
1658 let registry = ModelRegistry::default();
1659 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1660
1661 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1662 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1663 assert!(resolved.resolved.supports_reasoning);
1664 }
1665
1666 #[test]
1667 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1668 let registry = ModelRegistry::default();
1669 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1670
1671 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1672 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1673 assert!(resolved.resolved.supports_reasoning);
1674 }
1675
1676 #[test]
1677 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1678 let registry = ModelRegistry::default();
1679 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1680
1681 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1682 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1683 assert!(resolved.resolved.supports_reasoning);
1684 }
1685
1686 #[test]
1687 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1688 let registry = ModelRegistry::default();
1689 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1690
1691 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1692 assert_eq!(resolved.resolved.id, "trinity-mini");
1693 assert!(resolved.resolved.supports_reasoning);
1694 assert!(!resolved.used_fallback);
1695 }
1696
1697 #[test]
1698 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1699 let registry = ModelRegistry::default();
1700 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1701
1702 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1703 assert_eq!(resolved.resolved.id, "trinity-large-next");
1704 assert!(!resolved.resolved.supports_reasoning);
1705 assert!(!resolved.used_fallback);
1706 }
1707
1708 #[test]
1709 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1710 let registry = ModelRegistry::default();
1711 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1712
1713 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1714 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1715 }
1716
1717 #[test]
1718 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1719 let registry = ModelRegistry::default();
1720 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1721
1722 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1723 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1724 }
1725
1726 #[test]
1727 fn sglang_default_uses_canonical_model_id() {
1728 let registry = ModelRegistry::default();
1729 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1730
1731 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1732 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1733 }
1734
1735 #[test]
1736 fn zai_direct_models_resolve_when_provider_hinted() {
1737 let registry = ModelRegistry::default();
1738
1739 let default = registry.resolve(None, Some(ProviderKind::Zai));
1741 assert_eq!(default.resolved.provider, ProviderKind::Zai);
1742 assert_eq!(default.resolved.id, "GLM-5.2");
1743
1744 for (alias, expected) in [
1745 ("GLM-5.1", "GLM-5.1"),
1746 ("glm-5-1", "GLM-5.1"),
1747 ("GLM-5.2", "GLM-5.2"),
1748 ("glm-5.2", "GLM-5.2"),
1749 ("zai-glm-5-2", "GLM-5.2"),
1750 ("GLM-5-Turbo", "GLM-5-Turbo"),
1751 ("glm-5-turbo", "GLM-5-Turbo"),
1752 ("zai-glm-5-turbo", "GLM-5-Turbo"),
1753 ] {
1754 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
1755
1756 assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
1757 assert_eq!(resolved.resolved.id, expected);
1758 assert!(!resolved.used_fallback);
1759 assert!(resolved.resolved.supports_tools);
1760 assert!(resolved.resolved.supports_reasoning);
1761 }
1762 }
1763
1764 #[test]
1765 fn first_party_recent_provider_models_are_listed() {
1766 let registry = ModelRegistry::default();
1767 let models = registry.list();
1768
1769 for (provider, id) in [
1770 (ProviderKind::Zai, "GLM-5.2"),
1771 (ProviderKind::Stepfun, "step-3.7-flash"),
1772 (ProviderKind::Minimax, "MiniMax-M2.1"),
1773 (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
1774 (ProviderKind::Openmodel, "deepseek-v4-flash"),
1775 (ProviderKind::Meta, "muse-spark-1.1"),
1776 (ProviderKind::Xai, "grok-4.5"),
1777 ] {
1778 assert!(
1779 models
1780 .iter()
1781 .any(|model| model.provider == provider && model.id == id),
1782 "expected {provider:?} model {id} in registry"
1783 );
1784 }
1785 }
1786
1787 #[test]
1788 fn opencode_go_lists_only_current_chat_completions_models() {
1789 let registry = ModelRegistry::default();
1790 let listed = registry.list();
1791 let models: Vec<&str> = listed
1792 .iter()
1793 .filter(|model| model.provider == ProviderKind::OpencodeGo)
1794 .map(|model| model.id.as_str())
1795 .collect();
1796
1797 assert_eq!(
1798 models,
1799 vec![
1800 "deepseek-v4-pro",
1801 "grok-4.5",
1802 "glm-5.2",
1803 "glm-5.1",
1804 "kimi-k3",
1805 "kimi-k2.7-code",
1806 "kimi-k2.6",
1807 "deepseek-v4-flash",
1808 "mimo-v2.5",
1809 "mimo-v2.5-pro",
1810 ]
1811 );
1812
1813 let default = registry.resolve(None, Some(ProviderKind::OpencodeGo));
1814 assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
1815 assert_eq!(default.resolved.id, "deepseek-v4-pro");
1816
1817 for model in ["grok-4.5", "kimi-k3"] {
1818 for requested in [model.to_string(), format!("opencode-go/{model}")] {
1819 let resolved = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
1820 assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
1821 assert_eq!(resolved.resolved.id, model);
1822 assert!(!resolved.used_fallback);
1823 }
1824 }
1825
1826 for messages_only in [
1827 "minimax-m3",
1828 "minimax-m2.7",
1829 "minimax-m2.5",
1830 "qwen3.7-max",
1831 "qwen3.7-plus",
1832 "qwen3.6-plus",
1833 ] {
1834 for requested in [
1835 messages_only.to_string(),
1836 format!("opencode-go/{messages_only}"),
1837 ] {
1838 let rejected = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
1839 assert!(rejected.used_fallback, "{requested}");
1840 assert_eq!(
1841 rejected.resolved.provider,
1842 ProviderKind::OpencodeGo,
1843 "{requested} must not cross-route"
1844 );
1845 assert_eq!(rejected.resolved.id, "deepseek-v4-pro", "{requested}");
1846 }
1847 }
1848 }
1849
1850 #[test]
1851 fn xai_grok_models_resolve_when_provider_hinted() {
1852 let registry = ModelRegistry::default();
1853
1854 let default = registry.resolve(None, Some(ProviderKind::Xai));
1855 assert_eq!(default.resolved.provider, ProviderKind::Xai);
1856 assert_eq!(default.resolved.id, "grok-4.5");
1857 assert!(default.used_fallback);
1858
1859 let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
1860 assert_eq!(alias.resolved.provider, ProviderKind::Xai);
1861 assert_eq!(alias.resolved.id, "grok-4.5");
1862 assert!(!alias.used_fallback);
1863
1864 let fast = registry.resolve(
1865 Some("grok-4.20-0309-non-reasoning"),
1866 Some(ProviderKind::Xai),
1867 );
1868 assert_eq!(fast.resolved.provider, ProviderKind::Xai);
1869 assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
1870 assert!(!fast.resolved.supports_reasoning);
1871 }
1872
1873 #[test]
1874 fn meta_muse_spark_resolves_when_provider_hinted() {
1875 let registry = ModelRegistry::default();
1876
1877 let default = registry.resolve(None, Some(ProviderKind::Meta));
1878 assert_eq!(default.resolved.provider, ProviderKind::Meta);
1879 assert_eq!(default.resolved.id, "muse-spark-1.1");
1880 assert!(default.used_fallback);
1881
1882 let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
1883 assert_eq!(alias.resolved.provider, ProviderKind::Meta);
1884 assert_eq!(alias.resolved.id, "muse-spark-1.1");
1885 assert!(!alias.used_fallback);
1886 assert_eq!(model_family("muse-spark-1.1"), ModelFamily::Meta);
1887 }
1888
1889 #[test]
1890 fn openai_gpt56_family_resolves_when_provider_hinted() {
1891 let registry = ModelRegistry::default();
1892 for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
1893 let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
1894 assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
1895 assert_eq!(resolved.resolved.id, model, "{model}");
1896 assert!(resolved.resolved.supports_tools, "{model}");
1897 assert!(resolved.resolved.supports_reasoning, "{model}");
1898 assert!(!resolved.used_fallback, "{model}");
1899 }
1900 }
1901
1902 #[test]
1903 fn grok_ids_stay_in_grok_family() {
1904 assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
1905 assert_eq!(
1906 model_family("grok-4.20-0309-non-reasoning"),
1907 ModelFamily::Grok
1908 );
1909 }
1910
1911 #[test]
1912 fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
1913 let registry = ModelRegistry::default();
1914
1915 let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
1916 assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
1917 assert_eq!(stepfun.resolved.id, "step-3.7-flash");
1918
1919 for (alias, expected) in [
1920 ("minimax", "MiniMax-M3"),
1921 ("minimax-m3", "MiniMax-M3"),
1922 ("minimax-m2.7", "MiniMax-M2.7"),
1923 ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
1924 ("minimax-m2.1", "MiniMax-M2.1"),
1925 ("minimax-m2", "MiniMax-M2"),
1926 ] {
1927 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
1928
1929 assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
1930 assert_eq!(resolved.resolved.id, expected);
1931 assert!(!resolved.used_fallback);
1932 assert!(resolved.resolved.supports_tools);
1933 assert!(resolved.resolved.supports_reasoning);
1934 }
1935 }
1936
1937 #[test]
1938 fn minimax_anthropic_models_resolve_when_provider_hinted() {
1939 let registry = ModelRegistry::default();
1940
1941 for (alias, expected) in [
1942 ("minimax-anthropic", "MiniMax-M3"),
1943 ("minimax-m3", "MiniMax-M3"),
1944 ("minimax-m2.7", "MiniMax-M2.7"),
1945 ] {
1946 let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
1947
1948 assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
1949 assert_eq!(resolved.resolved.id, expected);
1950 assert!(!resolved.used_fallback);
1951 assert!(resolved.resolved.supports_tools);
1952 assert!(resolved.resolved.supports_reasoning);
1953 }
1954 }
1955
1956 #[test]
1957 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1958 let registry = ModelRegistry::default();
1959 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1960
1961 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1962 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1963 }
1964
1965 #[test]
1966 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1967 let registry = ModelRegistry::default();
1968
1969 for (alias, expected) in [
1970 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1971 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1972 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1973 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1974 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1975 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1976 ("glm-5.1", "z-ai/glm-5.1"),
1977 ("glm-5.2", "z-ai/glm-5.2"),
1978 ("minimax-m3", "minimax/minimax-m3"),
1979 ("minimax-2.7", "minimax/minimax-m2.7"),
1980 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1981 ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
1982 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1983 ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
1984 (
1985 "nvidia/nemotron-3-ultra",
1986 "nvidia/nemotron-3-ultra-550b-a55b",
1987 ),
1988 ] {
1989 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1990
1991 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1992 assert_eq!(resolved.resolved.id, expected);
1993 assert!(resolved.resolved.supports_tools);
1994 assert!(resolved.resolved.supports_reasoning);
1995 }
1996 }
1997
1998 #[test]
1999 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
2000 let registry = ModelRegistry::default();
2001 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
2002
2003 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
2004 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2005 }
2006
2007 #[test]
2008 fn together_inkling_keeps_published_wire_identity() {
2009 let registry = ModelRegistry::default();
2010 for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
2011 let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
2012
2013 assert_eq!(resolved.resolved.provider, ProviderKind::Together);
2014 assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
2015 assert!(resolved.resolved.supports_tools);
2016 assert!(resolved.resolved.supports_reasoning);
2017 assert!(!resolved.used_fallback);
2018 }
2019
2020 let unscoped = registry.resolve(Some("inkling"), None);
2021 assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
2022 assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
2023 assert!(!unscoped.used_fallback);
2024 }
2025
2026 #[test]
2027 fn registry_lists_and_resolves_every_v090_catalog_addition() {
2028 let registry = ModelRegistry::default();
2029 let advertised = [
2030 (ProviderKind::Anthropic, "claude-sonnet-5"),
2031 (ProviderKind::Anthropic, "claude-fable-5"),
2032 (ProviderKind::Openai, "gpt-5.3-codex"),
2033 (ProviderKind::Openai, "gpt-5.5"),
2034 (ProviderKind::Openai, "gpt-5.5-pro"),
2035 (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
2036 (ProviderKind::Arcee, "trinity-mini"),
2037 ];
2038
2039 let listed = registry.list();
2040 for (provider, model_id) in advertised {
2041 assert!(
2042 listed
2043 .iter()
2044 .any(|model| model.provider == provider && model.id == model_id),
2045 "missing {model_id} ({}) from model list",
2046 provider.as_str()
2047 );
2048 let resolved = registry.resolve(Some(model_id), Some(provider));
2049 assert_eq!(resolved.resolved.provider, provider, "{model_id}");
2050 assert_eq!(resolved.resolved.id, model_id, "{model_id}");
2051 assert!(!resolved.used_fallback, "{model_id}");
2052 }
2053 }
2054
2055 #[test]
2056 fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
2057 let registry = ModelRegistry::default();
2058
2059 let unscoped = registry.resolve(Some("gpt-5.5"), None);
2060 assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
2061 assert_eq!(unscoped.resolved.id, "gpt-5.5");
2062 assert!(!unscoped.used_fallback);
2063
2064 let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
2065 assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
2066 assert_eq!(codex.resolved.id, "gpt-5.5");
2067 assert!(!codex.used_fallback);
2068 }
2069
2070 #[test]
2071 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
2072 let registry = ModelRegistry::default();
2073 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
2074
2075 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2076 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2077 }
2078
2079 #[test]
2080 fn vllm_default_uses_canonical_model_id() {
2081 let registry = ModelRegistry::default();
2082 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
2083
2084 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2085 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2086 }
2087
2088 #[test]
2089 fn ollama_default_uses_small_local_model_id() {
2090 let registry = ModelRegistry::default();
2091 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
2092
2093 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2094 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2095 assert!(resolved.resolved.supports_reasoning);
2096 }
2097
2098 #[test]
2099 fn ollama_requested_model_tag_is_preserved() {
2100 let registry = ModelRegistry::default();
2101 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
2102
2103 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2104 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
2105 assert!(!resolved.used_fallback);
2106 }
2107
2108 #[test]
2109 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
2110 let registry = ModelRegistry::default();
2111 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
2112
2113 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2114 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2115 }
2116
2117 #[test]
2118 fn preserves_requested_model_casing_for_third_party_providers() {
2119 let registry = ModelRegistry::default();
2120 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
2121
2122 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2123 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2124 }
2125
2126 #[test]
2127 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
2128 let registry = ModelRegistry::default();
2129 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
2130
2131 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2132 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
2134 }
2135
2136 #[test]
2137 fn preserves_requested_model_casing_without_surrounding_whitespace() {
2138 let registry = ModelRegistry::default();
2139 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
2140
2141 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2142 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2143 }
2144
2145 #[test]
2146 fn alias_match_does_not_override_requested_casing() {
2147 let registry = ModelRegistry::default();
2148 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
2149
2150 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2151 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2152 }
2153
2154 #[test]
2155 fn model_family_classifies_known_model_ids() {
2156 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
2157 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
2158 assert_eq!(
2159 model_family("anthropic/claude-opus-4-7"),
2160 ModelFamily::Anthropic
2161 );
2162 assert_eq!(
2163 model_family("meta-llama/llama-3.3-70b-instruct"),
2164 ModelFamily::Meta
2165 );
2166 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
2167 }
2168
2169 #[test]
2170 fn model_family_uses_underlying_model_for_router_ids() {
2171 assert_eq!(
2172 model_family("groq/llama-3.3-70b-versatile"),
2173 ModelFamily::Meta
2174 );
2175 assert_eq!(
2176 model_family("openrouter/openai/gpt-5.4"),
2177 ModelFamily::OpenAI
2178 );
2179 assert_eq!(
2180 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
2181 ModelFamily::DeepSeek
2182 );
2183 }
2184
2185 #[test]
2186 fn model_family_covers_prominent_google_and_mistral_model_names() {
2187 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
2188 assert_eq!(
2189 model_family("mistralai/mixtral-8x22b"),
2190 ModelFamily::Mistral
2191 );
2192 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2193 }
2194
2195 #[test]
2196 fn model_family_falls_back_to_inferencer_for_unknown_models() {
2197 assert_eq!(
2198 model_family("custom-gateway/my-private-model"),
2199 ModelFamily::Inferencer
2200 );
2201 assert_eq!(model_family(""), ModelFamily::Inferencer);
2202 }
2203}