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