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 {
642 id: "kimi-k3".to_string(),
643 provider: ProviderKind::Moonshot,
644 aliases: vec!["moonshot-kimi-k3".to_string()],
645 supports_tools: true,
646 supports_reasoning: true,
647 },
648 ModelInfo {
649 id: "k3".to_string(),
650 provider: ProviderKind::Moonshot,
651 aliases: vec!["kimi-code-k3".to_string()],
652 supports_tools: true,
653 supports_reasoning: true,
654 },
655 ModelInfo {
656 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
657 provider: ProviderKind::Sglang,
658 aliases: vec![
659 "deepseek-v4-pro".to_string(),
660 "sglang-deepseek-v4-pro".to_string(),
661 ],
662 supports_tools: true,
663 supports_reasoning: true,
664 },
665 ModelInfo {
666 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
667 provider: ProviderKind::Sglang,
668 aliases: vec![
669 "deepseek-v4-flash".to_string(),
670 "deepseek-chat".to_string(),
671 "deepseek-reasoner".to_string(),
672 "sglang-deepseek-v4-flash".to_string(),
673 ],
674 supports_tools: true,
675 supports_reasoning: true,
676 },
677 ModelInfo {
678 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
679 provider: ProviderKind::Vllm,
680 aliases: vec![
681 "deepseek-v4-pro".to_string(),
682 "vllm-deepseek-v4-pro".to_string(),
683 ],
684 supports_tools: true,
685 supports_reasoning: true,
686 },
687 ModelInfo {
688 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
689 provider: ProviderKind::Vllm,
690 aliases: vec![
691 "deepseek-v4-flash".to_string(),
692 "deepseek-chat".to_string(),
693 "deepseek-reasoner".to_string(),
694 "vllm-deepseek-v4-flash".to_string(),
695 ],
696 supports_tools: true,
697 supports_reasoning: true,
698 },
699 ModelInfo {
700 id: "deepseek-v4-flash".to_string(),
701 provider: ProviderKind::Ollama,
702 aliases: vec![],
703 supports_tools: true,
704 supports_reasoning: true,
705 },
706 ModelInfo {
707 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
708 provider: ProviderKind::Huggingface,
709 aliases: vec![
710 "deepseek-v4-pro".to_string(),
711 "hf-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::Huggingface,
719 aliases: vec![
720 "deepseek-v4-flash".to_string(),
721 "deepseek-chat".to_string(),
722 "deepseek-reasoner".to_string(),
723 "hf-deepseek-v4-flash".to_string(),
724 ],
725 supports_tools: true,
726 supports_reasoning: true,
727 },
728 ModelInfo {
730 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
731 provider: ProviderKind::Together,
732 aliases: vec![
733 "deepseek-v4-pro".to_string(),
734 "together-deepseek-v4-pro".to_string(),
735 ],
736 supports_tools: true,
737 supports_reasoning: true,
738 },
739 ModelInfo {
740 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
741 provider: ProviderKind::Together,
742 aliases: vec![
743 "deepseek-v4-flash".to_string(),
744 "deepseek-chat".to_string(),
745 "together-deepseek-v4-flash".to_string(),
746 ],
747 supports_tools: true,
748 supports_reasoning: true,
749 },
750 ModelInfo {
751 id: "thinkingmachines/inkling".to_string(),
754 provider: ProviderKind::Together,
755 aliases: vec!["inkling".to_string(), "together-inkling".to_string()],
756 supports_tools: true,
757 supports_reasoning: true,
758 },
759 ModelInfo {
761 id: "qwen/qwen3.7-max".to_string(),
762 provider: ProviderKind::Openrouter,
763 aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
764 supports_tools: true,
765 supports_reasoning: true,
766 },
767 ModelInfo {
769 id: "gpt-5.5".to_string(),
770 provider: ProviderKind::OpenaiCodex,
771 aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
772 supports_tools: true,
773 supports_reasoning: true,
774 },
775 ModelInfo {
777 id: "claude-opus-4-8".to_string(),
778 provider: ProviderKind::Anthropic,
779 aliases: vec!["opus".to_string(), "claude-opus".to_string()],
780 supports_tools: true,
781 supports_reasoning: true,
782 },
783 ModelInfo {
784 id: "claude-sonnet-4-6".to_string(),
785 provider: ProviderKind::Anthropic,
786 aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
787 supports_tools: true,
788 supports_reasoning: true,
789 },
790 ModelInfo {
791 id: "claude-haiku-4-5".to_string(),
792 provider: ProviderKind::Anthropic,
793 aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
794 supports_tools: true,
795 supports_reasoning: false,
796 },
797 ModelInfo {
798 id: "claude-sonnet-5".to_string(),
799 provider: ProviderKind::Anthropic,
800 aliases: vec!["sonnet-5".to_string()],
801 supports_tools: true,
802 supports_reasoning: true,
803 },
804 ModelInfo {
805 id: "claude-fable-5".to_string(),
806 provider: ProviderKind::Anthropic,
807 aliases: vec!["fable".to_string(), "fable-5".to_string()],
808 supports_tools: true,
809 supports_reasoning: true,
810 },
811 ModelInfo {
813 id: "deepseek-v4-flash".to_string(),
814 provider: ProviderKind::Openmodel,
815 aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
816 supports_tools: true,
817 supports_reasoning: true,
818 },
819 ModelInfo {
821 id: "minimax/minimax-m2.7".to_string(),
822 provider: ProviderKind::Openrouter,
823 aliases: vec![
824 "minimax-2.7".to_string(),
825 "minimax-2-7".to_string(),
826 "openrouter-minimax-2.7".to_string(),
827 ],
828 supports_tools: true,
829 supports_reasoning: true,
830 },
831 ModelInfo {
832 id: "step-3.7-flash".to_string(),
833 provider: ProviderKind::Stepfun,
834 aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
835 supports_tools: true,
836 supports_reasoning: false,
837 },
838 ModelInfo {
839 id: "MiniMax-M3".to_string(),
840 provider: ProviderKind::Minimax,
841 aliases: vec![
842 "minimax".to_string(),
843 "minimax-m3".to_string(),
844 "minimax-m-3".to_string(),
845 ],
846 supports_tools: true,
847 supports_reasoning: true,
848 },
849 ModelInfo {
850 id: "MiniMax-M2.7".to_string(),
851 provider: ProviderKind::Minimax,
852 aliases: vec![
853 "minimax-m2.7".to_string(),
854 "minimax-m2-7".to_string(),
855 "minimax-m-2.7".to_string(),
856 "minimax-m-2-7".to_string(),
857 ],
858 supports_tools: true,
859 supports_reasoning: true,
860 },
861 ModelInfo {
862 id: "MiniMax-M3".to_string(),
863 provider: ProviderKind::MinimaxAnthropic,
864 aliases: vec![
865 "minimax-anthropic".to_string(),
866 "minimax-anthropic-m3".to_string(),
867 "minimax-m3".to_string(),
868 ],
869 supports_tools: true,
870 supports_reasoning: true,
871 },
872 ModelInfo {
873 id: "MiniMax-M2.7".to_string(),
874 provider: ProviderKind::MinimaxAnthropic,
875 aliases: vec![
876 "minimax-anthropic-m2.7".to_string(),
877 "minimax-anthropic-m2-7".to_string(),
878 "minimax-m2.7".to_string(),
879 ],
880 supports_tools: true,
881 supports_reasoning: true,
882 },
883 ModelInfo {
884 id: "MiniMax-M2.7-highspeed".to_string(),
885 provider: ProviderKind::Minimax,
886 aliases: vec![
887 "minimax-m2.7-highspeed".to_string(),
888 "minimax-m2-7-highspeed".to_string(),
889 "minimax-m-2.7-highspeed".to_string(),
890 "minimax-m-2-7-highspeed".to_string(),
891 ],
892 supports_tools: true,
893 supports_reasoning: true,
894 },
895 ModelInfo {
896 id: "MiniMax-M2.5".to_string(),
897 provider: ProviderKind::Minimax,
898 aliases: vec![
899 "minimax-m2.5".to_string(),
900 "minimax-m2-5".to_string(),
901 "minimax-m-2.5".to_string(),
902 "minimax-m-2-5".to_string(),
903 ],
904 supports_tools: true,
905 supports_reasoning: true,
906 },
907 ModelInfo {
908 id: "MiniMax-M2.5-highspeed".to_string(),
909 provider: ProviderKind::Minimax,
910 aliases: vec![
911 "minimax-m2.5-highspeed".to_string(),
912 "minimax-m2-5-highspeed".to_string(),
913 "minimax-m-2.5-highspeed".to_string(),
914 "minimax-m-2-5-highspeed".to_string(),
915 ],
916 supports_tools: true,
917 supports_reasoning: true,
918 },
919 ModelInfo {
920 id: "MiniMax-M2.1".to_string(),
921 provider: ProviderKind::Minimax,
922 aliases: vec![
923 "minimax-m2.1".to_string(),
924 "minimax-m2-1".to_string(),
925 "minimax-m-2.1".to_string(),
926 "minimax-m-2-1".to_string(),
927 ],
928 supports_tools: true,
929 supports_reasoning: true,
930 },
931 ModelInfo {
932 id: "MiniMax-M2.1-highspeed".to_string(),
933 provider: ProviderKind::Minimax,
934 aliases: vec![
935 "minimax-m2.1-highspeed".to_string(),
936 "minimax-m2-1-highspeed".to_string(),
937 "minimax-m-2.1-highspeed".to_string(),
938 "minimax-m-2-1-highspeed".to_string(),
939 ],
940 supports_tools: true,
941 supports_reasoning: true,
942 },
943 ModelInfo {
944 id: "MiniMax-M2".to_string(),
945 provider: ProviderKind::Minimax,
946 aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
947 supports_tools: true,
948 supports_reasoning: true,
949 },
950 ModelInfo {
952 id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
953 provider: ProviderKind::Openrouter,
954 aliases: vec![
955 "nvidia/nemotron-3-ultra".to_string(),
956 "nemotron-3-ultra".to_string(),
957 "nemotron-3-ultra-550b-a55b".to_string(),
958 "nvidia-nemotron-3-ultra".to_string(),
959 "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
960 ],
961 supports_tools: true,
962 supports_reasoning: true,
963 },
964 ModelInfo {
966 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
967 provider: ProviderKind::Deepinfra,
968 aliases: vec![
969 "deepseek-v4-pro".to_string(),
970 "di-deepseek-v4-pro".to_string(),
971 ],
972 supports_tools: true,
973 supports_reasoning: true,
974 },
975 ModelInfo {
976 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
977 provider: ProviderKind::Deepinfra,
978 aliases: vec![
979 "deepseek-v4-flash".to_string(),
980 "di-deepseek-v4-flash".to_string(),
981 ],
982 supports_tools: true,
983 supports_reasoning: true,
984 },
985 ModelInfo {
987 id: "fugu".to_string(),
988 provider: ProviderKind::Sakana,
989 aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
990 supports_tools: true,
991 supports_reasoning: false,
992 },
993 ModelInfo {
994 id: "fugu-ultra-20260615".to_string(),
995 provider: ProviderKind::Sakana,
996 aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
997 supports_tools: true,
998 supports_reasoning: true,
999 },
1000 ModelInfo {
1002 id: "LongCat-2.0".to_string(),
1003 provider: ProviderKind::LongCat,
1004 aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
1005 supports_tools: true,
1006 supports_reasoning: true,
1007 },
1008 ModelInfo {
1012 id: "deepseek-v4-pro".to_string(),
1013 provider: ProviderKind::OpencodeGo,
1014 aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
1015 supports_tools: true,
1016 supports_reasoning: true,
1017 },
1018 ModelInfo {
1019 id: "grok-4.5".to_string(),
1020 provider: ProviderKind::OpencodeGo,
1021 aliases: vec!["opencode-go/grok-4.5".to_string()],
1022 supports_tools: true,
1023 supports_reasoning: true,
1024 },
1025 ModelInfo {
1026 id: "glm-5.2".to_string(),
1027 provider: ProviderKind::OpencodeGo,
1028 aliases: vec!["opencode-go/glm-5.2".to_string()],
1029 supports_tools: true,
1030 supports_reasoning: true,
1031 },
1032 ModelInfo {
1033 id: "glm-5.1".to_string(),
1034 provider: ProviderKind::OpencodeGo,
1035 aliases: vec!["opencode-go/glm-5.1".to_string()],
1036 supports_tools: true,
1037 supports_reasoning: true,
1038 },
1039 ModelInfo {
1040 id: "kimi-k3".to_string(),
1041 provider: ProviderKind::OpencodeGo,
1042 aliases: vec!["opencode-go/kimi-k3".to_string()],
1043 supports_tools: true,
1044 supports_reasoning: true,
1045 },
1046 ModelInfo {
1047 id: "kimi-k2.7-code".to_string(),
1048 provider: ProviderKind::OpencodeGo,
1049 aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
1050 supports_tools: true,
1051 supports_reasoning: true,
1052 },
1053 ModelInfo {
1054 id: "kimi-k2.6".to_string(),
1055 provider: ProviderKind::OpencodeGo,
1056 aliases: vec!["opencode-go/kimi-k2.6".to_string()],
1057 supports_tools: true,
1058 supports_reasoning: true,
1059 },
1060 ModelInfo {
1061 id: "deepseek-v4-flash".to_string(),
1062 provider: ProviderKind::OpencodeGo,
1063 aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
1064 supports_tools: true,
1065 supports_reasoning: true,
1066 },
1067 ModelInfo {
1068 id: "mimo-v2.5".to_string(),
1069 provider: ProviderKind::OpencodeGo,
1070 aliases: vec!["opencode-go/mimo-v2.5".to_string()],
1071 supports_tools: true,
1072 supports_reasoning: true,
1073 },
1074 ModelInfo {
1075 id: "mimo-v2.5-pro".to_string(),
1076 provider: ProviderKind::OpencodeGo,
1077 aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
1078 supports_tools: true,
1079 supports_reasoning: true,
1080 },
1081 ModelInfo {
1083 id: "muse-spark-1.1".to_string(),
1084 provider: ProviderKind::Meta,
1085 aliases: vec!["muse-spark".to_string(), "muse".to_string()],
1086 supports_tools: true,
1087 supports_reasoning: true,
1088 },
1089 ModelInfo {
1091 id: "grok-4.5".to_string(),
1092 provider: ProviderKind::Xai,
1093 aliases: vec!["grok".to_string(), "xai-grok-4.5".to_string()],
1094 supports_tools: true,
1095 supports_reasoning: true,
1096 },
1097 ModelInfo {
1098 id: "grok-4.3".to_string(),
1099 provider: ProviderKind::Xai,
1100 aliases: vec!["xai-grok-4.3".to_string()],
1101 supports_tools: true,
1102 supports_reasoning: true,
1103 },
1104 ModelInfo {
1105 id: "grok-build".to_string(),
1106 provider: ProviderKind::Xai,
1107 aliases: vec!["xai-grok-build".to_string()],
1108 supports_tools: true,
1109 supports_reasoning: true,
1110 },
1111 ModelInfo {
1112 id: "grok-composer-2.5-fast".to_string(),
1113 provider: ProviderKind::Xai,
1114 aliases: vec!["xai-grok-composer".to_string()],
1115 supports_tools: true,
1116 supports_reasoning: false,
1117 },
1118 ModelInfo {
1119 id: "grok-4.20-0309-reasoning".to_string(),
1120 provider: ProviderKind::Xai,
1121 aliases: vec!["xai-grok-reasoning".to_string()],
1122 supports_tools: true,
1123 supports_reasoning: true,
1124 },
1125 ModelInfo {
1126 id: "grok-4.20-0309-non-reasoning".to_string(),
1127 provider: ProviderKind::Xai,
1128 aliases: vec!["xai-grok-fast".to_string()],
1129 supports_tools: true,
1130 supports_reasoning: false,
1131 },
1132 ];
1133 Self::new(models)
1134 }
1135}
1136
1137impl ModelRegistry {
1138 #[must_use]
1144 pub fn new(models: Vec<ModelInfo>) -> Self {
1145 let mut alias_map = HashMap::new();
1146 for (idx, model) in models.iter().enumerate() {
1147 alias_map.entry(normalize(&model.id)).or_insert(idx);
1148 for alias in &model.aliases {
1149 alias_map.entry(normalize(alias)).or_insert(idx);
1150 }
1151 }
1152 Self { models, alias_map }
1153 }
1154
1155 #[must_use]
1157 pub fn list(&self) -> Vec<ModelInfo> {
1158 self.models.clone()
1159 }
1160
1161 #[must_use]
1173 pub fn resolve(
1174 &self,
1175 requested: Option<&str>,
1176 provider_hint: Option<ProviderKind>,
1177 ) -> ModelResolution {
1178 let mut fallback_chain = Vec::new();
1179
1180 if let Some(name) = requested {
1181 fallback_chain.push(format!("requested:{name}"));
1182 if provider_hint == Some(ProviderKind::Ollama) {
1183 return ModelResolution {
1184 requested: Some(name.to_string()),
1185 resolved: ModelInfo {
1186 id: name.trim().to_string(),
1187 provider: ProviderKind::Ollama,
1188 aliases: Vec::new(),
1189 supports_tools: true,
1190 supports_reasoning: false,
1191 },
1192 used_fallback: false,
1193 fallback_chain,
1194 };
1195 }
1196 if provider_hint == Some(ProviderKind::OpencodeGo)
1202 && let Some(canonical) = opencode_go_chat_model_id(name)
1203 && let Some(model) = self
1204 .models
1205 .iter()
1206 .find(|model| {
1207 model.provider == ProviderKind::OpencodeGo
1208 && model.id.eq_ignore_ascii_case(canonical)
1209 })
1210 .cloned()
1211 {
1212 return ModelResolution {
1213 requested: Some(name.to_string()),
1214 resolved: model,
1215 used_fallback: false,
1216 fallback_chain,
1217 };
1218 }
1219 if provider_hint != Some(ProviderKind::OpencodeGo)
1220 && let Some(provider) = provider_hint
1221 && let Some(model) = self
1222 .models
1223 .iter()
1224 .find(|m| m.provider == provider && model_matches(m, name))
1225 .cloned()
1226 {
1227 return ModelResolution {
1228 requested: Some(name.to_string()),
1229 resolved: model,
1230 used_fallback: false,
1231 fallback_chain,
1232 };
1233 }
1234 if provider_hint == Some(ProviderKind::Atlascloud)
1235 && let Some(model) = atlascloud_passthrough_model(name)
1236 {
1237 return ModelResolution {
1238 requested: Some(name.to_string()),
1239 resolved: model,
1240 used_fallback: false,
1241 fallback_chain,
1242 };
1243 }
1244 if provider_hint == Some(ProviderKind::Arcee)
1245 && let Some(model) = arcee_passthrough_model(name)
1246 {
1247 return ModelResolution {
1248 requested: Some(name.to_string()),
1249 resolved: model,
1250 used_fallback: false,
1251 fallback_chain,
1252 };
1253 }
1254 if provider_hint == Some(ProviderKind::XiaomiMimo)
1255 && let Some(model) = xiaomi_mimo_passthrough_model(name)
1256 {
1257 return ModelResolution {
1258 requested: Some(name.to_string()),
1259 resolved: model,
1260 used_fallback: false,
1261 fallback_chain,
1262 };
1263 }
1264 if provider_hint != Some(ProviderKind::OpencodeGo)
1272 && let Some(idx) = self.alias_map.get(&normalize(name))
1273 && provider_hint.is_none_or(|hint| self.models[*idx].provider == hint)
1274 {
1275 return ModelResolution {
1276 requested: Some(name.to_string()),
1277 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1278 used_fallback: false,
1279 fallback_chain,
1280 };
1281 }
1282 }
1283
1284 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1285 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1286 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1287 return ModelResolution {
1288 requested: requested.map(ToOwned::to_owned),
1289 resolved: model,
1290 used_fallback: true,
1291 fallback_chain,
1292 };
1293 }
1294
1295 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1296 id: "deepseek-v4-pro".to_string(),
1297 provider: ProviderKind::Deepseek,
1298 aliases: Vec::new(),
1299 supports_tools: true,
1300 supports_reasoning: true,
1301 });
1302 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1303 ModelResolution {
1304 requested: requested.map(ToOwned::to_owned),
1305 resolved: final_fallback,
1306 used_fallback: true,
1307 fallback_chain,
1308 }
1309 }
1310}
1311
1312fn normalize(value: &str) -> String {
1313 value.trim().to_ascii_lowercase()
1314}
1315
1316#[must_use]
1317pub fn model_family(model_id: &str) -> ModelFamily {
1319 let normalized = normalize(model_id);
1320 if normalized.is_empty() {
1321 return ModelFamily::Inferencer;
1322 }
1323
1324 if normalized.contains("deepseek") {
1325 return ModelFamily::DeepSeek;
1326 }
1327 if normalized.contains("claude") || normalized.contains("anthropic") {
1328 return ModelFamily::Anthropic;
1329 }
1330 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1331 return ModelFamily::GptOss;
1332 }
1333 if normalized.starts_with("gpt-")
1334 || normalized.contains("/gpt-")
1335 || normalized.contains("openai/")
1336 {
1337 return ModelFamily::OpenAI;
1338 }
1339 if normalized.contains("gemini")
1340 || normalized.contains("gemma")
1341 || normalized.contains("google/")
1342 {
1343 return ModelFamily::Google;
1344 }
1345 if normalized.contains("llama")
1346 || normalized.contains("muse-spark")
1347 || normalized.contains("meta-")
1348 || normalized.contains("meta/")
1349 {
1350 return ModelFamily::Meta;
1351 }
1352 if normalized.contains("mistral")
1353 || normalized.contains("mixtral")
1354 || normalized.contains("codestral")
1355 {
1356 return ModelFamily::Mistral;
1357 }
1358 if normalized.contains("qwen") {
1359 return ModelFamily::Qwen;
1360 }
1361 if normalized.contains("grok") {
1362 return ModelFamily::Grok;
1363 }
1364 if normalized.contains("cohere") || normalized.contains("command-r") {
1365 return ModelFamily::Cohere;
1366 }
1367
1368 ModelFamily::Inferencer
1369}
1370
1371fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1372 let requested = normalize(requested);
1373 normalize(&model.id) == requested
1374 || model
1375 .aliases
1376 .iter()
1377 .any(|alias| normalize(alias) == requested)
1378}
1379
1380fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1381 let requested = requested.trim();
1382 if model.id.eq_ignore_ascii_case(requested) {
1383 model.id = requested.to_string();
1384 }
1385 model
1386}
1387
1388fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1389 let requested = requested.trim();
1390 if requested.is_empty() || !requested.contains('/') {
1391 return None;
1392 }
1393
1394 Some(ModelInfo {
1395 id: requested.to_string(),
1396 provider: ProviderKind::Atlascloud,
1397 aliases: Vec::new(),
1398 supports_tools: true,
1399 supports_reasoning: true,
1400 })
1401}
1402
1403fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1404 let requested = requested.trim();
1405 if requested.is_empty() {
1406 return None;
1407 }
1408 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1409
1410 Some(ModelInfo {
1411 id: requested.to_string(),
1412 provider: ProviderKind::Arcee,
1413 aliases: Vec::new(),
1414 supports_tools: true,
1415 supports_reasoning,
1416 })
1417}
1418
1419fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1420 let requested = requested.trim();
1421 if requested.is_empty() || requested.chars().any(char::is_control) {
1422 return None;
1423 }
1424
1425 Some(ModelInfo {
1426 id: requested.to_string(),
1427 provider: ProviderKind::XiaomiMimo,
1428 aliases: Vec::new(),
1429 supports_tools: true,
1430 supports_reasoning: true,
1431 })
1432}
1433
1434#[cfg(test)]
1435mod tests {
1436 use super::*;
1437
1438 #[test]
1439 fn model_registry_new_builds_alias_map_correctly() {
1440 let models = vec![
1441 ModelInfo {
1442 id: "Model-A".to_string(),
1443 provider: ProviderKind::Deepseek,
1444 aliases: vec!["alias-1".to_string(), " ALIAS-2 ".to_string()],
1445 supports_tools: true,
1446 supports_reasoning: false,
1447 },
1448 ModelInfo {
1449 id: "model-b".to_string(),
1450 provider: ProviderKind::Deepseek,
1451 aliases: vec!["alias-1".to_string()], supports_tools: true,
1453 supports_reasoning: true,
1454 },
1455 ];
1456
1457 let registry = ModelRegistry::new(models);
1458
1459 assert_eq!(registry.alias_map.len(), 4); assert_eq!(registry.alias_map.get("model-a"), Some(&0));
1461 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));
1464 }
1465
1466 #[test]
1467 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1468 let registry = ModelRegistry::default();
1469 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1470
1471 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1472 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1473 }
1474
1475 #[test]
1476 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1477 let registry = ModelRegistry::default();
1478 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1479
1480 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1481 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1482 }
1483
1484 #[test]
1485 fn nvidia_nim_default_uses_catalog_model_id() {
1486 let registry = ModelRegistry::default();
1487 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1488
1489 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1490 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1491 }
1492
1493 #[test]
1494 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1495 let registry = ModelRegistry::default();
1496 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1497
1498 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1499 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1500 }
1501
1502 #[test]
1503 fn atlascloud_default_uses_namespaced_model_id() {
1504 let registry = ModelRegistry::default();
1505 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1506
1507 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1508 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1509 assert!(resolved.resolved.supports_reasoning);
1510 }
1511
1512 #[test]
1513 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1514 let registry = ModelRegistry::default();
1515 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1516
1517 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1518 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1519 }
1520
1521 #[test]
1522 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1523 let registry = ModelRegistry::default();
1524 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1525
1526 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1527 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1528 }
1529
1530 #[test]
1531 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1532 let registry = ModelRegistry::default();
1533 let resolved =
1534 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1535
1536 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1537 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1538 assert!(resolved.resolved.supports_tools);
1539 assert!(resolved.resolved.supports_reasoning);
1540 assert!(!resolved.used_fallback);
1541 }
1542
1543 #[test]
1544 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1545 let registry = ModelRegistry::default();
1546 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1547
1548 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1549 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1550 assert!(!resolved.used_fallback);
1551 }
1552
1553 #[test]
1554 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1555 let registry = ModelRegistry::default();
1556 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1557
1558 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1559 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1560 assert!(resolved.used_fallback);
1561 }
1562
1563 #[test]
1564 fn openrouter_default_uses_namespaced_model_id() {
1565 let registry = ModelRegistry::default();
1566 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1567
1568 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1569 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1570 }
1571
1572 #[test]
1573 fn xiaomi_mimo_default_uses_canonical_model_id() {
1574 let registry = ModelRegistry::default();
1575 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1576
1577 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1578 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1579 assert!(resolved.resolved.supports_reasoning);
1580 }
1581
1582 #[test]
1583 fn moonshot_default_and_aliases_use_kimi_k27_code() {
1584 let registry = ModelRegistry::default();
1585
1586 for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1587 let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1588
1589 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1590 assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1591 assert!(resolved.resolved.supports_tools);
1592 assert!(resolved.resolved.supports_reasoning);
1593 }
1594 }
1595
1596 #[test]
1597 fn moonshot_explicit_kimi_k26_remains_available() {
1598 let registry = ModelRegistry::default();
1599 let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1600
1601 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1602 assert_eq!(resolved.resolved.id, "kimi-k2.6");
1603 assert!(resolved.resolved.supports_reasoning);
1604 }
1605
1606 #[test]
1610 fn moonshot_resolves_both_k3_products_without_crossing_them() {
1611 let registry = ModelRegistry::default();
1612
1613 for (requested, expected) in [("kimi-k3", "kimi-k3"), ("k3", "k3")] {
1614 let resolved = registry.resolve(Some(requested), Some(ProviderKind::Moonshot));
1615
1616 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1617 assert_eq!(resolved.resolved.id, expected, "{resolved:?}");
1618 assert!(
1619 !resolved.used_fallback,
1620 "an explicit Moonshot K3 request is not a fallback: {resolved:?}"
1621 );
1622 }
1623 }
1624
1625 #[test]
1629 fn moonshot_k3_ids_are_never_rewritten_into_each_other() {
1630 let registry = ModelRegistry::default();
1631
1632 assert_eq!(
1633 registry
1634 .resolve(Some("kimi-k3"), Some(ProviderKind::Moonshot))
1635 .resolved
1636 .id,
1637 "kimi-k3"
1638 );
1639 assert_eq!(
1640 registry
1641 .resolve(Some("k3"), Some(ProviderKind::Moonshot))
1642 .resolved
1643 .id,
1644 "k3"
1645 );
1646 }
1647
1648 #[test]
1652 fn a_provider_hint_never_resolves_to_another_providers_model() {
1653 let registry = ModelRegistry::default();
1654
1655 let resolved = registry.resolve(Some("glm-5.2"), Some(ProviderKind::Moonshot));
1656 assert_eq!(
1657 resolved.resolved.provider,
1658 ProviderKind::Moonshot,
1659 "a Moonshot request must not be answered by Z.ai: {resolved:?}"
1660 );
1661 assert!(
1662 resolved.used_fallback,
1663 "an unservable id must be reported as a fallback, not as the request: {resolved:?}"
1664 );
1665
1666 let go = registry.resolve(Some("kimi-k3"), Some(ProviderKind::OpencodeGo));
1667 assert_eq!(go.resolved.provider, ProviderKind::OpencodeGo);
1668 assert_eq!(go.resolved.id, "kimi-k3");
1669 }
1670
1671 #[test]
1672 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1673 let registry = ModelRegistry::default();
1674 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1675 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1676 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1677 assert!(!resolved.resolved.supports_tools);
1678 assert!(!resolved.resolved.supports_reasoning);
1679
1680 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1681 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1682
1683 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1684 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1685 }
1686
1687 #[test]
1688 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1689 let registry = ModelRegistry::default();
1690
1691 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1692 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1693 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1694 assert!(resolved.resolved.supports_tools);
1695 }
1696
1697 #[test]
1698 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1699 let registry = ModelRegistry::default();
1700 let resolved =
1701 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1702
1703 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1704 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1705 assert!(!resolved.used_fallback);
1706 }
1707
1708 #[test]
1709 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1710 let registry = ModelRegistry::default();
1711 let resolved = registry.resolve(
1712 Some("deepseek/deepseek-v4-pro"),
1713 Some(ProviderKind::XiaomiMimo),
1714 );
1715
1716 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1717 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1718 assert!(!resolved.used_fallback);
1719 }
1720
1721 #[test]
1722 fn wanjie_ark_default_uses_reasoner_model_id() {
1723 let registry = ModelRegistry::default();
1724 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1725
1726 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1727 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1728 assert!(resolved.resolved.supports_reasoning);
1729 }
1730
1731 #[test]
1732 fn novita_default_uses_namespaced_model_id() {
1733 let registry = ModelRegistry::default();
1734 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1735
1736 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1737 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1738 }
1739
1740 #[test]
1741 fn fireworks_default_uses_canonical_model_id() {
1742 let registry = ModelRegistry::default();
1743 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1744
1745 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1746 assert_eq!(
1747 resolved.resolved.id,
1748 "accounts/fireworks/models/deepseek-v4-pro"
1749 );
1750 }
1751
1752 #[test]
1753 fn siliconflow_default_uses_canonical_pro_model_id() {
1754 let registry = ModelRegistry::default();
1755 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1756
1757 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1758 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1759 assert!(resolved.resolved.supports_reasoning);
1760 }
1761
1762 #[test]
1763 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1764 let registry = ModelRegistry::default();
1765 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1766
1767 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1768 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1769 assert!(resolved.resolved.supports_reasoning);
1770 }
1771
1772 #[test]
1773 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1774 let registry = ModelRegistry::default();
1775 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1776
1777 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1778 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1779 assert!(resolved.resolved.supports_reasoning);
1780 }
1781
1782 #[test]
1783 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1784 let registry = ModelRegistry::default();
1785 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1786
1787 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1788 assert_eq!(resolved.resolved.id, "trinity-mini");
1789 assert!(resolved.resolved.supports_reasoning);
1790 assert!(!resolved.used_fallback);
1791 }
1792
1793 #[test]
1794 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1795 let registry = ModelRegistry::default();
1796 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1797
1798 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1799 assert_eq!(resolved.resolved.id, "trinity-large-next");
1800 assert!(!resolved.resolved.supports_reasoning);
1801 assert!(!resolved.used_fallback);
1802 }
1803
1804 #[test]
1805 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1806 let registry = ModelRegistry::default();
1807 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1808
1809 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1810 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1811 }
1812
1813 #[test]
1814 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1815 let registry = ModelRegistry::default();
1816 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1817
1818 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1819 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1820 }
1821
1822 #[test]
1823 fn sglang_default_uses_canonical_model_id() {
1824 let registry = ModelRegistry::default();
1825 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1826
1827 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1828 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1829 }
1830
1831 #[test]
1832 fn zai_direct_models_resolve_when_provider_hinted() {
1833 let registry = ModelRegistry::default();
1834
1835 let default = registry.resolve(None, Some(ProviderKind::Zai));
1837 assert_eq!(default.resolved.provider, ProviderKind::Zai);
1838 assert_eq!(default.resolved.id, "GLM-5.2");
1839
1840 for (alias, expected) in [
1841 ("GLM-5.1", "GLM-5.1"),
1842 ("glm-5-1", "GLM-5.1"),
1843 ("GLM-5.2", "GLM-5.2"),
1844 ("glm-5.2", "GLM-5.2"),
1845 ("zai-glm-5-2", "GLM-5.2"),
1846 ("GLM-5-Turbo", "GLM-5-Turbo"),
1847 ("glm-5-turbo", "GLM-5-Turbo"),
1848 ("zai-glm-5-turbo", "GLM-5-Turbo"),
1849 ] {
1850 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
1851
1852 assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
1853 assert_eq!(resolved.resolved.id, expected);
1854 assert!(!resolved.used_fallback);
1855 assert!(resolved.resolved.supports_tools);
1856 assert!(resolved.resolved.supports_reasoning);
1857 }
1858 }
1859
1860 #[test]
1861 fn first_party_recent_provider_models_are_listed() {
1862 let registry = ModelRegistry::default();
1863 let models = registry.list();
1864
1865 for (provider, id) in [
1866 (ProviderKind::Zai, "GLM-5.2"),
1867 (ProviderKind::Stepfun, "step-3.7-flash"),
1868 (ProviderKind::Minimax, "MiniMax-M2.1"),
1869 (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
1870 (ProviderKind::Openmodel, "deepseek-v4-flash"),
1871 (ProviderKind::Meta, "muse-spark-1.1"),
1872 (ProviderKind::Xai, "grok-4.5"),
1873 ] {
1874 assert!(
1875 models
1876 .iter()
1877 .any(|model| model.provider == provider && model.id == id),
1878 "expected {provider:?} model {id} in registry"
1879 );
1880 }
1881 }
1882
1883 #[test]
1884 fn opencode_go_lists_only_current_chat_completions_models() {
1885 let registry = ModelRegistry::default();
1886 let listed = registry.list();
1887 let models: Vec<&str> = listed
1888 .iter()
1889 .filter(|model| model.provider == ProviderKind::OpencodeGo)
1890 .map(|model| model.id.as_str())
1891 .collect();
1892
1893 assert_eq!(
1894 models,
1895 vec![
1896 "deepseek-v4-pro",
1897 "grok-4.5",
1898 "glm-5.2",
1899 "glm-5.1",
1900 "kimi-k3",
1901 "kimi-k2.7-code",
1902 "kimi-k2.6",
1903 "deepseek-v4-flash",
1904 "mimo-v2.5",
1905 "mimo-v2.5-pro",
1906 ]
1907 );
1908
1909 let default = registry.resolve(None, Some(ProviderKind::OpencodeGo));
1910 assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
1911 assert_eq!(default.resolved.id, "deepseek-v4-pro");
1912
1913 for model in ["grok-4.5", "kimi-k3"] {
1914 for requested in [model.to_string(), format!("opencode-go/{model}")] {
1915 let resolved = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
1916 assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
1917 assert_eq!(resolved.resolved.id, model);
1918 assert!(!resolved.used_fallback);
1919 }
1920 }
1921
1922 for messages_only in [
1923 "minimax-m3",
1924 "minimax-m2.7",
1925 "minimax-m2.5",
1926 "qwen3.7-max",
1927 "qwen3.7-plus",
1928 "qwen3.6-plus",
1929 ] {
1930 for requested in [
1931 messages_only.to_string(),
1932 format!("opencode-go/{messages_only}"),
1933 ] {
1934 let rejected = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
1935 assert!(rejected.used_fallback, "{requested}");
1936 assert_eq!(
1937 rejected.resolved.provider,
1938 ProviderKind::OpencodeGo,
1939 "{requested} must not cross-route"
1940 );
1941 assert_eq!(rejected.resolved.id, "deepseek-v4-pro", "{requested}");
1942 }
1943 }
1944 }
1945
1946 #[test]
1947 fn xai_grok_models_resolve_when_provider_hinted() {
1948 let registry = ModelRegistry::default();
1949
1950 let default = registry.resolve(None, Some(ProviderKind::Xai));
1951 assert_eq!(default.resolved.provider, ProviderKind::Xai);
1952 assert_eq!(default.resolved.id, "grok-4.5");
1953 assert!(default.used_fallback);
1954
1955 let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
1956 assert_eq!(alias.resolved.provider, ProviderKind::Xai);
1957 assert_eq!(alias.resolved.id, "grok-4.5");
1958 assert!(!alias.used_fallback);
1959
1960 let fast = registry.resolve(
1961 Some("grok-4.20-0309-non-reasoning"),
1962 Some(ProviderKind::Xai),
1963 );
1964 assert_eq!(fast.resolved.provider, ProviderKind::Xai);
1965 assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
1966 assert!(!fast.resolved.supports_reasoning);
1967 }
1968
1969 #[test]
1970 fn meta_muse_spark_resolves_when_provider_hinted() {
1971 let registry = ModelRegistry::default();
1972
1973 let default = registry.resolve(None, Some(ProviderKind::Meta));
1974 assert_eq!(default.resolved.provider, ProviderKind::Meta);
1975 assert_eq!(default.resolved.id, "muse-spark-1.1");
1976 assert!(default.used_fallback);
1977
1978 let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
1979 assert_eq!(alias.resolved.provider, ProviderKind::Meta);
1980 assert_eq!(alias.resolved.id, "muse-spark-1.1");
1981 assert!(!alias.used_fallback);
1982 assert_eq!(model_family("muse-spark-1.1"), ModelFamily::Meta);
1983 }
1984
1985 #[test]
1986 fn openai_gpt56_family_resolves_when_provider_hinted() {
1987 let registry = ModelRegistry::default();
1988 for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
1989 let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
1990 assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
1991 assert_eq!(resolved.resolved.id, model, "{model}");
1992 assert!(resolved.resolved.supports_tools, "{model}");
1993 assert!(resolved.resolved.supports_reasoning, "{model}");
1994 assert!(!resolved.used_fallback, "{model}");
1995 }
1996 }
1997
1998 #[test]
1999 fn grok_ids_stay_in_grok_family() {
2000 assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
2001 assert_eq!(
2002 model_family("grok-4.20-0309-non-reasoning"),
2003 ModelFamily::Grok
2004 );
2005 }
2006
2007 #[test]
2008 fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
2009 let registry = ModelRegistry::default();
2010
2011 let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
2012 assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
2013 assert_eq!(stepfun.resolved.id, "step-3.7-flash");
2014
2015 for (alias, expected) in [
2016 ("minimax", "MiniMax-M3"),
2017 ("minimax-m3", "MiniMax-M3"),
2018 ("minimax-m2.7", "MiniMax-M2.7"),
2019 ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
2020 ("minimax-m2.1", "MiniMax-M2.1"),
2021 ("minimax-m2", "MiniMax-M2"),
2022 ] {
2023 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
2024
2025 assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
2026 assert_eq!(resolved.resolved.id, expected);
2027 assert!(!resolved.used_fallback);
2028 assert!(resolved.resolved.supports_tools);
2029 assert!(resolved.resolved.supports_reasoning);
2030 }
2031 }
2032
2033 #[test]
2034 fn minimax_anthropic_models_resolve_when_provider_hinted() {
2035 let registry = ModelRegistry::default();
2036
2037 for (alias, expected) in [
2038 ("minimax-anthropic", "MiniMax-M3"),
2039 ("minimax-m3", "MiniMax-M3"),
2040 ("minimax-m2.7", "MiniMax-M2.7"),
2041 ] {
2042 let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
2043
2044 assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
2045 assert_eq!(resolved.resolved.id, expected);
2046 assert!(!resolved.used_fallback);
2047 assert!(resolved.resolved.supports_tools);
2048 assert!(resolved.resolved.supports_reasoning);
2049 }
2050 }
2051
2052 #[test]
2053 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
2054 let registry = ModelRegistry::default();
2055 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
2056
2057 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2058 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2059 }
2060
2061 #[test]
2062 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
2063 let registry = ModelRegistry::default();
2064
2065 for (alias, expected) in [
2066 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
2067 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
2068 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
2069 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
2070 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
2071 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
2072 ("glm-5.1", "z-ai/glm-5.1"),
2073 ("glm-5.2", "z-ai/glm-5.2"),
2074 ("minimax-m3", "minimax/minimax-m3"),
2075 ("minimax-2.7", "minimax/minimax-m2.7"),
2076 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
2077 ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
2078 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
2079 ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
2080 (
2081 "nvidia/nemotron-3-ultra",
2082 "nvidia/nemotron-3-ultra-550b-a55b",
2083 ),
2084 ] {
2085 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
2086
2087 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2088 assert_eq!(resolved.resolved.id, expected);
2089 assert!(resolved.resolved.supports_tools);
2090 assert!(resolved.resolved.supports_reasoning);
2091 }
2092 }
2093
2094 #[test]
2095 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
2096 let registry = ModelRegistry::default();
2097 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
2098
2099 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
2100 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2101 }
2102
2103 #[test]
2104 fn together_inkling_keeps_published_wire_identity() {
2105 let registry = ModelRegistry::default();
2106 for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
2107 let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
2108
2109 assert_eq!(resolved.resolved.provider, ProviderKind::Together);
2110 assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
2111 assert!(resolved.resolved.supports_tools);
2112 assert!(resolved.resolved.supports_reasoning);
2113 assert!(!resolved.used_fallback);
2114 }
2115
2116 let unscoped = registry.resolve(Some("inkling"), None);
2117 assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
2118 assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
2119 assert!(!unscoped.used_fallback);
2120 }
2121
2122 #[test]
2123 fn registry_lists_and_resolves_every_v090_catalog_addition() {
2124 let registry = ModelRegistry::default();
2125 let advertised = [
2126 (ProviderKind::Anthropic, "claude-sonnet-5"),
2127 (ProviderKind::Anthropic, "claude-fable-5"),
2128 (ProviderKind::Openai, "gpt-5.3-codex"),
2129 (ProviderKind::Openai, "gpt-5.5"),
2130 (ProviderKind::Openai, "gpt-5.5-pro"),
2131 (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
2132 (ProviderKind::Arcee, "trinity-mini"),
2133 ];
2134
2135 let listed = registry.list();
2136 for (provider, model_id) in advertised {
2137 assert!(
2138 listed
2139 .iter()
2140 .any(|model| model.provider == provider && model.id == model_id),
2141 "missing {model_id} ({}) from model list",
2142 provider.as_str()
2143 );
2144 let resolved = registry.resolve(Some(model_id), Some(provider));
2145 assert_eq!(resolved.resolved.provider, provider, "{model_id}");
2146 assert_eq!(resolved.resolved.id, model_id, "{model_id}");
2147 assert!(!resolved.used_fallback, "{model_id}");
2148 }
2149 }
2150
2151 #[test]
2152 fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
2153 let registry = ModelRegistry::default();
2154
2155 let unscoped = registry.resolve(Some("gpt-5.5"), None);
2156 assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
2157 assert_eq!(unscoped.resolved.id, "gpt-5.5");
2158 assert!(!unscoped.used_fallback);
2159
2160 let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
2161 assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
2162 assert_eq!(codex.resolved.id, "gpt-5.5");
2163 assert!(!codex.used_fallback);
2164 }
2165
2166 #[test]
2167 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
2168 let registry = ModelRegistry::default();
2169 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
2170
2171 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2172 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2173 }
2174
2175 #[test]
2176 fn vllm_default_uses_canonical_model_id() {
2177 let registry = ModelRegistry::default();
2178 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
2179
2180 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2181 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2182 }
2183
2184 #[test]
2185 fn ollama_default_uses_small_local_model_id() {
2186 let registry = ModelRegistry::default();
2187 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
2188
2189 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2190 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2191 assert!(resolved.resolved.supports_reasoning);
2192 }
2193
2194 #[test]
2195 fn ollama_requested_model_tag_is_preserved() {
2196 let registry = ModelRegistry::default();
2197 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
2198
2199 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2200 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
2201 assert!(!resolved.used_fallback);
2202 }
2203
2204 #[test]
2205 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
2206 let registry = ModelRegistry::default();
2207 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
2208
2209 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2210 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2211 }
2212
2213 #[test]
2214 fn preserves_requested_model_casing_for_third_party_providers() {
2215 let registry = ModelRegistry::default();
2216 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
2217
2218 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2219 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2220 }
2221
2222 #[test]
2223 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
2224 let registry = ModelRegistry::default();
2225 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
2226
2227 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2228 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
2230 }
2231
2232 #[test]
2233 fn preserves_requested_model_casing_without_surrounding_whitespace() {
2234 let registry = ModelRegistry::default();
2235 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
2236
2237 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2238 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2239 }
2240
2241 #[test]
2242 fn alias_match_does_not_override_requested_casing() {
2243 let registry = ModelRegistry::default();
2244 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
2245
2246 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2247 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2248 }
2249
2250 #[test]
2251 fn model_family_classifies_known_model_ids() {
2252 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
2253 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
2254 assert_eq!(
2255 model_family("anthropic/claude-opus-4-7"),
2256 ModelFamily::Anthropic
2257 );
2258 assert_eq!(
2259 model_family("meta-llama/llama-3.3-70b-instruct"),
2260 ModelFamily::Meta
2261 );
2262 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
2263 }
2264
2265 #[test]
2266 fn model_family_uses_underlying_model_for_router_ids() {
2267 assert_eq!(
2268 model_family("groq/llama-3.3-70b-versatile"),
2269 ModelFamily::Meta
2270 );
2271 assert_eq!(
2272 model_family("openrouter/openai/gpt-5.4"),
2273 ModelFamily::OpenAI
2274 );
2275 assert_eq!(
2276 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
2277 ModelFamily::DeepSeek
2278 );
2279 }
2280
2281 #[test]
2282 fn model_family_covers_prominent_google_and_mistral_model_names() {
2283 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
2284 assert_eq!(
2285 model_family("mistralai/mixtral-8x22b"),
2286 ModelFamily::Mistral
2287 );
2288 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2289 }
2290
2291 #[test]
2292 fn model_family_falls_back_to_inferencer_for_unknown_models() {
2293 assert_eq!(
2294 model_family("custom-gateway/my-private-model"),
2295 ModelFamily::Inferencer
2296 );
2297 assert_eq!(model_family(""), ModelFamily::Inferencer);
2298 }
2299}