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.3".to_string(),
437 provider: ProviderKind::Zai,
438 aliases: vec![
439 "glm-5.3".to_string(),
440 "glm-5-3".to_string(),
441 "zai-glm-5.3".to_string(),
442 "zai-glm-5-3".to_string(),
443 ],
444 supports_tools: true,
445 supports_reasoning: true,
446 },
447 ModelInfo {
450 id: "GLM-5.2".to_string(),
451 provider: ProviderKind::Zai,
452 aliases: vec![
453 "glm-5.2".to_string(),
454 "glm-5-2".to_string(),
455 "zai-glm-5.2".to_string(),
456 "zai-glm-5-2".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: "gpt-oss:120b".to_string(),
754 provider: ProviderKind::OllamaCloud,
755 aliases: vec![],
756 supports_tools: true,
757 supports_reasoning: true,
758 },
759 ModelInfo {
760 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
761 provider: ProviderKind::Huggingface,
762 aliases: vec![
763 "deepseek-v4-pro".to_string(),
764 "hf-deepseek-v4-pro".to_string(),
765 ],
766 supports_tools: true,
767 supports_reasoning: true,
768 },
769 ModelInfo {
770 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
771 provider: ProviderKind::Huggingface,
772 aliases: vec![
773 "deepseek-v4-flash".to_string(),
774 "deepseek-chat".to_string(),
775 "deepseek-reasoner".to_string(),
776 "hf-deepseek-v4-flash".to_string(),
777 ],
778 supports_tools: true,
779 supports_reasoning: true,
780 },
781 ModelInfo {
783 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
784 provider: ProviderKind::Together,
785 aliases: vec![
786 "deepseek-v4-pro".to_string(),
787 "together-deepseek-v4-pro".to_string(),
788 ],
789 supports_tools: true,
790 supports_reasoning: true,
791 },
792 ModelInfo {
793 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
794 provider: ProviderKind::Together,
795 aliases: vec![
796 "deepseek-v4-flash".to_string(),
797 "deepseek-chat".to_string(),
798 "together-deepseek-v4-flash".to_string(),
799 ],
800 supports_tools: true,
801 supports_reasoning: true,
802 },
803 ModelInfo {
804 id: "thinkingmachines/inkling".to_string(),
807 provider: ProviderKind::Together,
808 aliases: vec!["inkling".to_string(), "together-inkling".to_string()],
809 supports_tools: true,
810 supports_reasoning: true,
811 },
812 ModelInfo {
814 id: "qwen/qwen3.7-max".to_string(),
815 provider: ProviderKind::Openrouter,
816 aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
817 supports_tools: true,
818 supports_reasoning: true,
819 },
820 ModelInfo {
822 id: "gpt-5.5".to_string(),
823 provider: ProviderKind::OpenaiCodex,
824 aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
825 supports_tools: true,
826 supports_reasoning: true,
827 },
828 ModelInfo {
830 id: "claude-opus-4-8".to_string(),
831 provider: ProviderKind::Anthropic,
832 aliases: vec!["opus".to_string(), "claude-opus".to_string()],
833 supports_tools: true,
834 supports_reasoning: true,
835 },
836 ModelInfo {
840 id: "claude-opus-5".to_string(),
841 provider: ProviderKind::Anthropic,
842 aliases: vec!["opus-5".to_string()],
843 supports_tools: true,
844 supports_reasoning: true,
845 },
846 ModelInfo {
847 id: "claude-sonnet-4-6".to_string(),
848 provider: ProviderKind::Anthropic,
849 aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
850 supports_tools: true,
851 supports_reasoning: true,
852 },
853 ModelInfo {
854 id: "claude-haiku-4-5".to_string(),
855 provider: ProviderKind::Anthropic,
856 aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
857 supports_tools: true,
858 supports_reasoning: false,
859 },
860 ModelInfo {
861 id: "claude-sonnet-5".to_string(),
862 provider: ProviderKind::Anthropic,
863 aliases: vec!["sonnet-5".to_string()],
864 supports_tools: true,
865 supports_reasoning: true,
866 },
867 ModelInfo {
868 id: "claude-fable-5".to_string(),
869 provider: ProviderKind::Anthropic,
870 aliases: vec!["fable".to_string(), "fable-5".to_string()],
871 supports_tools: true,
872 supports_reasoning: true,
873 },
874 ModelInfo {
876 id: "deepseek-v4-flash".to_string(),
877 provider: ProviderKind::Openmodel,
878 aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
879 supports_tools: true,
880 supports_reasoning: true,
881 },
882 ModelInfo {
884 id: "minimax/minimax-m2.7".to_string(),
885 provider: ProviderKind::Openrouter,
886 aliases: vec![
887 "minimax-2.7".to_string(),
888 "minimax-2-7".to_string(),
889 "openrouter-minimax-2.7".to_string(),
890 ],
891 supports_tools: true,
892 supports_reasoning: true,
893 },
894 ModelInfo {
895 id: "step-3.7-flash".to_string(),
896 provider: ProviderKind::Stepfun,
897 aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
898 supports_tools: true,
899 supports_reasoning: false,
900 },
901 ModelInfo {
902 id: "MiniMax-M3".to_string(),
903 provider: ProviderKind::Minimax,
904 aliases: vec![
905 "minimax".to_string(),
906 "minimax-m3".to_string(),
907 "minimax-m-3".to_string(),
908 ],
909 supports_tools: true,
910 supports_reasoning: true,
911 },
912 ModelInfo {
913 id: "MiniMax-M2.7".to_string(),
914 provider: ProviderKind::Minimax,
915 aliases: vec![
916 "minimax-m2.7".to_string(),
917 "minimax-m2-7".to_string(),
918 "minimax-m-2.7".to_string(),
919 "minimax-m-2-7".to_string(),
920 ],
921 supports_tools: true,
922 supports_reasoning: true,
923 },
924 ModelInfo {
925 id: "MiniMax-M3".to_string(),
926 provider: ProviderKind::MinimaxAnthropic,
927 aliases: vec![
928 "minimax-anthropic".to_string(),
929 "minimax-anthropic-m3".to_string(),
930 "minimax-m3".to_string(),
931 ],
932 supports_tools: true,
933 supports_reasoning: true,
934 },
935 ModelInfo {
936 id: "MiniMax-M2.7".to_string(),
937 provider: ProviderKind::MinimaxAnthropic,
938 aliases: vec![
939 "minimax-anthropic-m2.7".to_string(),
940 "minimax-anthropic-m2-7".to_string(),
941 "minimax-m2.7".to_string(),
942 ],
943 supports_tools: true,
944 supports_reasoning: true,
945 },
946 ModelInfo {
947 id: "MiniMax-M2.7-highspeed".to_string(),
948 provider: ProviderKind::Minimax,
949 aliases: vec![
950 "minimax-m2.7-highspeed".to_string(),
951 "minimax-m2-7-highspeed".to_string(),
952 "minimax-m-2.7-highspeed".to_string(),
953 "minimax-m-2-7-highspeed".to_string(),
954 ],
955 supports_tools: true,
956 supports_reasoning: true,
957 },
958 ModelInfo {
959 id: "MiniMax-M2.5".to_string(),
960 provider: ProviderKind::Minimax,
961 aliases: vec![
962 "minimax-m2.5".to_string(),
963 "minimax-m2-5".to_string(),
964 "minimax-m-2.5".to_string(),
965 "minimax-m-2-5".to_string(),
966 ],
967 supports_tools: true,
968 supports_reasoning: true,
969 },
970 ModelInfo {
971 id: "MiniMax-M2.5-highspeed".to_string(),
972 provider: ProviderKind::Minimax,
973 aliases: vec![
974 "minimax-m2.5-highspeed".to_string(),
975 "minimax-m2-5-highspeed".to_string(),
976 "minimax-m-2.5-highspeed".to_string(),
977 "minimax-m-2-5-highspeed".to_string(),
978 ],
979 supports_tools: true,
980 supports_reasoning: true,
981 },
982 ModelInfo {
983 id: "MiniMax-M2.1".to_string(),
984 provider: ProviderKind::Minimax,
985 aliases: vec![
986 "minimax-m2.1".to_string(),
987 "minimax-m2-1".to_string(),
988 "minimax-m-2.1".to_string(),
989 "minimax-m-2-1".to_string(),
990 ],
991 supports_tools: true,
992 supports_reasoning: true,
993 },
994 ModelInfo {
995 id: "MiniMax-M2.1-highspeed".to_string(),
996 provider: ProviderKind::Minimax,
997 aliases: vec![
998 "minimax-m2.1-highspeed".to_string(),
999 "minimax-m2-1-highspeed".to_string(),
1000 "minimax-m-2.1-highspeed".to_string(),
1001 "minimax-m-2-1-highspeed".to_string(),
1002 ],
1003 supports_tools: true,
1004 supports_reasoning: true,
1005 },
1006 ModelInfo {
1007 id: "MiniMax-M2".to_string(),
1008 provider: ProviderKind::Minimax,
1009 aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
1010 supports_tools: true,
1011 supports_reasoning: true,
1012 },
1013 ModelInfo {
1015 id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
1016 provider: ProviderKind::Openrouter,
1017 aliases: vec![
1018 "nvidia/nemotron-3-ultra".to_string(),
1019 "nemotron-3-ultra".to_string(),
1020 "nemotron-3-ultra-550b-a55b".to_string(),
1021 "nvidia-nemotron-3-ultra".to_string(),
1022 "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
1023 ],
1024 supports_tools: true,
1025 supports_reasoning: true,
1026 },
1027 ModelInfo {
1029 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
1030 provider: ProviderKind::Deepinfra,
1031 aliases: vec![
1032 "deepseek-v4-pro".to_string(),
1033 "di-deepseek-v4-pro".to_string(),
1034 ],
1035 supports_tools: true,
1036 supports_reasoning: true,
1037 },
1038 ModelInfo {
1039 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
1040 provider: ProviderKind::Deepinfra,
1041 aliases: vec![
1042 "deepseek-v4-flash".to_string(),
1043 "di-deepseek-v4-flash".to_string(),
1044 ],
1045 supports_tools: true,
1046 supports_reasoning: true,
1047 },
1048 ModelInfo {
1050 id: "fugu".to_string(),
1051 provider: ProviderKind::Sakana,
1052 aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
1053 supports_tools: true,
1054 supports_reasoning: false,
1055 },
1056 ModelInfo {
1057 id: "fugu-ultra-20260615".to_string(),
1058 provider: ProviderKind::Sakana,
1059 aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
1060 supports_tools: true,
1061 supports_reasoning: true,
1062 },
1063 ModelInfo {
1065 id: "LongCat-2.0".to_string(),
1066 provider: ProviderKind::LongCat,
1067 aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
1068 supports_tools: true,
1069 supports_reasoning: true,
1070 },
1071 ModelInfo {
1075 id: "deepseek-v4-pro".to_string(),
1076 provider: ProviderKind::OpencodeGo,
1077 aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
1078 supports_tools: true,
1079 supports_reasoning: true,
1080 },
1081 ModelInfo {
1082 id: "grok-4.5".to_string(),
1083 provider: ProviderKind::OpencodeGo,
1084 aliases: vec!["opencode-go/grok-4.5".to_string()],
1085 supports_tools: true,
1086 supports_reasoning: true,
1087 },
1088 ModelInfo {
1092 id: "glm-5.2".to_string(),
1093 provider: ProviderKind::OpencodeGo,
1094 aliases: vec!["opencode-go/glm-5.2".to_string()],
1095 supports_tools: true,
1096 supports_reasoning: true,
1097 },
1098 ModelInfo {
1099 id: "glm-5.1".to_string(),
1100 provider: ProviderKind::OpencodeGo,
1101 aliases: vec!["opencode-go/glm-5.1".to_string()],
1102 supports_tools: true,
1103 supports_reasoning: true,
1104 },
1105 ModelInfo {
1106 id: "kimi-k3".to_string(),
1107 provider: ProviderKind::OpencodeGo,
1108 aliases: vec!["opencode-go/kimi-k3".to_string()],
1109 supports_tools: true,
1110 supports_reasoning: true,
1111 },
1112 ModelInfo {
1113 id: "kimi-k2.7-code".to_string(),
1114 provider: ProviderKind::OpencodeGo,
1115 aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
1116 supports_tools: true,
1117 supports_reasoning: true,
1118 },
1119 ModelInfo {
1120 id: "kimi-k2.6".to_string(),
1121 provider: ProviderKind::OpencodeGo,
1122 aliases: vec!["opencode-go/kimi-k2.6".to_string()],
1123 supports_tools: true,
1124 supports_reasoning: true,
1125 },
1126 ModelInfo {
1127 id: "deepseek-v4-flash".to_string(),
1128 provider: ProviderKind::OpencodeGo,
1129 aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
1130 supports_tools: true,
1131 supports_reasoning: true,
1132 },
1133 ModelInfo {
1134 id: "mimo-v2.5".to_string(),
1135 provider: ProviderKind::OpencodeGo,
1136 aliases: vec!["opencode-go/mimo-v2.5".to_string()],
1137 supports_tools: true,
1138 supports_reasoning: true,
1139 },
1140 ModelInfo {
1141 id: "mimo-v2.5-pro".to_string(),
1142 provider: ProviderKind::OpencodeGo,
1143 aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
1144 supports_tools: true,
1145 supports_reasoning: true,
1146 },
1147 ModelInfo {
1153 id: "muse-spark-1.2".to_string(),
1154 provider: ProviderKind::Meta,
1155 aliases: vec!["muse-spark".to_string(), "muse".to_string()],
1156 supports_tools: true,
1157 supports_reasoning: true,
1158 },
1159 ModelInfo {
1160 id: "muse-spark-1.2-contributor".to_string(),
1161 provider: ProviderKind::Meta,
1162 aliases: vec!["muse-spark-contributor".to_string()],
1163 supports_tools: true,
1164 supports_reasoning: true,
1165 },
1166 ModelInfo {
1168 id: "grok-4.6".to_string(),
1169 provider: ProviderKind::Xai,
1170 aliases: vec!["grok".to_string()],
1171 supports_tools: true,
1172 supports_reasoning: true,
1173 },
1174 ModelInfo {
1175 id: "grok-4.5".to_string(),
1176 provider: ProviderKind::Xai,
1177 aliases: vec!["xai-grok-4.5".to_string()],
1178 supports_tools: true,
1179 supports_reasoning: true,
1180 },
1181 ModelInfo {
1182 id: "grok-4.3".to_string(),
1183 provider: ProviderKind::Xai,
1184 aliases: vec!["xai-grok-4.3".to_string()],
1185 supports_tools: true,
1186 supports_reasoning: true,
1187 },
1188 ModelInfo {
1189 id: "grok-build".to_string(),
1190 provider: ProviderKind::Xai,
1191 aliases: vec!["xai-grok-build".to_string()],
1192 supports_tools: true,
1193 supports_reasoning: true,
1194 },
1195 ModelInfo {
1196 id: "grok-composer-2.5-fast".to_string(),
1197 provider: ProviderKind::Xai,
1198 aliases: vec!["xai-grok-composer".to_string()],
1199 supports_tools: true,
1200 supports_reasoning: false,
1201 },
1202 ModelInfo {
1203 id: "grok-4.20-0309-reasoning".to_string(),
1204 provider: ProviderKind::Xai,
1205 aliases: vec!["xai-grok-reasoning".to_string()],
1206 supports_tools: true,
1207 supports_reasoning: true,
1208 },
1209 ModelInfo {
1210 id: "grok-4.20-0309-non-reasoning".to_string(),
1211 provider: ProviderKind::Xai,
1212 aliases: vec!["xai-grok-fast".to_string()],
1213 supports_tools: true,
1214 supports_reasoning: false,
1215 },
1216 ModelInfo {
1217 id: "gemini-3.1-pro-preview".to_string(),
1218 provider: ProviderKind::Google,
1219 aliases: vec!["gemini-3.1-pro".to_string()],
1220 supports_tools: true,
1221 supports_reasoning: true,
1222 },
1223 ModelInfo {
1224 id: "gemini-3-pro-preview".to_string(),
1225 provider: ProviderKind::Google,
1226 aliases: vec!["gemini-3-pro".to_string()],
1227 supports_tools: true,
1228 supports_reasoning: true,
1229 },
1230 ModelInfo {
1233 id: "gemini-3.7-flash".to_string(),
1234 provider: ProviderKind::Google,
1235 aliases: vec![],
1236 supports_tools: true,
1237 supports_reasoning: true,
1238 },
1239 ModelInfo {
1240 id: "gemini-3.6-flash".to_string(),
1241 provider: ProviderKind::Google,
1242 aliases: vec![],
1243 supports_tools: true,
1244 supports_reasoning: true,
1245 },
1246 ModelInfo {
1247 id: "gemini-3.5-flash".to_string(),
1248 provider: ProviderKind::Google,
1249 aliases: vec![],
1250 supports_tools: true,
1251 supports_reasoning: true,
1252 },
1253 ModelInfo {
1254 id: "gemini-3.5-flash-lite".to_string(),
1255 provider: ProviderKind::Google,
1256 aliases: vec![],
1257 supports_tools: true,
1258 supports_reasoning: false,
1259 },
1260 ModelInfo {
1261 id: "gemini-2.5-pro".to_string(),
1262 provider: ProviderKind::Google,
1263 aliases: vec![],
1264 supports_tools: true,
1265 supports_reasoning: true,
1266 },
1267 ModelInfo {
1268 id: "gemini-2.5-flash".to_string(),
1269 provider: ProviderKind::Google,
1270 aliases: vec![],
1271 supports_tools: true,
1272 supports_reasoning: true,
1273 },
1274 ModelInfo {
1275 id: "mistral-code-latest".to_string(),
1276 provider: ProviderKind::Mistral,
1277 aliases: vec![
1278 "codestral".to_string(),
1279 "codestral-latest".to_string(),
1280 "mistral-code".to_string(),
1281 ],
1282 supports_tools: true,
1283 supports_reasoning: false,
1284 },
1285 ModelInfo {
1286 id: "mistral-medium-latest".to_string(),
1287 provider: ProviderKind::Mistral,
1288 aliases: vec![
1289 "mistral-medium".to_string(),
1290 "mistral-medium-3-5".to_string(),
1291 ],
1292 supports_tools: true,
1293 supports_reasoning: true,
1294 },
1295 ModelInfo {
1296 id: "mistral-small-latest".to_string(),
1297 provider: ProviderKind::Mistral,
1298 aliases: vec![
1299 "mistral-small".to_string(),
1300 "mistral-small-2603".to_string(),
1301 ],
1302 supports_tools: true,
1303 supports_reasoning: true,
1304 },
1305 ModelInfo {
1306 id: "magistral-small-latest".to_string(),
1307 provider: ProviderKind::Mistral,
1308 aliases: vec!["magistral".to_string(), "magistral-small".to_string()],
1309 supports_tools: true,
1310 supports_reasoning: true,
1311 },
1312 ModelInfo {
1313 id: "mistral-large-latest".to_string(),
1314 provider: ProviderKind::Mistral,
1315 aliases: vec!["mistral-large".to_string()],
1316 supports_tools: true,
1317 supports_reasoning: false,
1318 },
1319 ];
1320 Self::new(models)
1321 }
1322}
1323
1324impl ModelRegistry {
1325 #[must_use]
1331 pub fn new(models: Vec<ModelInfo>) -> Self {
1332 let mut alias_map = HashMap::new();
1333 for (idx, model) in models.iter().enumerate() {
1334 alias_map.entry(normalize(&model.id)).or_insert(idx);
1335 for alias in &model.aliases {
1336 alias_map.entry(normalize(alias)).or_insert(idx);
1337 }
1338 }
1339 Self { models, alias_map }
1340 }
1341
1342 #[must_use]
1344 pub fn list(&self) -> Vec<ModelInfo> {
1345 self.models.clone()
1346 }
1347
1348 #[must_use]
1360 pub fn resolve(
1361 &self,
1362 requested: Option<&str>,
1363 provider_hint: Option<ProviderKind>,
1364 ) -> ModelResolution {
1365 let mut fallback_chain = Vec::new();
1366
1367 if let Some(name) = requested {
1368 fallback_chain.push(format!("requested:{name}"));
1369 if matches!(
1370 provider_hint,
1371 Some(ProviderKind::Ollama | ProviderKind::OllamaCloud)
1372 ) {
1373 return ModelResolution {
1374 requested: Some(name.to_string()),
1375 resolved: ModelInfo {
1376 id: name.trim().to_string(),
1377 provider: provider_hint.expect("matched provider hint"),
1378 aliases: Vec::new(),
1379 supports_tools: true,
1380 supports_reasoning: false,
1381 },
1382 used_fallback: false,
1383 fallback_chain,
1384 };
1385 }
1386 if provider_hint == Some(ProviderKind::OpencodeGo)
1392 && let Some(canonical) = opencode_go_chat_model_id(name)
1393 && let Some(model) = self
1394 .models
1395 .iter()
1396 .find(|model| {
1397 model.provider == ProviderKind::OpencodeGo
1398 && model.id.eq_ignore_ascii_case(canonical)
1399 })
1400 .cloned()
1401 {
1402 return ModelResolution {
1403 requested: Some(name.to_string()),
1404 resolved: model,
1405 used_fallback: false,
1406 fallback_chain,
1407 };
1408 }
1409 if provider_hint != Some(ProviderKind::OpencodeGo)
1410 && let Some(provider) = provider_hint
1411 && let Some(model) = self
1412 .models
1413 .iter()
1414 .find(|m| m.provider == provider && model_matches(m, name))
1415 .cloned()
1416 {
1417 return ModelResolution {
1418 requested: Some(name.to_string()),
1419 resolved: model,
1420 used_fallback: false,
1421 fallback_chain,
1422 };
1423 }
1424 if provider_hint == Some(ProviderKind::Atlascloud)
1425 && let Some(model) = atlascloud_passthrough_model(name)
1426 {
1427 return ModelResolution {
1428 requested: Some(name.to_string()),
1429 resolved: model,
1430 used_fallback: false,
1431 fallback_chain,
1432 };
1433 }
1434 if provider_hint == Some(ProviderKind::Arcee)
1435 && let Some(model) = arcee_passthrough_model(name)
1436 {
1437 return ModelResolution {
1438 requested: Some(name.to_string()),
1439 resolved: model,
1440 used_fallback: false,
1441 fallback_chain,
1442 };
1443 }
1444 if provider_hint == Some(ProviderKind::XiaomiMimo)
1445 && let Some(model) = xiaomi_mimo_passthrough_model(name)
1446 {
1447 return ModelResolution {
1448 requested: Some(name.to_string()),
1449 resolved: model,
1450 used_fallback: false,
1451 fallback_chain,
1452 };
1453 }
1454 if provider_hint != Some(ProviderKind::OpencodeGo)
1462 && let Some(idx) = self.alias_map.get(&normalize(name))
1463 && provider_hint.is_none_or(|hint| self.models[*idx].provider == hint)
1464 {
1465 return ModelResolution {
1466 requested: Some(name.to_string()),
1467 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1468 used_fallback: false,
1469 fallback_chain,
1470 };
1471 }
1472 }
1473
1474 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1475 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1476 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1477 return ModelResolution {
1478 requested: requested.map(ToOwned::to_owned),
1479 resolved: model,
1480 used_fallback: true,
1481 fallback_chain,
1482 };
1483 }
1484
1485 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1486 id: "deepseek-v4-pro".to_string(),
1487 provider: ProviderKind::Deepseek,
1488 aliases: Vec::new(),
1489 supports_tools: true,
1490 supports_reasoning: true,
1491 });
1492 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1493 ModelResolution {
1494 requested: requested.map(ToOwned::to_owned),
1495 resolved: final_fallback,
1496 used_fallback: true,
1497 fallback_chain,
1498 }
1499 }
1500}
1501
1502fn normalize(value: &str) -> String {
1503 value.trim().to_ascii_lowercase()
1504}
1505
1506#[must_use]
1507pub fn model_family(model_id: &str) -> ModelFamily {
1509 let normalized = normalize(model_id);
1510 if normalized.is_empty() {
1511 return ModelFamily::Inferencer;
1512 }
1513
1514 if normalized.contains("deepseek") {
1515 return ModelFamily::DeepSeek;
1516 }
1517 if normalized.contains("claude") || normalized.contains("anthropic") {
1518 return ModelFamily::Anthropic;
1519 }
1520 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1521 return ModelFamily::GptOss;
1522 }
1523 if normalized.starts_with("gpt-")
1524 || normalized.contains("/gpt-")
1525 || normalized.contains("openai/")
1526 {
1527 return ModelFamily::OpenAI;
1528 }
1529 if normalized.contains("gemini")
1530 || normalized.contains("gemma")
1531 || normalized.contains("google/")
1532 {
1533 return ModelFamily::Google;
1534 }
1535 if normalized.contains("llama")
1536 || normalized.contains("muse-spark")
1537 || normalized.contains("meta-")
1538 || normalized.contains("meta/")
1539 {
1540 return ModelFamily::Meta;
1541 }
1542 if normalized.contains("mistral")
1543 || normalized.contains("mixtral")
1544 || normalized.contains("codestral")
1545 {
1546 return ModelFamily::Mistral;
1547 }
1548 if normalized.contains("qwen") {
1549 return ModelFamily::Qwen;
1550 }
1551 if normalized.contains("grok") {
1552 return ModelFamily::Grok;
1553 }
1554 if normalized.contains("cohere") || normalized.contains("command-r") {
1555 return ModelFamily::Cohere;
1556 }
1557
1558 ModelFamily::Inferencer
1559}
1560
1561fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1562 let requested = normalize(requested);
1563 normalize(&model.id) == requested
1564 || model
1565 .aliases
1566 .iter()
1567 .any(|alias| normalize(alias) == requested)
1568}
1569
1570fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1571 let requested = requested.trim();
1572 if model.id.eq_ignore_ascii_case(requested) {
1573 model.id = requested.to_string();
1574 }
1575 model
1576}
1577
1578fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1579 let requested = requested.trim();
1580 if requested.is_empty() || !requested.contains('/') {
1581 return None;
1582 }
1583
1584 Some(ModelInfo {
1585 id: requested.to_string(),
1586 provider: ProviderKind::Atlascloud,
1587 aliases: Vec::new(),
1588 supports_tools: true,
1589 supports_reasoning: true,
1590 })
1591}
1592
1593fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1594 let requested = requested.trim();
1595 if requested.is_empty() {
1596 return None;
1597 }
1598 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1599
1600 Some(ModelInfo {
1601 id: requested.to_string(),
1602 provider: ProviderKind::Arcee,
1603 aliases: Vec::new(),
1604 supports_tools: true,
1605 supports_reasoning,
1606 })
1607}
1608
1609fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1610 let requested = requested.trim();
1611 if requested.is_empty() || requested.chars().any(char::is_control) {
1612 return None;
1613 }
1614
1615 Some(ModelInfo {
1616 id: requested.to_string(),
1617 provider: ProviderKind::XiaomiMimo,
1618 aliases: Vec::new(),
1619 supports_tools: true,
1620 supports_reasoning: true,
1621 })
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626 use super::*;
1627
1628 #[test]
1629 fn model_registry_new_builds_alias_map_correctly() {
1630 let models = vec![
1631 ModelInfo {
1632 id: "Model-A".to_string(),
1633 provider: ProviderKind::Deepseek,
1634 aliases: vec!["alias-1".to_string(), " ALIAS-2 ".to_string()],
1635 supports_tools: true,
1636 supports_reasoning: false,
1637 },
1638 ModelInfo {
1639 id: "model-b".to_string(),
1640 provider: ProviderKind::Deepseek,
1641 aliases: vec!["alias-1".to_string()], supports_tools: true,
1643 supports_reasoning: true,
1644 },
1645 ];
1646
1647 let registry = ModelRegistry::new(models);
1648
1649 assert_eq!(registry.alias_map.len(), 4); assert_eq!(registry.alias_map.get("model-a"), Some(&0));
1651 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));
1654 }
1655
1656 #[test]
1657 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1658 let registry = ModelRegistry::default();
1659 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1660
1661 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1662 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1663 }
1664
1665 #[test]
1666 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1667 let registry = ModelRegistry::default();
1668 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1669
1670 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1671 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1672 }
1673
1674 #[test]
1675 fn nvidia_nim_default_uses_catalog_model_id() {
1676 let registry = ModelRegistry::default();
1677 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1678
1679 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1680 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1681 }
1682
1683 #[test]
1684 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1685 let registry = ModelRegistry::default();
1686 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1687
1688 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1689 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1690 }
1691
1692 #[test]
1693 fn atlascloud_default_uses_namespaced_model_id() {
1694 let registry = ModelRegistry::default();
1695 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1696
1697 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1698 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1699 assert!(resolved.resolved.supports_reasoning);
1700 }
1701
1702 #[test]
1703 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1704 let registry = ModelRegistry::default();
1705 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1706
1707 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1708 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1709 }
1710
1711 #[test]
1712 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1713 let registry = ModelRegistry::default();
1714 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1715
1716 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1717 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1718 }
1719
1720 #[test]
1721 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1722 let registry = ModelRegistry::default();
1723 let resolved =
1724 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1725
1726 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1727 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1728 assert!(resolved.resolved.supports_tools);
1729 assert!(resolved.resolved.supports_reasoning);
1730 assert!(!resolved.used_fallback);
1731 }
1732
1733 #[test]
1734 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1735 let registry = ModelRegistry::default();
1736 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1737
1738 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1739 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1740 assert!(!resolved.used_fallback);
1741 }
1742
1743 #[test]
1744 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1745 let registry = ModelRegistry::default();
1746 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1747
1748 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1749 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1750 assert!(resolved.used_fallback);
1751 }
1752
1753 #[test]
1754 fn openrouter_default_uses_namespaced_model_id() {
1755 let registry = ModelRegistry::default();
1756 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1757
1758 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1759 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1760 }
1761
1762 #[test]
1763 fn xiaomi_mimo_default_uses_canonical_model_id() {
1764 let registry = ModelRegistry::default();
1765 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1766
1767 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1768 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1769 assert!(resolved.resolved.supports_reasoning);
1770 }
1771
1772 #[test]
1773 fn moonshot_default_and_aliases_use_kimi_k27_code() {
1774 let registry = ModelRegistry::default();
1775
1776 for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1777 let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1778
1779 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1780 assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1781 assert!(resolved.resolved.supports_tools);
1782 assert!(resolved.resolved.supports_reasoning);
1783 }
1784 }
1785
1786 #[test]
1787 fn moonshot_explicit_kimi_k26_remains_available() {
1788 let registry = ModelRegistry::default();
1789 let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1790
1791 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1792 assert_eq!(resolved.resolved.id, "kimi-k2.6");
1793 assert!(resolved.resolved.supports_reasoning);
1794 }
1795
1796 #[test]
1800 fn moonshot_resolves_both_k3_products_without_crossing_them() {
1801 let registry = ModelRegistry::default();
1802
1803 for (requested, expected) in [("kimi-k3", "kimi-k3"), ("k3", "k3")] {
1804 let resolved = registry.resolve(Some(requested), Some(ProviderKind::Moonshot));
1805
1806 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1807 assert_eq!(resolved.resolved.id, expected, "{resolved:?}");
1808 assert!(
1809 !resolved.used_fallback,
1810 "an explicit Moonshot K3 request is not a fallback: {resolved:?}"
1811 );
1812 }
1813 }
1814
1815 #[test]
1819 fn moonshot_k3_ids_are_never_rewritten_into_each_other() {
1820 let registry = ModelRegistry::default();
1821
1822 assert_eq!(
1823 registry
1824 .resolve(Some("kimi-k3"), Some(ProviderKind::Moonshot))
1825 .resolved
1826 .id,
1827 "kimi-k3"
1828 );
1829 assert_eq!(
1830 registry
1831 .resolve(Some("k3"), Some(ProviderKind::Moonshot))
1832 .resolved
1833 .id,
1834 "k3"
1835 );
1836 }
1837
1838 #[test]
1842 fn a_provider_hint_never_resolves_to_another_providers_model() {
1843 let registry = ModelRegistry::default();
1844
1845 let resolved = registry.resolve(Some("glm-5.2"), Some(ProviderKind::Moonshot));
1846 assert_eq!(
1847 resolved.resolved.provider,
1848 ProviderKind::Moonshot,
1849 "a Moonshot request must not be answered by Z.ai: {resolved:?}"
1850 );
1851 assert!(
1852 resolved.used_fallback,
1853 "an unservable id must be reported as a fallback, not as the request: {resolved:?}"
1854 );
1855
1856 let go = registry.resolve(Some("kimi-k3"), Some(ProviderKind::OpencodeGo));
1857 assert_eq!(go.resolved.provider, ProviderKind::OpencodeGo);
1858 assert_eq!(go.resolved.id, "kimi-k3");
1859 }
1860
1861 #[test]
1862 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1863 let registry = ModelRegistry::default();
1864 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1865 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1866 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1867 assert!(!resolved.resolved.supports_tools);
1868 assert!(!resolved.resolved.supports_reasoning);
1869
1870 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1871 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1872
1873 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1874 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1875 }
1876
1877 #[test]
1878 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1879 let registry = ModelRegistry::default();
1880
1881 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1882 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1883 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1884 assert!(resolved.resolved.supports_tools);
1885 }
1886
1887 #[test]
1888 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1889 let registry = ModelRegistry::default();
1890 let resolved =
1891 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1892
1893 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1894 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1895 assert!(!resolved.used_fallback);
1896 }
1897
1898 #[test]
1899 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1900 let registry = ModelRegistry::default();
1901 let resolved = registry.resolve(
1902 Some("deepseek/deepseek-v4-pro"),
1903 Some(ProviderKind::XiaomiMimo),
1904 );
1905
1906 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1907 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1908 assert!(!resolved.used_fallback);
1909 }
1910
1911 #[test]
1912 fn wanjie_ark_default_uses_reasoner_model_id() {
1913 let registry = ModelRegistry::default();
1914 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1915
1916 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1917 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1918 assert!(resolved.resolved.supports_reasoning);
1919 }
1920
1921 #[test]
1922 fn novita_default_uses_namespaced_model_id() {
1923 let registry = ModelRegistry::default();
1924 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1925
1926 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1927 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1928 }
1929
1930 #[test]
1931 fn fireworks_default_uses_canonical_model_id() {
1932 let registry = ModelRegistry::default();
1933 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1934
1935 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1936 assert_eq!(
1937 resolved.resolved.id,
1938 "accounts/fireworks/models/deepseek-v4-pro"
1939 );
1940 }
1941
1942 #[test]
1943 fn siliconflow_default_uses_canonical_pro_model_id() {
1944 let registry = ModelRegistry::default();
1945 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1946
1947 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1948 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1949 assert!(resolved.resolved.supports_reasoning);
1950 }
1951
1952 #[test]
1953 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1954 let registry = ModelRegistry::default();
1955 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1956
1957 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1958 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1959 assert!(resolved.resolved.supports_reasoning);
1960 }
1961
1962 #[test]
1963 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1964 let registry = ModelRegistry::default();
1965 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1966
1967 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1968 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1969 assert!(resolved.resolved.supports_reasoning);
1970 }
1971
1972 #[test]
1973 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1974 let registry = ModelRegistry::default();
1975 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1976
1977 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1978 assert_eq!(resolved.resolved.id, "trinity-mini");
1979 assert!(resolved.resolved.supports_reasoning);
1980 assert!(!resolved.used_fallback);
1981 }
1982
1983 #[test]
1984 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1985 let registry = ModelRegistry::default();
1986 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1987
1988 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1989 assert_eq!(resolved.resolved.id, "trinity-large-next");
1990 assert!(!resolved.resolved.supports_reasoning);
1991 assert!(!resolved.used_fallback);
1992 }
1993
1994 #[test]
1995 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1996 let registry = ModelRegistry::default();
1997 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1998
1999 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
2000 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2001 }
2002
2003 #[test]
2004 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
2005 let registry = ModelRegistry::default();
2006 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
2007
2008 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
2009 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2010 }
2011
2012 #[test]
2013 fn sglang_default_uses_canonical_model_id() {
2014 let registry = ModelRegistry::default();
2015 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
2016
2017 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2018 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2019 }
2020
2021 #[test]
2022 fn zai_direct_models_resolve_when_provider_hinted() {
2023 let registry = ModelRegistry::default();
2024
2025 let default = registry.resolve(None, Some(ProviderKind::Zai));
2028 assert_eq!(default.resolved.provider, ProviderKind::Zai);
2029 assert_eq!(default.resolved.id, "GLM-5.3");
2030 assert!(default.used_fallback);
2031 assert_eq!(default.fallback_chain, ["provider_default:zai"]);
2032
2033 for (alias, expected) in [
2034 ("GLM-5.1", "GLM-5.1"),
2035 ("glm-5-1", "GLM-5.1"),
2036 ("GLM-5.2", "GLM-5.2"),
2037 ("glm-5.2", "GLM-5.2"),
2038 ("zai-glm-5-2", "GLM-5.2"),
2039 ("GLM-5.3", "GLM-5.3"),
2040 ("glm-5.3", "GLM-5.3"),
2041 ("glm-5-3", "GLM-5.3"),
2042 ("zai-glm-5-3", "GLM-5.3"),
2043 ("GLM-5-Turbo", "GLM-5-Turbo"),
2044 ("glm-5-turbo", "GLM-5-Turbo"),
2045 ("zai-glm-5-turbo", "GLM-5-Turbo"),
2046 ] {
2047 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
2048
2049 assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
2050 assert_eq!(resolved.resolved.id, expected);
2051 assert!(!resolved.used_fallback);
2052 assert!(resolved.resolved.supports_tools);
2053 assert!(resolved.resolved.supports_reasoning);
2054 }
2055 }
2056
2057 #[test]
2058 fn first_party_recent_provider_models_are_listed() {
2059 let registry = ModelRegistry::default();
2060 let models = registry.list();
2061
2062 for (provider, id) in [
2063 (ProviderKind::Zai, "GLM-5.2"),
2064 (ProviderKind::Stepfun, "step-3.7-flash"),
2065 (ProviderKind::Minimax, "MiniMax-M2.1"),
2066 (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
2067 (ProviderKind::Openmodel, "deepseek-v4-flash"),
2068 (ProviderKind::Meta, "muse-spark-1.2"),
2069 (ProviderKind::Xai, "grok-4.6"),
2070 ] {
2071 assert!(
2072 models
2073 .iter()
2074 .any(|model| model.provider == provider && model.id == id),
2075 "expected {provider:?} model {id} in registry"
2076 );
2077 }
2078 }
2079
2080 #[test]
2081 fn opencode_go_lists_only_current_chat_completions_models() {
2082 let registry = ModelRegistry::default();
2083 let listed = registry.list();
2084 let models: Vec<&str> = listed
2085 .iter()
2086 .filter(|model| model.provider == ProviderKind::OpencodeGo)
2087 .map(|model| model.id.as_str())
2088 .collect();
2089
2090 assert_eq!(
2091 models,
2092 vec![
2093 "deepseek-v4-pro",
2094 "grok-4.5",
2095 "glm-5.2",
2096 "glm-5.1",
2097 "kimi-k3",
2098 "kimi-k2.7-code",
2099 "kimi-k2.6",
2100 "deepseek-v4-flash",
2101 "mimo-v2.5",
2102 "mimo-v2.5-pro",
2103 ]
2104 );
2105
2106 let default = registry.resolve(None, Some(ProviderKind::OpencodeGo));
2107 assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
2108 assert_eq!(default.resolved.id, "deepseek-v4-pro");
2109
2110 for model in ["grok-4.5", "kimi-k3"] {
2111 for requested in [model.to_string(), format!("opencode-go/{model}")] {
2112 let resolved = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
2113 assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
2114 assert_eq!(resolved.resolved.id, model);
2115 assert!(!resolved.used_fallback);
2116 }
2117 }
2118
2119 for messages_only in [
2120 "minimax-m3",
2121 "minimax-m2.7",
2122 "minimax-m2.5",
2123 "qwen3.7-max",
2124 "qwen3.7-plus",
2125 "qwen3.6-plus",
2126 ] {
2127 for requested in [
2128 messages_only.to_string(),
2129 format!("opencode-go/{messages_only}"),
2130 ] {
2131 let rejected = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
2132 assert!(rejected.used_fallback, "{requested}");
2133 assert_eq!(
2134 rejected.resolved.provider,
2135 ProviderKind::OpencodeGo,
2136 "{requested} must not cross-route"
2137 );
2138 assert_eq!(rejected.resolved.id, "deepseek-v4-pro", "{requested}");
2139 }
2140 }
2141 }
2142
2143 #[test]
2144 fn xai_grok_models_resolve_when_provider_hinted() {
2145 let registry = ModelRegistry::default();
2146
2147 let default = registry.resolve(None, Some(ProviderKind::Xai));
2148 assert_eq!(default.resolved.provider, ProviderKind::Xai);
2149 assert_eq!(default.resolved.id, "grok-4.6");
2150 assert!(default.used_fallback);
2151
2152 let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
2153 assert_eq!(alias.resolved.provider, ProviderKind::Xai);
2154 assert_eq!(alias.resolved.id, "grok-4.6");
2155 assert!(!alias.used_fallback);
2156
2157 let fast = registry.resolve(
2158 Some("grok-4.20-0309-non-reasoning"),
2159 Some(ProviderKind::Xai),
2160 );
2161 assert_eq!(fast.resolved.provider, ProviderKind::Xai);
2162 assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
2163 assert!(!fast.resolved.supports_reasoning);
2164 }
2165
2166 #[test]
2167 fn meta_muse_spark_resolves_when_provider_hinted() {
2168 let registry = ModelRegistry::default();
2169
2170 let default = registry.resolve(None, Some(ProviderKind::Meta));
2171 assert_eq!(default.resolved.provider, ProviderKind::Meta);
2172 assert_eq!(default.resolved.id, "muse-spark-1.2");
2173 assert!(default.used_fallback);
2174
2175 let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
2176 assert_eq!(alias.resolved.provider, ProviderKind::Meta);
2177 assert_eq!(alias.resolved.id, "muse-spark-1.2");
2178 assert!(!alias.used_fallback);
2179 assert_eq!(model_family("muse-spark-1.2"), ModelFamily::Meta);
2180 }
2181
2182 #[test]
2183 fn openai_gpt56_family_resolves_when_provider_hinted() {
2184 let registry = ModelRegistry::default();
2185 for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
2186 let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
2187 assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
2188 assert_eq!(resolved.resolved.id, model, "{model}");
2189 assert!(resolved.resolved.supports_tools, "{model}");
2190 assert!(resolved.resolved.supports_reasoning, "{model}");
2191 assert!(!resolved.used_fallback, "{model}");
2192 }
2193 }
2194
2195 #[test]
2196 fn grok_ids_stay_in_grok_family() {
2197 assert_eq!(model_family("grok-4.6"), ModelFamily::Grok);
2198 assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
2199 assert_eq!(
2200 model_family("grok-4.20-0309-non-reasoning"),
2201 ModelFamily::Grok
2202 );
2203 }
2204
2205 #[test]
2206 fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
2207 let registry = ModelRegistry::default();
2208
2209 let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
2210 assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
2211 assert_eq!(stepfun.resolved.id, "step-3.7-flash");
2212
2213 for (alias, expected) in [
2214 ("minimax", "MiniMax-M3"),
2215 ("minimax-m3", "MiniMax-M3"),
2216 ("minimax-m2.7", "MiniMax-M2.7"),
2217 ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
2218 ("minimax-m2.1", "MiniMax-M2.1"),
2219 ("minimax-m2", "MiniMax-M2"),
2220 ] {
2221 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
2222
2223 assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
2224 assert_eq!(resolved.resolved.id, expected);
2225 assert!(!resolved.used_fallback);
2226 assert!(resolved.resolved.supports_tools);
2227 assert!(resolved.resolved.supports_reasoning);
2228 }
2229 }
2230
2231 #[test]
2232 fn minimax_anthropic_models_resolve_when_provider_hinted() {
2233 let registry = ModelRegistry::default();
2234
2235 for (alias, expected) in [
2236 ("minimax-anthropic", "MiniMax-M3"),
2237 ("minimax-m3", "MiniMax-M3"),
2238 ("minimax-m2.7", "MiniMax-M2.7"),
2239 ] {
2240 let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
2241
2242 assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
2243 assert_eq!(resolved.resolved.id, expected);
2244 assert!(!resolved.used_fallback);
2245 assert!(resolved.resolved.supports_tools);
2246 assert!(resolved.resolved.supports_reasoning);
2247 }
2248 }
2249
2250 #[test]
2251 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
2252 let registry = ModelRegistry::default();
2253 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
2254
2255 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2256 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2257 }
2258
2259 #[test]
2260 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
2261 let registry = ModelRegistry::default();
2262
2263 for (alias, expected) in [
2264 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
2265 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
2266 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
2267 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
2268 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
2269 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
2270 ("glm-5.1", "z-ai/glm-5.1"),
2271 ("glm-5.2", "z-ai/glm-5.2"),
2272 ("glm-5.3", "z-ai/glm-5.3"),
2273 ("minimax-m3", "minimax/minimax-m3"),
2274 ("minimax-2.7", "minimax/minimax-m2.7"),
2275 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
2276 ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
2277 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
2278 ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
2279 (
2280 "nvidia/nemotron-3-ultra",
2281 "nvidia/nemotron-3-ultra-550b-a55b",
2282 ),
2283 ] {
2284 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
2285
2286 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2287 assert_eq!(resolved.resolved.id, expected);
2288 assert!(resolved.resolved.supports_tools);
2289 assert!(resolved.resolved.supports_reasoning);
2290 }
2291 }
2292
2293 #[test]
2294 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
2295 let registry = ModelRegistry::default();
2296 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
2297
2298 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
2299 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2300 }
2301
2302 #[test]
2303 fn together_inkling_keeps_published_wire_identity() {
2304 let registry = ModelRegistry::default();
2305 for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
2306 let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
2307
2308 assert_eq!(resolved.resolved.provider, ProviderKind::Together);
2309 assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
2310 assert!(resolved.resolved.supports_tools);
2311 assert!(resolved.resolved.supports_reasoning);
2312 assert!(!resolved.used_fallback);
2313 }
2314
2315 let unscoped = registry.resolve(Some("inkling"), None);
2316 assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
2317 assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
2318 assert!(!unscoped.used_fallback);
2319 }
2320
2321 #[test]
2322 fn registry_lists_and_resolves_every_v090_catalog_addition() {
2323 let registry = ModelRegistry::default();
2324 let advertised = [
2325 (ProviderKind::Anthropic, "claude-sonnet-5"),
2326 (ProviderKind::Anthropic, "claude-fable-5"),
2327 (ProviderKind::Openai, "gpt-5.3-codex"),
2328 (ProviderKind::Openai, "gpt-5.5"),
2329 (ProviderKind::Openai, "gpt-5.5-pro"),
2330 (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
2331 (ProviderKind::Arcee, "trinity-mini"),
2332 ];
2333
2334 let listed = registry.list();
2335 for (provider, model_id) in advertised {
2336 assert!(
2337 listed
2338 .iter()
2339 .any(|model| model.provider == provider && model.id == model_id),
2340 "missing {model_id} ({}) from model list",
2341 provider.as_str()
2342 );
2343 let resolved = registry.resolve(Some(model_id), Some(provider));
2344 assert_eq!(resolved.resolved.provider, provider, "{model_id}");
2345 assert_eq!(resolved.resolved.id, model_id, "{model_id}");
2346 assert!(!resolved.used_fallback, "{model_id}");
2347 }
2348 }
2349
2350 #[test]
2351 fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
2352 let registry = ModelRegistry::default();
2353
2354 let unscoped = registry.resolve(Some("gpt-5.5"), None);
2355 assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
2356 assert_eq!(unscoped.resolved.id, "gpt-5.5");
2357 assert!(!unscoped.used_fallback);
2358
2359 let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
2360 assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
2361 assert_eq!(codex.resolved.id, "gpt-5.5");
2362 assert!(!codex.used_fallback);
2363 }
2364
2365 #[test]
2366 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
2367 let registry = ModelRegistry::default();
2368 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
2369
2370 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2371 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2372 }
2373
2374 #[test]
2375 fn vllm_default_uses_canonical_model_id() {
2376 let registry = ModelRegistry::default();
2377 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
2378
2379 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2380 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2381 }
2382
2383 #[test]
2384 fn ollama_default_uses_small_local_model_id() {
2385 let registry = ModelRegistry::default();
2386 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
2387
2388 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2389 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2390 assert!(resolved.resolved.supports_reasoning);
2391 }
2392
2393 #[test]
2394 fn ollama_cloud_default_uses_the_hosted_catalog_model_id() {
2395 let registry = ModelRegistry::default();
2396 let resolved = registry.resolve(None, Some(ProviderKind::OllamaCloud));
2397
2398 assert_eq!(resolved.resolved.provider, ProviderKind::OllamaCloud);
2399 assert_eq!(resolved.resolved.id, "gpt-oss:120b");
2400 assert!(resolved.resolved.supports_reasoning);
2401 }
2402
2403 #[test]
2404 fn ollama_requested_model_tag_is_preserved() {
2405 let registry = ModelRegistry::default();
2406 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
2407
2408 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2409 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
2410 assert!(!resolved.used_fallback);
2411 }
2412
2413 #[test]
2414 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
2415 let registry = ModelRegistry::default();
2416 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
2417
2418 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2419 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2420 }
2421
2422 #[test]
2423 fn preserves_requested_model_casing_for_third_party_providers() {
2424 let registry = ModelRegistry::default();
2425 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
2426
2427 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2428 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2429 }
2430
2431 #[test]
2432 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
2433 let registry = ModelRegistry::default();
2434 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
2435
2436 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2437 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
2439 }
2440
2441 #[test]
2442 fn preserves_requested_model_casing_without_surrounding_whitespace() {
2443 let registry = ModelRegistry::default();
2444 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
2445
2446 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2447 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2448 }
2449
2450 #[test]
2451 fn alias_match_does_not_override_requested_casing() {
2452 let registry = ModelRegistry::default();
2453 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
2454
2455 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2456 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2457 }
2458
2459 #[test]
2460 fn model_family_classifies_known_model_ids() {
2461 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
2462 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
2463 assert_eq!(
2464 model_family("anthropic/claude-opus-4-7"),
2465 ModelFamily::Anthropic
2466 );
2467 assert_eq!(
2468 model_family("meta-llama/llama-3.3-70b-instruct"),
2469 ModelFamily::Meta
2470 );
2471 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
2472 }
2473
2474 #[test]
2475 fn model_family_uses_underlying_model_for_router_ids() {
2476 assert_eq!(
2477 model_family("groq/llama-3.3-70b-versatile"),
2478 ModelFamily::Meta
2479 );
2480 assert_eq!(
2481 model_family("openrouter/openai/gpt-5.4"),
2482 ModelFamily::OpenAI
2483 );
2484 assert_eq!(
2485 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
2486 ModelFamily::DeepSeek
2487 );
2488 }
2489
2490 #[test]
2491 fn model_family_covers_prominent_google_and_mistral_model_names() {
2492 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
2493 assert_eq!(
2494 model_family("mistralai/mixtral-8x22b"),
2495 ModelFamily::Mistral
2496 );
2497 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2498 }
2499
2500 #[test]
2501 fn model_family_falls_back_to_inferencer_for_unknown_models() {
2502 assert_eq!(
2503 model_family("custom-gateway/my-private-model"),
2504 ModelFamily::Inferencer
2505 );
2506 assert_eq!(model_family(""), ModelFamily::Inferencer);
2507 }
2508}