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