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