Skip to main content

codewhale_agent/
lib.rs

1use std::collections::HashMap;
2
3use codewhale_config::ProviderKind;
4use serde::{Deserialize, Serialize};
5
6/// High-level model family used for shared identity affordances across clients.
7#[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/// Metadata for a single model entry in the registry.
23///
24/// Each model has a canonical `id` used by the provider, a list of `aliases`
25/// that users may reference, and capability flags indicating whether the model
26/// supports tool use and reasoning.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ModelInfo {
29    /// The canonical model identifier used by the provider (e.g. `"deepseek-v4-pro"`).
30    pub id: String,
31    /// The provider that serves this model.
32    pub provider: ProviderKind,
33    /// Alternative names that users can use to reference this model (case-insensitive).
34    pub aliases: Vec<String>,
35    /// Whether this model supports tool/function calling.
36    pub supports_tools: bool,
37    /// Whether this model supports extended reasoning.
38    pub supports_reasoning: bool,
39}
40
41/// The result of resolving a user-requested model name to a concrete model entry.
42///
43/// Contains the resolved [`ModelInfo`], whether a fallback was used, and the
44/// chain of resolution strategies that were attempted.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelResolution {
47    /// The original model name requested by the user, if any.
48    pub requested: Option<String>,
49    /// The concrete model that was resolved.
50    pub resolved: ModelInfo,
51    /// Whether a fallback was used because the requested model was not found.
52    pub used_fallback: bool,
53    /// The ordered list of resolution strategies that were attempted.
54    pub fallback_chain: Vec<String>,
55}
56
57/// A registry of supported models and their aliases, used to resolve user-facing
58/// model names to concrete provider-specific model entries.
59///
60/// The default registry is populated with all built-in models across supported
61/// providers (DeepSeek, NVIDIA NIM, OpenAI-compatible, and others).
62#[derive(Debug, Clone)]
63pub struct ModelRegistry {
64    models: Vec<ModelInfo>,
65    alias_map: HashMap<String, usize>,
66}
67
68/// Creates a registry pre-populated with all built-in models and their aliases.
69impl 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            // Together AI provider models
641            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            // Qwen 3.7 Max (OpenRouter)
663            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            // OpenAI Codex (ChatGPT OAuth) models
671            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            // Anthropic native Messages API models (#3014)
679            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            // OpenModel Anthropic-compatible Messages route
701            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            // MiniMax 2.7 (OpenRouter)
709            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            // NVIDIA Nemotron 3 Ultra (OpenRouter)
818            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            // DeepInfra (https://deepinfra.com)
832            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            // Sakana AI Fugu (https://api.sakana.ai)
853            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            // Meituan LongCat (https://longcat.chat/platform)
868            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    /// Creates a new registry from a list of [`ModelInfo`] entries.
882    ///
883    /// Builds an internal alias map for fast lookup by model id or alias.
884    /// If multiple models share the same id or alias, the first one registered
885    /// takes priority.
886    #[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    /// Returns a clone of all models in the registry.
899    #[must_use]
900    pub fn list(&self) -> Vec<ModelInfo> {
901        self.models.clone()
902    }
903
904    /// Resolves a user-requested model name to a concrete [`ModelInfo`].
905    ///
906    /// Resolution follows this priority order:
907    /// 1. If the provider is Ollama, the requested name is used as-is (to
908    ///    support arbitrary local model tags like `qwen2.5-coder:7b`).
909    /// 2. If a `provider_hint` is given, search for a model matching that
910    ///    provider whose id or alias matches the request (case-insensitive).
911    /// 3. Look up the alias map for a case-insensitive match.
912    /// 4. Fall back to the first model belonging to the hinted provider
913    ///    (or DeepSeek if no hint was given).
914    /// 5. As a last resort, fall back to the first model in the registry.
915    #[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]
1026/// Classify a model identifier by its underlying model family.
1027pub 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        // GLM-5.2 is now the default direct Z.AI model.
1448        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        // Registry's canonical id is used even when user provides different casing
1632        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}