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: "deepseek-v4-flash".to_string(),
703 provider: ProviderKind::Openmodel,
704 aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
705 supports_tools: true,
706 supports_reasoning: true,
707 },
708 ModelInfo {
710 id: "minimax/minimax-m2.7".to_string(),
711 provider: ProviderKind::Openrouter,
712 aliases: vec![
713 "minimax-2.7".to_string(),
714 "minimax-2-7".to_string(),
715 "openrouter-minimax-2.7".to_string(),
716 ],
717 supports_tools: true,
718 supports_reasoning: true,
719 },
720 ModelInfo {
721 id: "step-3.7-flash".to_string(),
722 provider: ProviderKind::Stepfun,
723 aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
724 supports_tools: true,
725 supports_reasoning: false,
726 },
727 ModelInfo {
728 id: "MiniMax-M3".to_string(),
729 provider: ProviderKind::Minimax,
730 aliases: vec![
731 "minimax".to_string(),
732 "minimax-m3".to_string(),
733 "minimax-m-3".to_string(),
734 ],
735 supports_tools: true,
736 supports_reasoning: true,
737 },
738 ModelInfo {
739 id: "MiniMax-M2.7".to_string(),
740 provider: ProviderKind::Minimax,
741 aliases: vec![
742 "minimax-m2.7".to_string(),
743 "minimax-m2-7".to_string(),
744 "minimax-m-2.7".to_string(),
745 "minimax-m-2-7".to_string(),
746 ],
747 supports_tools: true,
748 supports_reasoning: true,
749 },
750 ModelInfo {
751 id: "MiniMax-M2.7-highspeed".to_string(),
752 provider: ProviderKind::Minimax,
753 aliases: vec![
754 "minimax-m2.7-highspeed".to_string(),
755 "minimax-m2-7-highspeed".to_string(),
756 "minimax-m-2.7-highspeed".to_string(),
757 "minimax-m-2-7-highspeed".to_string(),
758 ],
759 supports_tools: true,
760 supports_reasoning: true,
761 },
762 ModelInfo {
763 id: "MiniMax-M2.5".to_string(),
764 provider: ProviderKind::Minimax,
765 aliases: vec![
766 "minimax-m2.5".to_string(),
767 "minimax-m2-5".to_string(),
768 "minimax-m-2.5".to_string(),
769 "minimax-m-2-5".to_string(),
770 ],
771 supports_tools: true,
772 supports_reasoning: true,
773 },
774 ModelInfo {
775 id: "MiniMax-M2.5-highspeed".to_string(),
776 provider: ProviderKind::Minimax,
777 aliases: vec![
778 "minimax-m2.5-highspeed".to_string(),
779 "minimax-m2-5-highspeed".to_string(),
780 "minimax-m-2.5-highspeed".to_string(),
781 "minimax-m-2-5-highspeed".to_string(),
782 ],
783 supports_tools: true,
784 supports_reasoning: true,
785 },
786 ModelInfo {
787 id: "MiniMax-M2.1".to_string(),
788 provider: ProviderKind::Minimax,
789 aliases: vec![
790 "minimax-m2.1".to_string(),
791 "minimax-m2-1".to_string(),
792 "minimax-m-2.1".to_string(),
793 "minimax-m-2-1".to_string(),
794 ],
795 supports_tools: true,
796 supports_reasoning: true,
797 },
798 ModelInfo {
799 id: "MiniMax-M2.1-highspeed".to_string(),
800 provider: ProviderKind::Minimax,
801 aliases: vec![
802 "minimax-m2.1-highspeed".to_string(),
803 "minimax-m2-1-highspeed".to_string(),
804 "minimax-m-2.1-highspeed".to_string(),
805 "minimax-m-2-1-highspeed".to_string(),
806 ],
807 supports_tools: true,
808 supports_reasoning: true,
809 },
810 ModelInfo {
811 id: "MiniMax-M2".to_string(),
812 provider: ProviderKind::Minimax,
813 aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
814 supports_tools: true,
815 supports_reasoning: true,
816 },
817 ModelInfo {
819 id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
820 provider: ProviderKind::Openrouter,
821 aliases: vec![
822 "nvidia/nemotron-3-ultra".to_string(),
823 "nemotron-3-ultra".to_string(),
824 "nemotron-3-ultra-550b-a55b".to_string(),
825 "nvidia-nemotron-3-ultra".to_string(),
826 "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
827 ],
828 supports_tools: true,
829 supports_reasoning: true,
830 },
831 ModelInfo {
833 id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
834 provider: ProviderKind::Deepinfra,
835 aliases: vec![
836 "deepseek-v4-pro".to_string(),
837 "di-deepseek-v4-pro".to_string(),
838 ],
839 supports_tools: true,
840 supports_reasoning: true,
841 },
842 ModelInfo {
843 id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
844 provider: ProviderKind::Deepinfra,
845 aliases: vec![
846 "deepseek-v4-flash".to_string(),
847 "di-deepseek-v4-flash".to_string(),
848 ],
849 supports_tools: true,
850 supports_reasoning: true,
851 },
852 ModelInfo {
854 id: "fugu".to_string(),
855 provider: ProviderKind::Sakana,
856 aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
857 supports_tools: true,
858 supports_reasoning: false,
859 },
860 ModelInfo {
861 id: "fugu-ultra-20260615".to_string(),
862 provider: ProviderKind::Sakana,
863 aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
864 supports_tools: true,
865 supports_reasoning: true,
866 },
867 ];
868 Self::new(models)
869 }
870}
871
872impl ModelRegistry {
873 #[must_use]
879 pub fn new(models: Vec<ModelInfo>) -> Self {
880 let mut alias_map = HashMap::new();
881 for (idx, model) in models.iter().enumerate() {
882 alias_map.entry(normalize(&model.id)).or_insert(idx);
883 for alias in &model.aliases {
884 alias_map.entry(normalize(alias)).or_insert(idx);
885 }
886 }
887 Self { models, alias_map }
888 }
889
890 #[must_use]
892 pub fn list(&self) -> Vec<ModelInfo> {
893 self.models.clone()
894 }
895
896 #[must_use]
908 pub fn resolve(
909 &self,
910 requested: Option<&str>,
911 provider_hint: Option<ProviderKind>,
912 ) -> ModelResolution {
913 let mut fallback_chain = Vec::new();
914
915 if let Some(name) = requested {
916 fallback_chain.push(format!("requested:{name}"));
917 if provider_hint == Some(ProviderKind::Ollama) {
918 return ModelResolution {
919 requested: Some(name.to_string()),
920 resolved: ModelInfo {
921 id: name.trim().to_string(),
922 provider: ProviderKind::Ollama,
923 aliases: Vec::new(),
924 supports_tools: true,
925 supports_reasoning: false,
926 },
927 used_fallback: false,
928 fallback_chain,
929 };
930 }
931 if let Some(provider) = provider_hint
932 && let Some(model) = self
933 .models
934 .iter()
935 .find(|m| m.provider == provider && model_matches(m, name))
936 .cloned()
937 {
938 return ModelResolution {
939 requested: Some(name.to_string()),
940 resolved: model,
941 used_fallback: false,
942 fallback_chain,
943 };
944 }
945 if provider_hint == Some(ProviderKind::Atlascloud)
946 && let Some(model) = atlascloud_passthrough_model(name)
947 {
948 return ModelResolution {
949 requested: Some(name.to_string()),
950 resolved: model,
951 used_fallback: false,
952 fallback_chain,
953 };
954 }
955 if provider_hint == Some(ProviderKind::Arcee)
956 && let Some(model) = arcee_passthrough_model(name)
957 {
958 return ModelResolution {
959 requested: Some(name.to_string()),
960 resolved: model,
961 used_fallback: false,
962 fallback_chain,
963 };
964 }
965 if provider_hint == Some(ProviderKind::XiaomiMimo)
966 && let Some(model) = xiaomi_mimo_passthrough_model(name)
967 {
968 return ModelResolution {
969 requested: Some(name.to_string()),
970 resolved: model,
971 used_fallback: false,
972 fallback_chain,
973 };
974 }
975 if let Some(idx) = self.alias_map.get(&normalize(name)) {
976 return ModelResolution {
977 requested: Some(name.to_string()),
978 resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
979 used_fallback: false,
980 fallback_chain,
981 };
982 }
983 }
984
985 let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
986 fallback_chain.push(format!("provider_default:{}", provider.as_str()));
987 if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
988 return ModelResolution {
989 requested: requested.map(ToOwned::to_owned),
990 resolved: model,
991 used_fallback: true,
992 fallback_chain,
993 };
994 }
995
996 let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
997 id: "deepseek-v4-pro".to_string(),
998 provider: ProviderKind::Deepseek,
999 aliases: Vec::new(),
1000 supports_tools: true,
1001 supports_reasoning: true,
1002 });
1003 fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1004 ModelResolution {
1005 requested: requested.map(ToOwned::to_owned),
1006 resolved: final_fallback,
1007 used_fallback: true,
1008 fallback_chain,
1009 }
1010 }
1011}
1012
1013fn normalize(value: &str) -> String {
1014 value.trim().to_ascii_lowercase()
1015}
1016
1017#[must_use]
1018pub fn model_family(model_id: &str) -> ModelFamily {
1020 let normalized = normalize(model_id);
1021 if normalized.is_empty() {
1022 return ModelFamily::Inferencer;
1023 }
1024
1025 if normalized.contains("deepseek") {
1026 return ModelFamily::DeepSeek;
1027 }
1028 if normalized.contains("claude") || normalized.contains("anthropic") {
1029 return ModelFamily::Anthropic;
1030 }
1031 if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1032 return ModelFamily::GptOss;
1033 }
1034 if normalized.starts_with("gpt-")
1035 || normalized.contains("/gpt-")
1036 || normalized.contains("openai/")
1037 {
1038 return ModelFamily::OpenAI;
1039 }
1040 if normalized.contains("gemini")
1041 || normalized.contains("gemma")
1042 || normalized.contains("google/")
1043 {
1044 return ModelFamily::Google;
1045 }
1046 if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/")
1047 {
1048 return ModelFamily::Meta;
1049 }
1050 if normalized.contains("mistral")
1051 || normalized.contains("mixtral")
1052 || normalized.contains("codestral")
1053 {
1054 return ModelFamily::Mistral;
1055 }
1056 if normalized.contains("qwen") {
1057 return ModelFamily::Qwen;
1058 }
1059 if normalized.contains("grok") {
1060 return ModelFamily::Grok;
1061 }
1062 if normalized.contains("cohere") || normalized.contains("command-r") {
1063 return ModelFamily::Cohere;
1064 }
1065
1066 ModelFamily::Inferencer
1067}
1068
1069fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1070 let requested = normalize(requested);
1071 normalize(&model.id) == requested
1072 || model
1073 .aliases
1074 .iter()
1075 .any(|alias| normalize(alias) == requested)
1076}
1077
1078fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1079 let requested = requested.trim();
1080 if model.id.eq_ignore_ascii_case(requested) {
1081 model.id = requested.to_string();
1082 }
1083 model
1084}
1085
1086fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1087 let requested = requested.trim();
1088 if requested.is_empty() || !requested.contains('/') {
1089 return None;
1090 }
1091
1092 Some(ModelInfo {
1093 id: requested.to_string(),
1094 provider: ProviderKind::Atlascloud,
1095 aliases: Vec::new(),
1096 supports_tools: true,
1097 supports_reasoning: true,
1098 })
1099}
1100
1101fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1102 let requested = requested.trim();
1103 if requested.is_empty() {
1104 return None;
1105 }
1106 let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1107
1108 Some(ModelInfo {
1109 id: requested.to_string(),
1110 provider: ProviderKind::Arcee,
1111 aliases: Vec::new(),
1112 supports_tools: true,
1113 supports_reasoning,
1114 })
1115}
1116
1117fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1118 let requested = requested.trim();
1119 if requested.is_empty() || requested.chars().any(char::is_control) {
1120 return None;
1121 }
1122
1123 Some(ModelInfo {
1124 id: requested.to_string(),
1125 provider: ProviderKind::XiaomiMimo,
1126 aliases: Vec::new(),
1127 supports_tools: true,
1128 supports_reasoning: true,
1129 })
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135
1136 #[test]
1137 fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1138 let registry = ModelRegistry::default();
1139 let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1140
1141 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1142 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1143 }
1144
1145 #[test]
1146 fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1147 let registry = ModelRegistry::default();
1148 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1149
1150 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1151 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1152 }
1153
1154 #[test]
1155 fn nvidia_nim_default_uses_catalog_model_id() {
1156 let registry = ModelRegistry::default();
1157 let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1158
1159 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1160 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1161 }
1162
1163 #[test]
1164 fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1165 let registry = ModelRegistry::default();
1166 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1167
1168 assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1169 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1170 }
1171
1172 #[test]
1173 fn atlascloud_default_uses_namespaced_model_id() {
1174 let registry = ModelRegistry::default();
1175 let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1176
1177 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1178 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1179 assert!(resolved.resolved.supports_reasoning);
1180 }
1181
1182 #[test]
1183 fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1184 let registry = ModelRegistry::default();
1185 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1186
1187 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1188 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1189 }
1190
1191 #[test]
1192 fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1193 let registry = ModelRegistry::default();
1194 let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1195
1196 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1197 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1198 }
1199
1200 #[test]
1201 fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1202 let registry = ModelRegistry::default();
1203 let resolved =
1204 registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1205
1206 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1207 assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1208 assert!(resolved.resolved.supports_tools);
1209 assert!(resolved.resolved.supports_reasoning);
1210 assert!(!resolved.used_fallback);
1211 }
1212
1213 #[test]
1214 fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1215 let registry = ModelRegistry::default();
1216 let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1217
1218 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1219 assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1220 assert!(!resolved.used_fallback);
1221 }
1222
1223 #[test]
1224 fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1225 let registry = ModelRegistry::default();
1226 let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1227
1228 assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1229 assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1230 assert!(resolved.used_fallback);
1231 }
1232
1233 #[test]
1234 fn openrouter_default_uses_namespaced_model_id() {
1235 let registry = ModelRegistry::default();
1236 let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1237
1238 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1239 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1240 }
1241
1242 #[test]
1243 fn xiaomi_mimo_default_uses_canonical_model_id() {
1244 let registry = ModelRegistry::default();
1245 let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1246
1247 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1248 assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1249 assert!(resolved.resolved.supports_reasoning);
1250 }
1251
1252 #[test]
1253 fn moonshot_default_and_aliases_use_kimi_k27_code() {
1254 let registry = ModelRegistry::default();
1255
1256 for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1257 let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1258
1259 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1260 assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1261 assert!(resolved.resolved.supports_tools);
1262 assert!(resolved.resolved.supports_reasoning);
1263 }
1264 }
1265
1266 #[test]
1267 fn moonshot_explicit_kimi_k26_remains_available() {
1268 let registry = ModelRegistry::default();
1269 let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1270
1271 assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1272 assert_eq!(resolved.resolved.id, "kimi-k2.6");
1273 assert!(resolved.resolved.supports_reasoning);
1274 }
1275
1276 #[test]
1277 fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1278 let registry = ModelRegistry::default();
1279 let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1280 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1281 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1282 assert!(!resolved.resolved.supports_tools);
1283 assert!(!resolved.resolved.supports_reasoning);
1284
1285 let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1286 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1287
1288 let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1289 assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1290 }
1291
1292 #[test]
1293 fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1294 let registry = ModelRegistry::default();
1295
1296 let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1297 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1298 assert_eq!(resolved.resolved.id, "mimo-v2.5");
1299 assert!(resolved.resolved.supports_tools);
1300 }
1301
1302 #[test]
1303 fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1304 let registry = ModelRegistry::default();
1305 let resolved =
1306 registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1307
1308 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1309 assert_eq!(resolved.resolved.id, "account-custom-mimo");
1310 assert!(!resolved.used_fallback);
1311 }
1312
1313 #[test]
1314 fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1315 let registry = ModelRegistry::default();
1316 let resolved = registry.resolve(
1317 Some("deepseek/deepseek-v4-pro"),
1318 Some(ProviderKind::XiaomiMimo),
1319 );
1320
1321 assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1322 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1323 assert!(!resolved.used_fallback);
1324 }
1325
1326 #[test]
1327 fn wanjie_ark_default_uses_reasoner_model_id() {
1328 let registry = ModelRegistry::default();
1329 let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1330
1331 assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1332 assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1333 assert!(resolved.resolved.supports_reasoning);
1334 }
1335
1336 #[test]
1337 fn novita_default_uses_namespaced_model_id() {
1338 let registry = ModelRegistry::default();
1339 let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1340
1341 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1342 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1343 }
1344
1345 #[test]
1346 fn fireworks_default_uses_canonical_model_id() {
1347 let registry = ModelRegistry::default();
1348 let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1349
1350 assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1351 assert_eq!(
1352 resolved.resolved.id,
1353 "accounts/fireworks/models/deepseek-v4-pro"
1354 );
1355 }
1356
1357 #[test]
1358 fn siliconflow_default_uses_canonical_pro_model_id() {
1359 let registry = ModelRegistry::default();
1360 let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1361
1362 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1363 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1364 assert!(resolved.resolved.supports_reasoning);
1365 }
1366
1367 #[test]
1368 fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1369 let registry = ModelRegistry::default();
1370 let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1371
1372 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1373 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1374 assert!(resolved.resolved.supports_reasoning);
1375 }
1376
1377 #[test]
1378 fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1379 let registry = ModelRegistry::default();
1380 let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1381
1382 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1383 assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1384 assert!(resolved.resolved.supports_reasoning);
1385 }
1386
1387 #[test]
1388 fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1389 let registry = ModelRegistry::default();
1390 let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1391
1392 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1393 assert_eq!(resolved.resolved.id, "trinity-mini");
1394 assert!(!resolved.resolved.supports_reasoning);
1395 }
1396
1397 #[test]
1398 fn arcee_provider_hint_preserves_explicit_future_model_id() {
1399 let registry = ModelRegistry::default();
1400 let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1401
1402 assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1403 assert_eq!(resolved.resolved.id, "trinity-large-next");
1404 assert!(!resolved.resolved.supports_reasoning);
1405 assert!(!resolved.used_fallback);
1406 }
1407
1408 #[test]
1409 fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1410 let registry = ModelRegistry::default();
1411 let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1412
1413 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1414 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1415 }
1416
1417 #[test]
1418 fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1419 let registry = ModelRegistry::default();
1420 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1421
1422 assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1423 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1424 }
1425
1426 #[test]
1427 fn sglang_default_uses_canonical_model_id() {
1428 let registry = ModelRegistry::default();
1429 let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1430
1431 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1432 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1433 }
1434
1435 #[test]
1436 fn zai_direct_models_resolve_when_provider_hinted() {
1437 let registry = ModelRegistry::default();
1438
1439 let default = registry.resolve(None, Some(ProviderKind::Zai));
1441 assert_eq!(default.resolved.provider, ProviderKind::Zai);
1442 assert_eq!(default.resolved.id, "GLM-5.2");
1443
1444 for (alias, expected) in [
1445 ("GLM-5.1", "GLM-5.1"),
1446 ("glm-5-1", "GLM-5.1"),
1447 ("GLM-5.2", "GLM-5.2"),
1448 ("glm-5.2", "GLM-5.2"),
1449 ("zai-glm-5-2", "GLM-5.2"),
1450 ("GLM-5-Turbo", "GLM-5-Turbo"),
1451 ("glm-5-turbo", "GLM-5-Turbo"),
1452 ("zai-glm-5-turbo", "GLM-5-Turbo"),
1453 ] {
1454 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
1455
1456 assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
1457 assert_eq!(resolved.resolved.id, expected);
1458 assert!(!resolved.used_fallback);
1459 assert!(resolved.resolved.supports_tools);
1460 assert!(resolved.resolved.supports_reasoning);
1461 }
1462 }
1463
1464 #[test]
1465 fn first_party_recent_provider_models_are_listed() {
1466 let registry = ModelRegistry::default();
1467 let models = registry.list();
1468
1469 for (provider, id) in [
1470 (ProviderKind::Zai, "GLM-5.2"),
1471 (ProviderKind::Stepfun, "step-3.7-flash"),
1472 (ProviderKind::Minimax, "MiniMax-M2.1"),
1473 (ProviderKind::Openmodel, "deepseek-v4-flash"),
1474 ] {
1475 assert!(
1476 models
1477 .iter()
1478 .any(|model| model.provider == provider && model.id == id),
1479 "expected {provider:?} model {id} in registry"
1480 );
1481 }
1482 }
1483
1484 #[test]
1485 fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
1486 let registry = ModelRegistry::default();
1487
1488 let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
1489 assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
1490 assert_eq!(stepfun.resolved.id, "step-3.7-flash");
1491
1492 for (alias, expected) in [
1493 ("minimax", "MiniMax-M3"),
1494 ("minimax-m3", "MiniMax-M3"),
1495 ("minimax-m2.7", "MiniMax-M2.7"),
1496 ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
1497 ("minimax-m2.1", "MiniMax-M2.1"),
1498 ("minimax-m2", "MiniMax-M2"),
1499 ] {
1500 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
1501
1502 assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
1503 assert_eq!(resolved.resolved.id, expected);
1504 assert!(!resolved.used_fallback);
1505 assert!(resolved.resolved.supports_tools);
1506 assert!(resolved.resolved.supports_reasoning);
1507 }
1508 }
1509
1510 #[test]
1511 fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1512 let registry = ModelRegistry::default();
1513 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1514
1515 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1516 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1517 }
1518
1519 #[test]
1520 fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1521 let registry = ModelRegistry::default();
1522
1523 for (alias, expected) in [
1524 ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1525 ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1526 ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1527 ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1528 ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1529 ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1530 ("glm-5.1", "z-ai/glm-5.1"),
1531 ("glm-5.2", "z-ai/glm-5.2"),
1532 ("minimax-m3", "minimax/minimax-m3"),
1533 ("minimax-2.7", "minimax/minimax-m2.7"),
1534 ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1535 ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
1536 ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1537 ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
1538 (
1539 "nvidia/nemotron-3-ultra",
1540 "nvidia/nemotron-3-ultra-550b-a55b",
1541 ),
1542 ] {
1543 let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1544
1545 assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1546 assert_eq!(resolved.resolved.id, expected);
1547 assert!(resolved.resolved.supports_tools);
1548 assert!(resolved.resolved.supports_reasoning);
1549 }
1550 }
1551
1552 #[test]
1553 fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
1554 let registry = ModelRegistry::default();
1555 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
1556
1557 assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1558 assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1559 }
1560
1561 #[test]
1562 fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
1563 let registry = ModelRegistry::default();
1564 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
1565
1566 assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1567 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1568 }
1569
1570 #[test]
1571 fn vllm_default_uses_canonical_model_id() {
1572 let registry = ModelRegistry::default();
1573 let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
1574
1575 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1576 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1577 }
1578
1579 #[test]
1580 fn ollama_default_uses_small_local_model_id() {
1581 let registry = ModelRegistry::default();
1582 let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
1583
1584 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1585 assert_eq!(resolved.resolved.id, "deepseek-coder:1.3b");
1586 assert!(!resolved.resolved.supports_reasoning);
1587 }
1588
1589 #[test]
1590 fn ollama_requested_model_tag_is_preserved() {
1591 let registry = ModelRegistry::default();
1592 let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
1593
1594 assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1595 assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
1596 assert!(!resolved.used_fallback);
1597 }
1598
1599 #[test]
1600 fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
1601 let registry = ModelRegistry::default();
1602 let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
1603
1604 assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1605 assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1606 }
1607
1608 #[test]
1609 fn preserves_requested_model_casing_for_third_party_providers() {
1610 let registry = ModelRegistry::default();
1611 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
1612
1613 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1614 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1615 }
1616
1617 #[test]
1618 fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
1619 let registry = ModelRegistry::default();
1620 let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
1621
1622 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1623 assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1625 }
1626
1627 #[test]
1628 fn preserves_requested_model_casing_without_surrounding_whitespace() {
1629 let registry = ModelRegistry::default();
1630 let resolved = registry.resolve(Some(" DeepSeek-V4-Pro "), None);
1631
1632 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1633 assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1634 }
1635
1636 #[test]
1637 fn alias_match_does_not_override_requested_casing() {
1638 let registry = ModelRegistry::default();
1639 let resolved = registry.resolve(Some("deepseek-reasoner"), None);
1640
1641 assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1642 assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1643 }
1644
1645 #[test]
1646 fn model_family_classifies_known_model_ids() {
1647 assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
1648 assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
1649 assert_eq!(
1650 model_family("anthropic/claude-opus-4-7"),
1651 ModelFamily::Anthropic
1652 );
1653 assert_eq!(
1654 model_family("meta-llama/llama-3.3-70b-instruct"),
1655 ModelFamily::Meta
1656 );
1657 assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
1658 }
1659
1660 #[test]
1661 fn model_family_uses_underlying_model_for_router_ids() {
1662 assert_eq!(
1663 model_family("groq/llama-3.3-70b-versatile"),
1664 ModelFamily::Meta
1665 );
1666 assert_eq!(
1667 model_family("openrouter/openai/gpt-5.4"),
1668 ModelFamily::OpenAI
1669 );
1670 assert_eq!(
1671 model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
1672 ModelFamily::DeepSeek
1673 );
1674 }
1675
1676 #[test]
1677 fn model_family_covers_prominent_google_and_mistral_model_names() {
1678 assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
1679 assert_eq!(
1680 model_family("mistralai/mixtral-8x22b"),
1681 ModelFamily::Mistral
1682 );
1683 assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
1684 }
1685
1686 #[test]
1687 fn model_family_falls_back_to_inferencer_for_unknown_models() {
1688 assert_eq!(
1689 model_family("custom-gateway/my-private-model"),
1690 ModelFamily::Inferencer
1691 );
1692 assert_eq!(model_family(""), ModelFamily::Inferencer);
1693 }
1694}