Skip to main content

codewhale_agent/
lib.rs

1use std::collections::HashMap;
2
3use codewhale_config::{ProviderKind, opencode_go_chat_model_id};
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            // OpenAI public API models carried by the bundled catalog.
131            ModelInfo {
132                id: "gpt-5.3-codex".to_string(),
133                provider: ProviderKind::Openai,
134                aliases: vec!["gpt53-codex".to_string()],
135                supports_tools: true,
136                supports_reasoning: true,
137            },
138            ModelInfo {
139                id: "gpt-5.5".to_string(),
140                provider: ProviderKind::Openai,
141                aliases: vec!["openai-gpt-5.5".to_string()],
142                supports_tools: true,
143                supports_reasoning: true,
144            },
145            ModelInfo {
146                id: "gpt-5.5-pro".to_string(),
147                provider: ProviderKind::Openai,
148                aliases: vec!["openai-gpt-5.5-pro".to_string()],
149                supports_tools: true,
150                supports_reasoning: true,
151            },
152            // OpenAI public API GPT-5.6 family.
153            ModelInfo {
154                id: "gpt-5.6".to_string(),
155                provider: ProviderKind::Openai,
156                aliases: vec!["gpt56".to_string()],
157                supports_tools: true,
158                supports_reasoning: true,
159            },
160            ModelInfo {
161                id: "gpt-5.6-sol".to_string(),
162                provider: ProviderKind::Openai,
163                aliases: vec!["gpt56-sol".to_string()],
164                supports_tools: true,
165                supports_reasoning: true,
166            },
167            ModelInfo {
168                id: "gpt-5.6-terra".to_string(),
169                provider: ProviderKind::Openai,
170                aliases: vec!["gpt56-terra".to_string()],
171                supports_tools: true,
172                supports_reasoning: true,
173            },
174            ModelInfo {
175                id: "gpt-5.6-luna".to_string(),
176                provider: ProviderKind::Openai,
177                aliases: vec!["gpt56-luna".to_string()],
178                supports_tools: true,
179                supports_reasoning: true,
180            },
181            ModelInfo {
182                id: "deepseek-ai/deepseek-v4-flash".to_string(),
183                provider: ProviderKind::Atlascloud,
184                aliases: vec![
185                    "deepseek-v4-flash".to_string(),
186                    "atlascloud-deepseek-v4-flash".to_string(),
187                ],
188                supports_tools: true,
189                supports_reasoning: true,
190            },
191            ModelInfo {
192                id: "deepseek-ai/deepseek-v4-pro".to_string(),
193                provider: ProviderKind::Atlascloud,
194                aliases: vec![
195                    "deepseek-v4-pro".to_string(),
196                    "atlascloud-deepseek-v4-pro".to_string(),
197                ],
198                supports_tools: true,
199                supports_reasoning: true,
200            },
201            ModelInfo {
202                id: "deepseek-reasoner".to_string(),
203                provider: ProviderKind::WanjieArk,
204                aliases: vec![
205                    "wanjie-deepseek-reasoner".to_string(),
206                    "ark-wanjie-deepseek-reasoner".to_string(),
207                ],
208                supports_tools: true,
209                supports_reasoning: true,
210            },
211            ModelInfo {
212                id: "DeepSeek-V4-Pro".to_string(),
213                provider: ProviderKind::Volcengine,
214                aliases: vec![
215                    "deepseek-v4-pro".to_string(),
216                    "volcengine-deepseek-v4-pro".to_string(),
217                    "ark-deepseek-v4-pro".to_string(),
218                ],
219                supports_tools: true,
220                supports_reasoning: true,
221            },
222            ModelInfo {
223                id: "DeepSeek-V4-Flash".to_string(),
224                provider: ProviderKind::Volcengine,
225                aliases: vec![
226                    "deepseek-v4-flash".to_string(),
227                    "deepseek-chat".to_string(),
228                    "volcengine-deepseek-v4-flash".to_string(),
229                    "ark-deepseek-v4-flash".to_string(),
230                ],
231                supports_tools: true,
232                supports_reasoning: true,
233            },
234            ModelInfo {
235                id: "trinity-large-thinking".to_string(),
236                provider: ProviderKind::Arcee,
237                aliases: vec![
238                    "trinity".to_string(),
239                    "arcee-trinity".to_string(),
240                    "arcee-trinity-large-thinking".to_string(),
241                ],
242                supports_tools: true,
243                supports_reasoning: true,
244            },
245            ModelInfo {
246                id: "trinity-mini".to_string(),
247                provider: ProviderKind::Arcee,
248                aliases: vec!["arcee-trinity-mini".to_string()],
249                supports_tools: true,
250                supports_reasoning: true,
251            },
252            ModelInfo {
253                id: "deepseek/deepseek-v4-pro".to_string(),
254                provider: ProviderKind::Openrouter,
255                aliases: vec![
256                    "deepseek-v4-pro".to_string(),
257                    "openrouter-deepseek-v4-pro".to_string(),
258                ],
259                supports_tools: true,
260                supports_reasoning: true,
261            },
262            ModelInfo {
263                id: "deepseek/deepseek-v4-flash".to_string(),
264                provider: ProviderKind::Openrouter,
265                aliases: vec![
266                    "deepseek-v4-flash".to_string(),
267                    "deepseek-chat".to_string(),
268                    "deepseek-reasoner".to_string(),
269                    "openrouter-deepseek-v4-flash".to_string(),
270                ],
271                supports_tools: true,
272                supports_reasoning: true,
273            },
274            ModelInfo {
275                id: "deepseek/deepseek-v4-pro".to_string(),
276                provider: ProviderKind::Orcarouter,
277                aliases: vec!["orcarouter-deepseek-v4-pro".to_string()],
278                supports_tools: true,
279                supports_reasoning: true,
280            },
281            ModelInfo {
282                id: "deepseek/deepseek-v4-flash".to_string(),
283                provider: ProviderKind::Orcarouter,
284                aliases: vec!["orcarouter-deepseek-v4-flash".to_string()],
285                supports_tools: true,
286                supports_reasoning: true,
287            },
288            ModelInfo {
289                id: "orcarouter/auto".to_string(),
290                provider: ProviderKind::Orcarouter,
291                aliases: vec!["orcarouter-auto".to_string()],
292                supports_tools: true,
293                supports_reasoning: true,
294            },
295            ModelInfo {
296                id: "arcee-ai/trinity-large-thinking".to_string(),
297                provider: ProviderKind::Openrouter,
298                aliases: vec![
299                    "trinity".to_string(),
300                    "trinity-large-thinking".to_string(),
301                    "arcee-trinity-large-thinking".to_string(),
302                ],
303                supports_tools: true,
304                supports_reasoning: true,
305            },
306            ModelInfo {
307                id: "xiaomi/mimo-v2.5-pro".to_string(),
308                provider: ProviderKind::Openrouter,
309                aliases: vec![
310                    "openrouter-mimo-v2.5-pro".to_string(),
311                    "openrouter-xiaomi-mimo-v2.5-pro".to_string(),
312                ],
313                supports_tools: true,
314                supports_reasoning: true,
315            },
316            ModelInfo {
317                id: "xiaomi/mimo-v2.5".to_string(),
318                provider: ProviderKind::Openrouter,
319                aliases: vec![
320                    "openrouter-mimo-v2.5".to_string(),
321                    "openrouter-xiaomi-mimo-v2.5".to_string(),
322                ],
323                supports_tools: true,
324                supports_reasoning: true,
325            },
326            ModelInfo {
327                id: "qwen/qwen3.6-flash".to_string(),
328                provider: ProviderKind::Openrouter,
329                aliases: vec!["qwen3.6-flash".to_string(), "qwen-3.6-flash".to_string()],
330                supports_tools: true,
331                supports_reasoning: true,
332            },
333            ModelInfo {
334                id: "qwen/qwen3.6-35b-a3b".to_string(),
335                provider: ProviderKind::Openrouter,
336                aliases: vec![
337                    "qwen3.6-35b-a3b".to_string(),
338                    "qwen-3.6-35b-a3b".to_string(),
339                ],
340                supports_tools: true,
341                supports_reasoning: true,
342            },
343            ModelInfo {
344                id: "qwen/qwen3.6-max-preview".to_string(),
345                provider: ProviderKind::Openrouter,
346                aliases: vec![
347                    "qwen3.6-max-preview".to_string(),
348                    "qwen-3.6-max-preview".to_string(),
349                    "qwen-max-preview".to_string(),
350                ],
351                supports_tools: true,
352                supports_reasoning: true,
353            },
354            ModelInfo {
355                id: "qwen/qwen3.6-27b".to_string(),
356                provider: ProviderKind::Openrouter,
357                aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()],
358                supports_tools: true,
359                supports_reasoning: true,
360            },
361            ModelInfo {
362                id: "qwen/qwen3.6-plus".to_string(),
363                provider: ProviderKind::Openrouter,
364                aliases: vec!["qwen3.6-plus".to_string(), "qwen-3.6-plus".to_string()],
365                supports_tools: true,
366                supports_reasoning: true,
367            },
368            ModelInfo {
369                id: "qwen/qwen3.7-plus".to_string(),
370                provider: ProviderKind::Openrouter,
371                aliases: vec!["qwen3.7-plus".to_string(), "qwen-3.7-plus".to_string()],
372                supports_tools: true,
373                supports_reasoning: true,
374            },
375            ModelInfo {
376                id: "moonshotai/kimi-k2.7-code".to_string(),
377                provider: ProviderKind::Openrouter,
378                aliases: vec![
379                    "kimi-k2.7-code".to_string(),
380                    "openrouter-kimi-k2.7-code".to_string(),
381                ],
382                supports_tools: true,
383                supports_reasoning: true,
384            },
385            ModelInfo {
386                id: "moonshotai/kimi-k2.6".to_string(),
387                provider: ProviderKind::Openrouter,
388                aliases: vec!["openrouter-kimi-k2.6".to_string()],
389                supports_tools: true,
390                supports_reasoning: true,
391            },
392            ModelInfo {
393                id: "minimax/minimax-m3".to_string(),
394                provider: ProviderKind::Openrouter,
395                aliases: vec![
396                    "minimax-m3".to_string(),
397                    "minimax-m-3".to_string(),
398                    "openrouter-minimax-m3".to_string(),
399                ],
400                supports_tools: true,
401                supports_reasoning: true,
402            },
403            ModelInfo {
404                id: "z-ai/glm-5.1".to_string(),
405                provider: ProviderKind::Openrouter,
406                aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
407                supports_tools: true,
408                supports_reasoning: true,
409            },
410            ModelInfo {
411                id: "z-ai/glm-5.2".to_string(),
412                provider: ProviderKind::Openrouter,
413                aliases: vec!["glm-5.2".to_string(), "zai-glm-5.2".to_string()],
414                supports_tools: true,
415                supports_reasoning: true,
416            },
417            // GLM-5.3 is live; capabilities still inherit from glm-5.2 until
418            // Z.ai publishes distinct 5.3 numbers. See
419            // crates/config/assets/models_dev.bundled.json
420            // `_meta.pending_release_metadata`.
421            ModelInfo {
422                id: "z-ai/glm-5.3".to_string(),
423                provider: ProviderKind::Openrouter,
424                aliases: vec!["glm-5.3".to_string(), "zai-glm-5.3".to_string()],
425                supports_tools: true,
426                supports_reasoning: true,
427            },
428            ModelInfo {
429                id: "z-ai/glm-5-turbo".to_string(),
430                provider: ProviderKind::Openrouter,
431                aliases: vec!["glm-5-turbo".to_string(), "zai-glm-5-turbo".to_string()],
432                supports_tools: true,
433                supports_reasoning: true,
434            },
435            ModelInfo {
436                id: "GLM-5.3".to_string(),
437                provider: ProviderKind::Zai,
438                aliases: vec![
439                    "glm-5.3".to_string(),
440                    "glm-5-3".to_string(),
441                    "zai-glm-5.3".to_string(),
442                    "zai-glm-5-3".to_string(),
443                ],
444                supports_tools: true,
445                supports_reasoning: true,
446            },
447            // The first Z.ai row is the provider default. Keep this ordering
448            // aligned with `DEFAULT_ZAI_MODEL` in codewhale-config.
449            ModelInfo {
450                id: "GLM-5.2".to_string(),
451                provider: ProviderKind::Zai,
452                aliases: vec![
453                    "glm-5.2".to_string(),
454                    "glm-5-2".to_string(),
455                    "zai-glm-5.2".to_string(),
456                    "zai-glm-5-2".to_string(),
457                ],
458                supports_tools: true,
459                supports_reasoning: true,
460            },
461            ModelInfo {
462                id: "GLM-5.1".to_string(),
463                provider: ProviderKind::Zai,
464                aliases: vec![
465                    "glm-5.1".to_string(),
466                    "glm-5-1".to_string(),
467                    "zai-glm-5.1".to_string(),
468                    "zai-glm-5-1".to_string(),
469                ],
470                supports_tools: true,
471                supports_reasoning: true,
472            },
473            ModelInfo {
474                id: "GLM-5-Turbo".to_string(),
475                provider: ProviderKind::Zai,
476                aliases: vec![
477                    "glm-5-turbo".to_string(),
478                    "glm-5turbo".to_string(),
479                    "zai-glm-5-turbo".to_string(),
480                ],
481                supports_tools: true,
482                supports_reasoning: true,
483            },
484            ModelInfo {
485                id: "tencent/hy3-preview".to_string(),
486                provider: ProviderKind::Openrouter,
487                aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
488                supports_tools: true,
489                supports_reasoning: true,
490            },
491            ModelInfo {
492                id: "google/gemma-4-31b-it".to_string(),
493                provider: ProviderKind::Openrouter,
494                aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
495                supports_tools: true,
496                supports_reasoning: true,
497            },
498            ModelInfo {
499                id: "google/gemma-4-26b-a4b-it".to_string(),
500                provider: ProviderKind::Openrouter,
501                aliases: vec![
502                    "gemma-4-26b-a4b".to_string(),
503                    "gemma-4-26b-a4b-it".to_string(),
504                ],
505                supports_tools: true,
506                supports_reasoning: true,
507            },
508            ModelInfo {
509                id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
510                provider: ProviderKind::Openrouter,
511                aliases: vec![
512                    "nemotron-3-nano-omni".to_string(),
513                    "nemotron-3-nano-omni-reasoning".to_string(),
514                ],
515                supports_tools: true,
516                supports_reasoning: true,
517            },
518            ModelInfo {
519                id: "mimo-v2.5-pro".to_string(),
520                provider: ProviderKind::XiaomiMimo,
521                aliases: vec![
522                    "mimo".to_string(),
523                    "pro".to_string(),
524                    "xiaomi-mimo-v2.5-pro".to_string(),
525                    "xiaomi-mimo-v2-5-pro".to_string(),
526                ],
527                supports_tools: true,
528                supports_reasoning: true,
529            },
530            ModelInfo {
531                id: "mimo-v2.5".to_string(),
532                provider: ProviderKind::XiaomiMimo,
533                aliases: vec![
534                    "omni".to_string(),
535                    "mimo-omni".to_string(),
536                    "v2.5-omni".to_string(),
537                    "mimo-v2.5-omni".to_string(),
538                    "xiaomi-mimo-v2.5".to_string(),
539                    "xiaomi-mimo-v2.5-omni".to_string(),
540                ],
541                supports_tools: true,
542                supports_reasoning: true,
543            },
544            ModelInfo {
545                id: "mimo-v2.5-asr".to_string(),
546                provider: ProviderKind::XiaomiMimo,
547                aliases: vec![
548                    "asr".to_string(),
549                    "speech-to-text".to_string(),
550                    "transcribe".to_string(),
551                ],
552                supports_tools: false,
553                supports_reasoning: false,
554            },
555            ModelInfo {
556                id: "mimo-v2.5-tts".to_string(),
557                provider: ProviderKind::XiaomiMimo,
558                aliases: vec![
559                    "tts".to_string(),
560                    "speech".to_string(),
561                    "mimo-tts".to_string(),
562                ],
563                supports_tools: false,
564                supports_reasoning: false,
565            },
566            ModelInfo {
567                id: "mimo-v2.5-tts-voicedesign".to_string(),
568                provider: ProviderKind::XiaomiMimo,
569                aliases: vec![
570                    "voicedesign".to_string(),
571                    "voice-design".to_string(),
572                    "mimo-voice-design".to_string(),
573                ],
574                supports_tools: false,
575                supports_reasoning: false,
576            },
577            ModelInfo {
578                id: "mimo-v2.5-tts-voiceclone".to_string(),
579                provider: ProviderKind::XiaomiMimo,
580                aliases: vec![
581                    "voiceclone".to_string(),
582                    "voice-clone".to_string(),
583                    "mimo-voice-clone".to_string(),
584                ],
585                supports_tools: false,
586                supports_reasoning: false,
587            },
588            ModelInfo {
589                id: "mimo-v2-tts".to_string(),
590                provider: ProviderKind::XiaomiMimo,
591                aliases: vec!["mimo-v2-speech".to_string()],
592                supports_tools: false,
593                supports_reasoning: false,
594            },
595            ModelInfo {
596                id: "deepseek/deepseek-v4-pro".to_string(),
597                provider: ProviderKind::Novita,
598                aliases: vec![
599                    "deepseek-v4-pro".to_string(),
600                    "novita-deepseek-v4-pro".to_string(),
601                ],
602                supports_tools: true,
603                supports_reasoning: true,
604            },
605            ModelInfo {
606                id: "deepseek/deepseek-v4-flash".to_string(),
607                provider: ProviderKind::Novita,
608                aliases: vec![
609                    "deepseek-v4-flash".to_string(),
610                    "deepseek-chat".to_string(),
611                    "deepseek-reasoner".to_string(),
612                    "novita-deepseek-v4-flash".to_string(),
613                ],
614                supports_tools: true,
615                supports_reasoning: true,
616            },
617            ModelInfo {
618                id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
619                provider: ProviderKind::Fireworks,
620                aliases: vec![
621                    "deepseek-v4-pro".to_string(),
622                    "fireworks-deepseek-v4-pro".to_string(),
623                ],
624                supports_tools: true,
625                supports_reasoning: true,
626            },
627            ModelInfo {
628                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
629                provider: ProviderKind::Siliconflow,
630                aliases: vec![
631                    "deepseek-v4-pro".to_string(),
632                    "deepseek-reasoner".to_string(),
633                    "deepseek-r1".to_string(),
634                    "siliconflow-deepseek-v4-pro".to_string(),
635                ],
636                supports_tools: true,
637                supports_reasoning: true,
638            },
639            ModelInfo {
640                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
641                provider: ProviderKind::Siliconflow,
642                aliases: vec![
643                    "deepseek-v4-flash".to_string(),
644                    "deepseek-chat".to_string(),
645                    "deepseek-v3".to_string(),
646                    "siliconflow-deepseek-v4-flash".to_string(),
647                ],
648                supports_tools: true,
649                supports_reasoning: true,
650            },
651            ModelInfo {
652                id: "trinity-large-preview".to_string(),
653                provider: ProviderKind::Arcee,
654                aliases: vec!["arcee-trinity-large-preview".to_string()],
655                supports_tools: true,
656                supports_reasoning: false,
657            },
658            ModelInfo {
659                id: "kimi-k2.7-code".to_string(),
660                provider: ProviderKind::Moonshot,
661                aliases: vec![
662                    "kimi".to_string(),
663                    "kimi-k2".to_string(),
664                    "kimi-k2.7".to_string(),
665                    "kimi-code".to_string(),
666                    "moonshot-kimi-k2.7-code".to_string(),
667                ],
668                supports_tools: true,
669                supports_reasoning: true,
670            },
671            ModelInfo {
672                id: "kimi-k2.6".to_string(),
673                provider: ProviderKind::Moonshot,
674                aliases: vec!["kimi-k2.6".to_string(), "moonshot-kimi-k2.6".to_string()],
675                supports_tools: true,
676                supports_reasoning: true,
677            },
678            // Moonshot ships K3 as two distinct products under one provider
679            // id, separated by endpoint (v0.9.1 kimi-k3 dogfood report):
680            //   * `kimi-k3` on the direct platform API (api.moonshot.ai/v1)
681            //   * `k3` on the Kimi Code coding-plan API (api.kimi.com/coding/v1)
682            // Both must be resolvable here or `--model kimi-k3` silently
683            // reports the provider default instead. The endpoint pairing is
684            // enforced separately by `validate_kimi_code_api_model_id`; keep
685            // the two ids in separate entries so neither one's alias set can
686            // launder a request onto the other product's route.
687            ModelInfo {
688                id: "kimi-k3".to_string(),
689                provider: ProviderKind::Moonshot,
690                aliases: vec!["moonshot-kimi-k3".to_string()],
691                supports_tools: true,
692                supports_reasoning: true,
693            },
694            ModelInfo {
695                id: "k3".to_string(),
696                provider: ProviderKind::Moonshot,
697                aliases: vec!["kimi-code-k3".to_string()],
698                supports_tools: true,
699                supports_reasoning: true,
700            },
701            ModelInfo {
702                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
703                provider: ProviderKind::Sglang,
704                aliases: vec![
705                    "deepseek-v4-pro".to_string(),
706                    "sglang-deepseek-v4-pro".to_string(),
707                ],
708                supports_tools: true,
709                supports_reasoning: true,
710            },
711            ModelInfo {
712                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
713                provider: ProviderKind::Sglang,
714                aliases: vec![
715                    "deepseek-v4-flash".to_string(),
716                    "deepseek-chat".to_string(),
717                    "deepseek-reasoner".to_string(),
718                    "sglang-deepseek-v4-flash".to_string(),
719                ],
720                supports_tools: true,
721                supports_reasoning: true,
722            },
723            ModelInfo {
724                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
725                provider: ProviderKind::Vllm,
726                aliases: vec![
727                    "deepseek-v4-pro".to_string(),
728                    "vllm-deepseek-v4-pro".to_string(),
729                ],
730                supports_tools: true,
731                supports_reasoning: true,
732            },
733            ModelInfo {
734                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
735                provider: ProviderKind::Vllm,
736                aliases: vec![
737                    "deepseek-v4-flash".to_string(),
738                    "deepseek-chat".to_string(),
739                    "deepseek-reasoner".to_string(),
740                    "vllm-deepseek-v4-flash".to_string(),
741                ],
742                supports_tools: true,
743                supports_reasoning: true,
744            },
745            ModelInfo {
746                id: "deepseek-v4-flash".to_string(),
747                provider: ProviderKind::Ollama,
748                aliases: vec![],
749                supports_tools: true,
750                supports_reasoning: true,
751            },
752            ModelInfo {
753                id: "gpt-oss:120b".to_string(),
754                provider: ProviderKind::OllamaCloud,
755                aliases: vec![],
756                supports_tools: true,
757                supports_reasoning: true,
758            },
759            ModelInfo {
760                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
761                provider: ProviderKind::Huggingface,
762                aliases: vec![
763                    "deepseek-v4-pro".to_string(),
764                    "hf-deepseek-v4-pro".to_string(),
765                ],
766                supports_tools: true,
767                supports_reasoning: true,
768            },
769            ModelInfo {
770                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
771                provider: ProviderKind::Huggingface,
772                aliases: vec![
773                    "deepseek-v4-flash".to_string(),
774                    "deepseek-chat".to_string(),
775                    "deepseek-reasoner".to_string(),
776                    "hf-deepseek-v4-flash".to_string(),
777                ],
778                supports_tools: true,
779                supports_reasoning: true,
780            },
781            // Together AI provider models
782            ModelInfo {
783                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
784                provider: ProviderKind::Together,
785                aliases: vec![
786                    "deepseek-v4-pro".to_string(),
787                    "together-deepseek-v4-pro".to_string(),
788                ],
789                supports_tools: true,
790                supports_reasoning: true,
791            },
792            ModelInfo {
793                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
794                provider: ProviderKind::Together,
795                aliases: vec![
796                    "deepseek-v4-flash".to_string(),
797                    "deepseek-chat".to_string(),
798                    "together-deepseek-v4-flash".to_string(),
799                ],
800                supports_tools: true,
801                supports_reasoning: true,
802            },
803            ModelInfo {
804                // Together's published hosted endpoint is lowercase even though
805                // the open-weight Hugging Face repository uses `Inkling`.
806                id: "thinkingmachines/inkling".to_string(),
807                provider: ProviderKind::Together,
808                aliases: vec!["inkling".to_string(), "together-inkling".to_string()],
809                supports_tools: true,
810                supports_reasoning: true,
811            },
812            // Qwen 3.7 Max (OpenRouter)
813            ModelInfo {
814                id: "qwen/qwen3.7-max".to_string(),
815                provider: ProviderKind::Openrouter,
816                aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
817                supports_tools: true,
818                supports_reasoning: true,
819            },
820            // OpenAI Codex (ChatGPT OAuth) models
821            ModelInfo {
822                id: "gpt-5.5".to_string(),
823                provider: ProviderKind::OpenaiCodex,
824                aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
825                supports_tools: true,
826                supports_reasoning: true,
827            },
828            // Anthropic native Messages API models (#3014)
829            ModelInfo {
830                id: "claude-opus-4-8".to_string(),
831                provider: ProviderKind::Anthropic,
832                aliases: vec!["opus".to_string(), "claude-opus".to_string()],
833                supports_tools: true,
834                supports_reasoning: true,
835            },
836            // Claude Opus 5 (GA 2026-07-24; API id/alias `claude-opus-5`, 1M
837            // context / 128K output per
838            // https://platform.claude.com/docs/en/about-claude/models/overview).
839            ModelInfo {
840                id: "claude-opus-5".to_string(),
841                provider: ProviderKind::Anthropic,
842                aliases: vec!["opus-5".to_string()],
843                supports_tools: true,
844                supports_reasoning: true,
845            },
846            ModelInfo {
847                id: "claude-sonnet-4-6".to_string(),
848                provider: ProviderKind::Anthropic,
849                aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
850                supports_tools: true,
851                supports_reasoning: true,
852            },
853            ModelInfo {
854                id: "claude-haiku-4-5".to_string(),
855                provider: ProviderKind::Anthropic,
856                aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
857                supports_tools: true,
858                supports_reasoning: false,
859            },
860            ModelInfo {
861                id: "claude-sonnet-5".to_string(),
862                provider: ProviderKind::Anthropic,
863                aliases: vec!["sonnet-5".to_string()],
864                supports_tools: true,
865                supports_reasoning: true,
866            },
867            ModelInfo {
868                id: "claude-fable-5".to_string(),
869                provider: ProviderKind::Anthropic,
870                aliases: vec!["fable".to_string(), "fable-5".to_string()],
871                supports_tools: true,
872                supports_reasoning: true,
873            },
874            // OpenModel Anthropic-compatible Messages route
875            ModelInfo {
876                id: "deepseek-v4-flash".to_string(),
877                provider: ProviderKind::Openmodel,
878                aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
879                supports_tools: true,
880                supports_reasoning: true,
881            },
882            // MiniMax 2.7 (OpenRouter)
883            ModelInfo {
884                id: "minimax/minimax-m2.7".to_string(),
885                provider: ProviderKind::Openrouter,
886                aliases: vec![
887                    "minimax-2.7".to_string(),
888                    "minimax-2-7".to_string(),
889                    "openrouter-minimax-2.7".to_string(),
890                ],
891                supports_tools: true,
892                supports_reasoning: true,
893            },
894            ModelInfo {
895                id: "step-3.7-flash".to_string(),
896                provider: ProviderKind::Stepfun,
897                aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
898                supports_tools: true,
899                supports_reasoning: false,
900            },
901            ModelInfo {
902                id: "MiniMax-M3".to_string(),
903                provider: ProviderKind::Minimax,
904                aliases: vec![
905                    "minimax".to_string(),
906                    "minimax-m3".to_string(),
907                    "minimax-m-3".to_string(),
908                ],
909                supports_tools: true,
910                supports_reasoning: true,
911            },
912            ModelInfo {
913                id: "MiniMax-M2.7".to_string(),
914                provider: ProviderKind::Minimax,
915                aliases: vec![
916                    "minimax-m2.7".to_string(),
917                    "minimax-m2-7".to_string(),
918                    "minimax-m-2.7".to_string(),
919                    "minimax-m-2-7".to_string(),
920                ],
921                supports_tools: true,
922                supports_reasoning: true,
923            },
924            ModelInfo {
925                id: "MiniMax-M3".to_string(),
926                provider: ProviderKind::MinimaxAnthropic,
927                aliases: vec![
928                    "minimax-anthropic".to_string(),
929                    "minimax-anthropic-m3".to_string(),
930                    "minimax-m3".to_string(),
931                ],
932                supports_tools: true,
933                supports_reasoning: true,
934            },
935            ModelInfo {
936                id: "MiniMax-M2.7".to_string(),
937                provider: ProviderKind::MinimaxAnthropic,
938                aliases: vec![
939                    "minimax-anthropic-m2.7".to_string(),
940                    "minimax-anthropic-m2-7".to_string(),
941                    "minimax-m2.7".to_string(),
942                ],
943                supports_tools: true,
944                supports_reasoning: true,
945            },
946            ModelInfo {
947                id: "MiniMax-M2.7-highspeed".to_string(),
948                provider: ProviderKind::Minimax,
949                aliases: vec![
950                    "minimax-m2.7-highspeed".to_string(),
951                    "minimax-m2-7-highspeed".to_string(),
952                    "minimax-m-2.7-highspeed".to_string(),
953                    "minimax-m-2-7-highspeed".to_string(),
954                ],
955                supports_tools: true,
956                supports_reasoning: true,
957            },
958            ModelInfo {
959                id: "MiniMax-M2.5".to_string(),
960                provider: ProviderKind::Minimax,
961                aliases: vec![
962                    "minimax-m2.5".to_string(),
963                    "minimax-m2-5".to_string(),
964                    "minimax-m-2.5".to_string(),
965                    "minimax-m-2-5".to_string(),
966                ],
967                supports_tools: true,
968                supports_reasoning: true,
969            },
970            ModelInfo {
971                id: "MiniMax-M2.5-highspeed".to_string(),
972                provider: ProviderKind::Minimax,
973                aliases: vec![
974                    "minimax-m2.5-highspeed".to_string(),
975                    "minimax-m2-5-highspeed".to_string(),
976                    "minimax-m-2.5-highspeed".to_string(),
977                    "minimax-m-2-5-highspeed".to_string(),
978                ],
979                supports_tools: true,
980                supports_reasoning: true,
981            },
982            ModelInfo {
983                id: "MiniMax-M2.1".to_string(),
984                provider: ProviderKind::Minimax,
985                aliases: vec![
986                    "minimax-m2.1".to_string(),
987                    "minimax-m2-1".to_string(),
988                    "minimax-m-2.1".to_string(),
989                    "minimax-m-2-1".to_string(),
990                ],
991                supports_tools: true,
992                supports_reasoning: true,
993            },
994            ModelInfo {
995                id: "MiniMax-M2.1-highspeed".to_string(),
996                provider: ProviderKind::Minimax,
997                aliases: vec![
998                    "minimax-m2.1-highspeed".to_string(),
999                    "minimax-m2-1-highspeed".to_string(),
1000                    "minimax-m-2.1-highspeed".to_string(),
1001                    "minimax-m-2-1-highspeed".to_string(),
1002                ],
1003                supports_tools: true,
1004                supports_reasoning: true,
1005            },
1006            ModelInfo {
1007                id: "MiniMax-M2".to_string(),
1008                provider: ProviderKind::Minimax,
1009                aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
1010                supports_tools: true,
1011                supports_reasoning: true,
1012            },
1013            // NVIDIA Nemotron 3 Ultra (OpenRouter)
1014            ModelInfo {
1015                id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
1016                provider: ProviderKind::Openrouter,
1017                aliases: vec![
1018                    "nvidia/nemotron-3-ultra".to_string(),
1019                    "nemotron-3-ultra".to_string(),
1020                    "nemotron-3-ultra-550b-a55b".to_string(),
1021                    "nvidia-nemotron-3-ultra".to_string(),
1022                    "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
1023                ],
1024                supports_tools: true,
1025                supports_reasoning: true,
1026            },
1027            // DeepInfra (https://deepinfra.com)
1028            ModelInfo {
1029                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
1030                provider: ProviderKind::Deepinfra,
1031                aliases: vec![
1032                    "deepseek-v4-pro".to_string(),
1033                    "di-deepseek-v4-pro".to_string(),
1034                ],
1035                supports_tools: true,
1036                supports_reasoning: true,
1037            },
1038            ModelInfo {
1039                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
1040                provider: ProviderKind::Deepinfra,
1041                aliases: vec![
1042                    "deepseek-v4-flash".to_string(),
1043                    "di-deepseek-v4-flash".to_string(),
1044                ],
1045                supports_tools: true,
1046                supports_reasoning: true,
1047            },
1048            // Sakana AI Fugu (https://api.sakana.ai)
1049            ModelInfo {
1050                id: "fugu".to_string(),
1051                provider: ProviderKind::Sakana,
1052                aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
1053                supports_tools: true,
1054                supports_reasoning: false,
1055            },
1056            ModelInfo {
1057                id: "fugu-ultra-20260615".to_string(),
1058                provider: ProviderKind::Sakana,
1059                aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
1060                supports_tools: true,
1061                supports_reasoning: true,
1062            },
1063            // Meituan LongCat (https://longcat.chat/platform)
1064            ModelInfo {
1065                id: "LongCat-2.0".to_string(),
1066                provider: ProviderKind::LongCat,
1067                aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
1068                supports_tools: true,
1069                supports_reasoning: true,
1070            },
1071            // OpenCode Go Chat Completions models (https://opencode.ai/docs/go/).
1072            // Go models documented only on `/messages` are intentionally not
1073            // advertised by this OpenAI-compatible provider slice.
1074            ModelInfo {
1075                id: "deepseek-v4-pro".to_string(),
1076                provider: ProviderKind::OpencodeGo,
1077                aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
1078                supports_tools: true,
1079                supports_reasoning: true,
1080            },
1081            ModelInfo {
1082                id: "grok-4.5".to_string(),
1083                provider: ProviderKind::OpencodeGo,
1084                aliases: vec!["opencode-go/grok-4.5".to_string()],
1085                supports_tools: true,
1086                supports_reasoning: true,
1087            },
1088            // No glm-5.3 row (2026-08-03): OpenCode Go publishes no glm-5.3
1089            // model. The Z.ai/OpenRouter glm-5.3 rows inherit glm-5.2 metadata;
1090            // that inheritance is not evidence this gateway serves it.
1091            ModelInfo {
1092                id: "glm-5.2".to_string(),
1093                provider: ProviderKind::OpencodeGo,
1094                aliases: vec!["opencode-go/glm-5.2".to_string()],
1095                supports_tools: true,
1096                supports_reasoning: true,
1097            },
1098            ModelInfo {
1099                id: "glm-5.1".to_string(),
1100                provider: ProviderKind::OpencodeGo,
1101                aliases: vec!["opencode-go/glm-5.1".to_string()],
1102                supports_tools: true,
1103                supports_reasoning: true,
1104            },
1105            ModelInfo {
1106                id: "kimi-k3".to_string(),
1107                provider: ProviderKind::OpencodeGo,
1108                aliases: vec!["opencode-go/kimi-k3".to_string()],
1109                supports_tools: true,
1110                supports_reasoning: true,
1111            },
1112            ModelInfo {
1113                id: "kimi-k2.7-code".to_string(),
1114                provider: ProviderKind::OpencodeGo,
1115                aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
1116                supports_tools: true,
1117                supports_reasoning: true,
1118            },
1119            ModelInfo {
1120                id: "kimi-k2.6".to_string(),
1121                provider: ProviderKind::OpencodeGo,
1122                aliases: vec!["opencode-go/kimi-k2.6".to_string()],
1123                supports_tools: true,
1124                supports_reasoning: true,
1125            },
1126            ModelInfo {
1127                id: "deepseek-v4-flash".to_string(),
1128                provider: ProviderKind::OpencodeGo,
1129                aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
1130                supports_tools: true,
1131                supports_reasoning: true,
1132            },
1133            ModelInfo {
1134                id: "mimo-v2.5".to_string(),
1135                provider: ProviderKind::OpencodeGo,
1136                aliases: vec!["opencode-go/mimo-v2.5".to_string()],
1137                supports_tools: true,
1138                supports_reasoning: true,
1139            },
1140            ModelInfo {
1141                id: "mimo-v2.5-pro".to_string(),
1142                provider: ProviderKind::OpencodeGo,
1143                aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
1144                supports_tools: true,
1145                supports_reasoning: true,
1146            },
1147            // Meta Model API / Muse Spark. Keep these in step with
1148            // `DEFAULT_META_MODEL` in config's provider_defaults and with the
1149            // bundled models.dev catalog: this registry resolves the `muse`
1150            // aliases for the CLI and app-server, so a stale id here silently
1151            // routes them somewhere the configured default never points.
1152            ModelInfo {
1153                id: "muse-spark-1.2".to_string(),
1154                provider: ProviderKind::Meta,
1155                aliases: vec!["muse-spark".to_string(), "muse".to_string()],
1156                supports_tools: true,
1157                supports_reasoning: true,
1158            },
1159            ModelInfo {
1160                id: "muse-spark-1.2-contributor".to_string(),
1161                provider: ProviderKind::Meta,
1162                aliases: vec!["muse-spark-contributor".to_string()],
1163                supports_tools: true,
1164                supports_reasoning: true,
1165            },
1166            // xAI / Grok (https://api.x.ai/v1)
1167            ModelInfo {
1168                id: "grok-4.6".to_string(),
1169                provider: ProviderKind::Xai,
1170                aliases: vec!["grok".to_string()],
1171                supports_tools: true,
1172                supports_reasoning: true,
1173            },
1174            ModelInfo {
1175                id: "grok-4.5".to_string(),
1176                provider: ProviderKind::Xai,
1177                aliases: vec!["xai-grok-4.5".to_string()],
1178                supports_tools: true,
1179                supports_reasoning: true,
1180            },
1181            ModelInfo {
1182                id: "grok-4.3".to_string(),
1183                provider: ProviderKind::Xai,
1184                aliases: vec!["xai-grok-4.3".to_string()],
1185                supports_tools: true,
1186                supports_reasoning: true,
1187            },
1188            ModelInfo {
1189                id: "grok-build".to_string(),
1190                provider: ProviderKind::Xai,
1191                aliases: vec!["xai-grok-build".to_string()],
1192                supports_tools: true,
1193                supports_reasoning: true,
1194            },
1195            ModelInfo {
1196                id: "grok-composer-2.5-fast".to_string(),
1197                provider: ProviderKind::Xai,
1198                aliases: vec!["xai-grok-composer".to_string()],
1199                supports_tools: true,
1200                supports_reasoning: false,
1201            },
1202            ModelInfo {
1203                id: "grok-4.20-0309-reasoning".to_string(),
1204                provider: ProviderKind::Xai,
1205                aliases: vec!["xai-grok-reasoning".to_string()],
1206                supports_tools: true,
1207                supports_reasoning: true,
1208            },
1209            ModelInfo {
1210                id: "grok-4.20-0309-non-reasoning".to_string(),
1211                provider: ProviderKind::Xai,
1212                aliases: vec!["xai-grok-fast".to_string()],
1213                supports_tools: true,
1214                supports_reasoning: false,
1215            },
1216            ModelInfo {
1217                id: "gemini-3.1-pro-preview".to_string(),
1218                provider: ProviderKind::Google,
1219                aliases: vec!["gemini-3.1-pro".to_string()],
1220                supports_tools: true,
1221                supports_reasoning: true,
1222            },
1223            ModelInfo {
1224                id: "gemini-3-pro-preview".to_string(),
1225                provider: ProviderKind::Google,
1226                aliases: vec!["gemini-3-pro".to_string()],
1227                supports_tools: true,
1228                supports_reasoning: true,
1229            },
1230            // Gemini 3.7 Flash (2026-08 latest Flash; 1,048,576 in / 65,536 out,
1231            // https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash).
1232            ModelInfo {
1233                id: "gemini-3.7-flash".to_string(),
1234                provider: ProviderKind::Google,
1235                aliases: vec![],
1236                supports_tools: true,
1237                supports_reasoning: true,
1238            },
1239            ModelInfo {
1240                id: "gemini-3.6-flash".to_string(),
1241                provider: ProviderKind::Google,
1242                aliases: vec![],
1243                supports_tools: true,
1244                supports_reasoning: true,
1245            },
1246            ModelInfo {
1247                id: "gemini-3.5-flash".to_string(),
1248                provider: ProviderKind::Google,
1249                aliases: vec![],
1250                supports_tools: true,
1251                supports_reasoning: true,
1252            },
1253            ModelInfo {
1254                id: "gemini-3.5-flash-lite".to_string(),
1255                provider: ProviderKind::Google,
1256                aliases: vec![],
1257                supports_tools: true,
1258                supports_reasoning: false,
1259            },
1260            ModelInfo {
1261                id: "gemini-2.5-pro".to_string(),
1262                provider: ProviderKind::Google,
1263                aliases: vec![],
1264                supports_tools: true,
1265                supports_reasoning: true,
1266            },
1267            ModelInfo {
1268                id: "gemini-2.5-flash".to_string(),
1269                provider: ProviderKind::Google,
1270                aliases: vec![],
1271                supports_tools: true,
1272                supports_reasoning: true,
1273            },
1274            ModelInfo {
1275                id: "mistral-code-latest".to_string(),
1276                provider: ProviderKind::Mistral,
1277                aliases: vec![
1278                    "codestral".to_string(),
1279                    "codestral-latest".to_string(),
1280                    "mistral-code".to_string(),
1281                ],
1282                supports_tools: true,
1283                supports_reasoning: false,
1284            },
1285            ModelInfo {
1286                id: "mistral-medium-latest".to_string(),
1287                provider: ProviderKind::Mistral,
1288                aliases: vec![
1289                    "mistral-medium".to_string(),
1290                    "mistral-medium-3-5".to_string(),
1291                ],
1292                supports_tools: true,
1293                supports_reasoning: true,
1294            },
1295            ModelInfo {
1296                id: "mistral-small-latest".to_string(),
1297                provider: ProviderKind::Mistral,
1298                aliases: vec![
1299                    "mistral-small".to_string(),
1300                    "mistral-small-2603".to_string(),
1301                ],
1302                supports_tools: true,
1303                supports_reasoning: true,
1304            },
1305            ModelInfo {
1306                id: "magistral-small-latest".to_string(),
1307                provider: ProviderKind::Mistral,
1308                aliases: vec!["magistral".to_string(), "magistral-small".to_string()],
1309                supports_tools: true,
1310                supports_reasoning: true,
1311            },
1312            ModelInfo {
1313                id: "mistral-large-latest".to_string(),
1314                provider: ProviderKind::Mistral,
1315                aliases: vec!["mistral-large".to_string()],
1316                supports_tools: true,
1317                supports_reasoning: false,
1318            },
1319        ];
1320        Self::new(models)
1321    }
1322}
1323
1324impl ModelRegistry {
1325    /// Creates a new registry from a list of [`ModelInfo`] entries.
1326    ///
1327    /// Builds an internal alias map for fast lookup by model id or alias.
1328    /// If multiple models share the same id or alias, the first one registered
1329    /// takes priority.
1330    #[must_use]
1331    pub fn new(models: Vec<ModelInfo>) -> Self {
1332        let mut alias_map = HashMap::new();
1333        for (idx, model) in models.iter().enumerate() {
1334            alias_map.entry(normalize(&model.id)).or_insert(idx);
1335            for alias in &model.aliases {
1336                alias_map.entry(normalize(alias)).or_insert(idx);
1337            }
1338        }
1339        Self { models, alias_map }
1340    }
1341
1342    /// Returns a clone of all models in the registry.
1343    #[must_use]
1344    pub fn list(&self) -> Vec<ModelInfo> {
1345        self.models.clone()
1346    }
1347
1348    /// Resolves a user-requested model name to a concrete [`ModelInfo`].
1349    ///
1350    /// Resolution follows this priority order:
1351    /// 1. If the provider is Ollama, the requested name is used as-is (to
1352    ///    support arbitrary local model tags like `qwen2.5-coder:7b`).
1353    /// 2. If a `provider_hint` is given, search for a model matching that
1354    ///    provider whose id or alias matches the request (case-insensitive).
1355    /// 3. Look up the alias map for a case-insensitive match.
1356    /// 4. Fall back to the first model belonging to the hinted provider
1357    ///    (or DeepSeek if no hint was given).
1358    /// 5. As a last resort, fall back to the first model in the registry.
1359    #[must_use]
1360    pub fn resolve(
1361        &self,
1362        requested: Option<&str>,
1363        provider_hint: Option<ProviderKind>,
1364    ) -> ModelResolution {
1365        let mut fallback_chain = Vec::new();
1366
1367        if let Some(name) = requested {
1368            fallback_chain.push(format!("requested:{name}"));
1369            if matches!(
1370                provider_hint,
1371                Some(ProviderKind::Ollama | ProviderKind::OllamaCloud)
1372            ) {
1373                return ModelResolution {
1374                    requested: Some(name.to_string()),
1375                    resolved: ModelInfo {
1376                        id: name.trim().to_string(),
1377                        provider: provider_hint.expect("matched provider hint"),
1378                        aliases: Vec::new(),
1379                        supports_tools: true,
1380                        supports_reasoning: false,
1381                    },
1382                    used_fallback: false,
1383                    fallback_chain,
1384                };
1385            }
1386            // OpenCode Go's catalog spans Chat Completions and Anthropic
1387            // Messages, while Codewhale's provider slice intentionally speaks
1388            // Chat only. Resolve a hinted Go model through the shared Chat
1389            // allowlist and never fall through to a same-named global alias on
1390            // OpenRouter or MiniMax.
1391            if provider_hint == Some(ProviderKind::OpencodeGo)
1392                && let Some(canonical) = opencode_go_chat_model_id(name)
1393                && let Some(model) = self
1394                    .models
1395                    .iter()
1396                    .find(|model| {
1397                        model.provider == ProviderKind::OpencodeGo
1398                            && model.id.eq_ignore_ascii_case(canonical)
1399                    })
1400                    .cloned()
1401            {
1402                return ModelResolution {
1403                    requested: Some(name.to_string()),
1404                    resolved: model,
1405                    used_fallback: false,
1406                    fallback_chain,
1407                };
1408            }
1409            if provider_hint != Some(ProviderKind::OpencodeGo)
1410                && let Some(provider) = provider_hint
1411                && let Some(model) = self
1412                    .models
1413                    .iter()
1414                    .find(|m| m.provider == provider && model_matches(m, name))
1415                    .cloned()
1416            {
1417                return ModelResolution {
1418                    requested: Some(name.to_string()),
1419                    resolved: model,
1420                    used_fallback: false,
1421                    fallback_chain,
1422                };
1423            }
1424            if provider_hint == Some(ProviderKind::Atlascloud)
1425                && let Some(model) = atlascloud_passthrough_model(name)
1426            {
1427                return ModelResolution {
1428                    requested: Some(name.to_string()),
1429                    resolved: model,
1430                    used_fallback: false,
1431                    fallback_chain,
1432                };
1433            }
1434            if provider_hint == Some(ProviderKind::Arcee)
1435                && let Some(model) = arcee_passthrough_model(name)
1436            {
1437                return ModelResolution {
1438                    requested: Some(name.to_string()),
1439                    resolved: model,
1440                    used_fallback: false,
1441                    fallback_chain,
1442                };
1443            }
1444            if provider_hint == Some(ProviderKind::XiaomiMimo)
1445                && let Some(model) = xiaomi_mimo_passthrough_model(name)
1446            {
1447                return ModelResolution {
1448                    requested: Some(name.to_string()),
1449                    resolved: model,
1450                    used_fallback: false,
1451                    fallback_chain,
1452                };
1453            }
1454            // The global alias map is a *provider-less* convenience lookup. It
1455            // must never answer a provider-scoped question with another
1456            // vendor's model: before this fix, `--provider moonshot` asking for
1457            // `kimi-k3` was answered with OpenCode Go's `kimi-k3` because the
1458            // hinted provider had no such id. A hinted request that the hinted
1459            // provider cannot serve falls through to that provider's default
1460            // with `used_fallback: true`, which callers already surface.
1461            if provider_hint != Some(ProviderKind::OpencodeGo)
1462                && let Some(idx) = self.alias_map.get(&normalize(name))
1463                && provider_hint.is_none_or(|hint| self.models[*idx].provider == hint)
1464            {
1465                return ModelResolution {
1466                    requested: Some(name.to_string()),
1467                    resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1468                    used_fallback: false,
1469                    fallback_chain,
1470                };
1471            }
1472        }
1473
1474        let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1475        fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1476        if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1477            return ModelResolution {
1478                requested: requested.map(ToOwned::to_owned),
1479                resolved: model,
1480                used_fallback: true,
1481                fallback_chain,
1482            };
1483        }
1484
1485        let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1486            id: "deepseek-v4-pro".to_string(),
1487            provider: ProviderKind::Deepseek,
1488            aliases: Vec::new(),
1489            supports_tools: true,
1490            supports_reasoning: true,
1491        });
1492        fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1493        ModelResolution {
1494            requested: requested.map(ToOwned::to_owned),
1495            resolved: final_fallback,
1496            used_fallback: true,
1497            fallback_chain,
1498        }
1499    }
1500}
1501
1502fn normalize(value: &str) -> String {
1503    value.trim().to_ascii_lowercase()
1504}
1505
1506#[must_use]
1507/// Classify a model identifier by its underlying model family.
1508pub fn model_family(model_id: &str) -> ModelFamily {
1509    let normalized = normalize(model_id);
1510    if normalized.is_empty() {
1511        return ModelFamily::Inferencer;
1512    }
1513
1514    if normalized.contains("deepseek") {
1515        return ModelFamily::DeepSeek;
1516    }
1517    if normalized.contains("claude") || normalized.contains("anthropic") {
1518        return ModelFamily::Anthropic;
1519    }
1520    if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1521        return ModelFamily::GptOss;
1522    }
1523    if normalized.starts_with("gpt-")
1524        || normalized.contains("/gpt-")
1525        || normalized.contains("openai/")
1526    {
1527        return ModelFamily::OpenAI;
1528    }
1529    if normalized.contains("gemini")
1530        || normalized.contains("gemma")
1531        || normalized.contains("google/")
1532    {
1533        return ModelFamily::Google;
1534    }
1535    if normalized.contains("llama")
1536        || normalized.contains("muse-spark")
1537        || normalized.contains("meta-")
1538        || normalized.contains("meta/")
1539    {
1540        return ModelFamily::Meta;
1541    }
1542    if normalized.contains("mistral")
1543        || normalized.contains("mixtral")
1544        || normalized.contains("codestral")
1545    {
1546        return ModelFamily::Mistral;
1547    }
1548    if normalized.contains("qwen") {
1549        return ModelFamily::Qwen;
1550    }
1551    if normalized.contains("grok") {
1552        return ModelFamily::Grok;
1553    }
1554    if normalized.contains("cohere") || normalized.contains("command-r") {
1555        return ModelFamily::Cohere;
1556    }
1557
1558    ModelFamily::Inferencer
1559}
1560
1561fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1562    let requested = normalize(requested);
1563    normalize(&model.id) == requested
1564        || model
1565            .aliases
1566            .iter()
1567            .any(|alias| normalize(alias) == requested)
1568}
1569
1570fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1571    let requested = requested.trim();
1572    if model.id.eq_ignore_ascii_case(requested) {
1573        model.id = requested.to_string();
1574    }
1575    model
1576}
1577
1578fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1579    let requested = requested.trim();
1580    if requested.is_empty() || !requested.contains('/') {
1581        return None;
1582    }
1583
1584    Some(ModelInfo {
1585        id: requested.to_string(),
1586        provider: ProviderKind::Atlascloud,
1587        aliases: Vec::new(),
1588        supports_tools: true,
1589        supports_reasoning: true,
1590    })
1591}
1592
1593fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1594    let requested = requested.trim();
1595    if requested.is_empty() {
1596        return None;
1597    }
1598    let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1599
1600    Some(ModelInfo {
1601        id: requested.to_string(),
1602        provider: ProviderKind::Arcee,
1603        aliases: Vec::new(),
1604        supports_tools: true,
1605        supports_reasoning,
1606    })
1607}
1608
1609fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1610    let requested = requested.trim();
1611    if requested.is_empty() || requested.chars().any(char::is_control) {
1612        return None;
1613    }
1614
1615    Some(ModelInfo {
1616        id: requested.to_string(),
1617        provider: ProviderKind::XiaomiMimo,
1618        aliases: Vec::new(),
1619        supports_tools: true,
1620        supports_reasoning: true,
1621    })
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626    use super::*;
1627
1628    #[test]
1629    fn model_registry_new_builds_alias_map_correctly() {
1630        let models = vec![
1631            ModelInfo {
1632                id: "Model-A".to_string(),
1633                provider: ProviderKind::Deepseek,
1634                aliases: vec!["alias-1".to_string(), " ALIAS-2 ".to_string()],
1635                supports_tools: true,
1636                supports_reasoning: false,
1637            },
1638            ModelInfo {
1639                id: "model-b".to_string(),
1640                provider: ProviderKind::Deepseek,
1641                aliases: vec!["alias-1".to_string()], // Duplicate alias, should not override
1642                supports_tools: true,
1643                supports_reasoning: true,
1644            },
1645        ];
1646
1647        let registry = ModelRegistry::new(models);
1648
1649        assert_eq!(registry.alias_map.len(), 4); // "model-a", "alias-1", "alias-2", "model-b"
1650        assert_eq!(registry.alias_map.get("model-a"), Some(&0));
1651        assert_eq!(registry.alias_map.get("alias-1"), Some(&0)); // First one wins
1652        assert_eq!(registry.alias_map.get("alias-2"), Some(&0)); // Normalized
1653        assert_eq!(registry.alias_map.get("model-b"), Some(&1));
1654    }
1655
1656    #[test]
1657    fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1658        let registry = ModelRegistry::default();
1659        let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1660
1661        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1662        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1663    }
1664
1665    #[test]
1666    fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1667        let registry = ModelRegistry::default();
1668        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1669
1670        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1671        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1672    }
1673
1674    #[test]
1675    fn nvidia_nim_default_uses_catalog_model_id() {
1676        let registry = ModelRegistry::default();
1677        let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1678
1679        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1680        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1681    }
1682
1683    #[test]
1684    fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1685        let registry = ModelRegistry::default();
1686        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1687
1688        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1689        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1690    }
1691
1692    #[test]
1693    fn atlascloud_default_uses_namespaced_model_id() {
1694        let registry = ModelRegistry::default();
1695        let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1696
1697        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1698        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1699        assert!(resolved.resolved.supports_reasoning);
1700    }
1701
1702    #[test]
1703    fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1704        let registry = ModelRegistry::default();
1705        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1706
1707        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1708        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1709    }
1710
1711    #[test]
1712    fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1713        let registry = ModelRegistry::default();
1714        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1715
1716        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1717        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1718    }
1719
1720    #[test]
1721    fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1722        let registry = ModelRegistry::default();
1723        let resolved =
1724            registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1725
1726        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1727        assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1728        assert!(resolved.resolved.supports_tools);
1729        assert!(resolved.resolved.supports_reasoning);
1730        assert!(!resolved.used_fallback);
1731    }
1732
1733    #[test]
1734    fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1735        let registry = ModelRegistry::default();
1736        let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1737
1738        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1739        assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1740        assert!(!resolved.used_fallback);
1741    }
1742
1743    #[test]
1744    fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1745        let registry = ModelRegistry::default();
1746        let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1747
1748        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1749        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1750        assert!(resolved.used_fallback);
1751    }
1752
1753    #[test]
1754    fn openrouter_default_uses_namespaced_model_id() {
1755        let registry = ModelRegistry::default();
1756        let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1757
1758        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1759        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1760    }
1761
1762    #[test]
1763    fn xiaomi_mimo_default_uses_canonical_model_id() {
1764        let registry = ModelRegistry::default();
1765        let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1766
1767        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1768        assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1769        assert!(resolved.resolved.supports_reasoning);
1770    }
1771
1772    #[test]
1773    fn moonshot_default_and_aliases_use_kimi_k27_code() {
1774        let registry = ModelRegistry::default();
1775
1776        for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1777            let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1778
1779            assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1780            assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1781            assert!(resolved.resolved.supports_tools);
1782            assert!(resolved.resolved.supports_reasoning);
1783        }
1784    }
1785
1786    #[test]
1787    fn moonshot_explicit_kimi_k26_remains_available() {
1788        let registry = ModelRegistry::default();
1789        let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1790
1791        assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1792        assert_eq!(resolved.resolved.id, "kimi-k2.6");
1793        assert!(resolved.resolved.supports_reasoning);
1794    }
1795
1796    /// v0.9.1 dogfood report: a user ran `--provider moonshot --model kimi-k3` and was told
1797    /// the model was `kimi-k2.7-code`. The registry knew neither Moonshot K3
1798    /// product, so the explicit request fell through to the provider default.
1799    #[test]
1800    fn moonshot_resolves_both_k3_products_without_crossing_them() {
1801        let registry = ModelRegistry::default();
1802
1803        for (requested, expected) in [("kimi-k3", "kimi-k3"), ("k3", "k3")] {
1804            let resolved = registry.resolve(Some(requested), Some(ProviderKind::Moonshot));
1805
1806            assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1807            assert_eq!(resolved.resolved.id, expected, "{resolved:?}");
1808            assert!(
1809                !resolved.used_fallback,
1810                "an explicit Moonshot K3 request is not a fallback: {resolved:?}"
1811            );
1812        }
1813    }
1814
1815    /// The bare `k3` id belongs to the Kimi Code coding-plan endpoint and
1816    /// `kimi-k3` to the direct platform endpoint. Neither may be laundered
1817    /// into the other's id by alias expansion.
1818    #[test]
1819    fn moonshot_k3_ids_are_never_rewritten_into_each_other() {
1820        let registry = ModelRegistry::default();
1821
1822        assert_eq!(
1823            registry
1824                .resolve(Some("kimi-k3"), Some(ProviderKind::Moonshot))
1825                .resolved
1826                .id,
1827            "kimi-k3"
1828        );
1829        assert_eq!(
1830            registry
1831                .resolve(Some("k3"), Some(ProviderKind::Moonshot))
1832                .resolved
1833                .id,
1834            "k3"
1835        );
1836    }
1837
1838    /// A provider-scoped question must never be answered with another
1839    /// vendor's model. `kimi-k3` also exists in the OpenCode Go catalog;
1840    /// before this fix that entry answered `--provider moonshot` requests.
1841    #[test]
1842    fn a_provider_hint_never_resolves_to_another_providers_model() {
1843        let registry = ModelRegistry::default();
1844
1845        let resolved = registry.resolve(Some("glm-5.2"), Some(ProviderKind::Moonshot));
1846        assert_eq!(
1847            resolved.resolved.provider,
1848            ProviderKind::Moonshot,
1849            "a Moonshot request must not be answered by Z.ai: {resolved:?}"
1850        );
1851        assert!(
1852            resolved.used_fallback,
1853            "an unservable id must be reported as a fallback, not as the request: {resolved:?}"
1854        );
1855
1856        let go = registry.resolve(Some("kimi-k3"), Some(ProviderKind::OpencodeGo));
1857        assert_eq!(go.resolved.provider, ProviderKind::OpencodeGo);
1858        assert_eq!(go.resolved.id, "kimi-k3");
1859    }
1860
1861    #[test]
1862    fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1863        let registry = ModelRegistry::default();
1864        let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1865        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1866        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1867        assert!(!resolved.resolved.supports_tools);
1868        assert!(!resolved.resolved.supports_reasoning);
1869
1870        let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1871        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1872
1873        let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1874        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1875    }
1876
1877    #[test]
1878    fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1879        let registry = ModelRegistry::default();
1880
1881        let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1882        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1883        assert_eq!(resolved.resolved.id, "mimo-v2.5");
1884        assert!(resolved.resolved.supports_tools);
1885    }
1886
1887    #[test]
1888    fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1889        let registry = ModelRegistry::default();
1890        let resolved =
1891            registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1892
1893        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1894        assert_eq!(resolved.resolved.id, "account-custom-mimo");
1895        assert!(!resolved.used_fallback);
1896    }
1897
1898    #[test]
1899    fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1900        let registry = ModelRegistry::default();
1901        let resolved = registry.resolve(
1902            Some("deepseek/deepseek-v4-pro"),
1903            Some(ProviderKind::XiaomiMimo),
1904        );
1905
1906        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1907        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1908        assert!(!resolved.used_fallback);
1909    }
1910
1911    #[test]
1912    fn wanjie_ark_default_uses_reasoner_model_id() {
1913        let registry = ModelRegistry::default();
1914        let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1915
1916        assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1917        assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1918        assert!(resolved.resolved.supports_reasoning);
1919    }
1920
1921    #[test]
1922    fn novita_default_uses_namespaced_model_id() {
1923        let registry = ModelRegistry::default();
1924        let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1925
1926        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1927        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1928    }
1929
1930    #[test]
1931    fn fireworks_default_uses_canonical_model_id() {
1932        let registry = ModelRegistry::default();
1933        let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1934
1935        assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1936        assert_eq!(
1937            resolved.resolved.id,
1938            "accounts/fireworks/models/deepseek-v4-pro"
1939        );
1940    }
1941
1942    #[test]
1943    fn siliconflow_default_uses_canonical_pro_model_id() {
1944        let registry = ModelRegistry::default();
1945        let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1946
1947        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1948        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1949        assert!(resolved.resolved.supports_reasoning);
1950    }
1951
1952    #[test]
1953    fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1954        let registry = ModelRegistry::default();
1955        let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1956
1957        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1958        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1959        assert!(resolved.resolved.supports_reasoning);
1960    }
1961
1962    #[test]
1963    fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1964        let registry = ModelRegistry::default();
1965        let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1966
1967        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1968        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1969        assert!(resolved.resolved.supports_reasoning);
1970    }
1971
1972    #[test]
1973    fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1974        let registry = ModelRegistry::default();
1975        let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1976
1977        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1978        assert_eq!(resolved.resolved.id, "trinity-mini");
1979        assert!(resolved.resolved.supports_reasoning);
1980        assert!(!resolved.used_fallback);
1981    }
1982
1983    #[test]
1984    fn arcee_provider_hint_preserves_explicit_future_model_id() {
1985        let registry = ModelRegistry::default();
1986        let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1987
1988        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1989        assert_eq!(resolved.resolved.id, "trinity-large-next");
1990        assert!(!resolved.resolved.supports_reasoning);
1991        assert!(!resolved.used_fallback);
1992    }
1993
1994    #[test]
1995    fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1996        let registry = ModelRegistry::default();
1997        let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1998
1999        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
2000        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2001    }
2002
2003    #[test]
2004    fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
2005        let registry = ModelRegistry::default();
2006        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
2007
2008        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
2009        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2010    }
2011
2012    #[test]
2013    fn sglang_default_uses_canonical_model_id() {
2014        let registry = ModelRegistry::default();
2015        let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
2016
2017        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2018        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2019    }
2020
2021    #[test]
2022    fn zai_direct_models_resolve_when_provider_hinted() {
2023        let registry = ModelRegistry::default();
2024
2025        // Keep the agent registry fallback aligned with codewhale-config's
2026        // DEFAULT_ZAI_MODEL.
2027        let default = registry.resolve(None, Some(ProviderKind::Zai));
2028        assert_eq!(default.resolved.provider, ProviderKind::Zai);
2029        assert_eq!(default.resolved.id, "GLM-5.3");
2030        assert!(default.used_fallback);
2031        assert_eq!(default.fallback_chain, ["provider_default:zai"]);
2032
2033        for (alias, expected) in [
2034            ("GLM-5.1", "GLM-5.1"),
2035            ("glm-5-1", "GLM-5.1"),
2036            ("GLM-5.2", "GLM-5.2"),
2037            ("glm-5.2", "GLM-5.2"),
2038            ("zai-glm-5-2", "GLM-5.2"),
2039            ("GLM-5.3", "GLM-5.3"),
2040            ("glm-5.3", "GLM-5.3"),
2041            ("glm-5-3", "GLM-5.3"),
2042            ("zai-glm-5-3", "GLM-5.3"),
2043            ("GLM-5-Turbo", "GLM-5-Turbo"),
2044            ("glm-5-turbo", "GLM-5-Turbo"),
2045            ("zai-glm-5-turbo", "GLM-5-Turbo"),
2046        ] {
2047            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
2048
2049            assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
2050            assert_eq!(resolved.resolved.id, expected);
2051            assert!(!resolved.used_fallback);
2052            assert!(resolved.resolved.supports_tools);
2053            assert!(resolved.resolved.supports_reasoning);
2054        }
2055    }
2056
2057    #[test]
2058    fn first_party_recent_provider_models_are_listed() {
2059        let registry = ModelRegistry::default();
2060        let models = registry.list();
2061
2062        for (provider, id) in [
2063            (ProviderKind::Zai, "GLM-5.2"),
2064            (ProviderKind::Stepfun, "step-3.7-flash"),
2065            (ProviderKind::Minimax, "MiniMax-M2.1"),
2066            (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
2067            (ProviderKind::Openmodel, "deepseek-v4-flash"),
2068            (ProviderKind::Meta, "muse-spark-1.2"),
2069            (ProviderKind::Xai, "grok-4.6"),
2070        ] {
2071            assert!(
2072                models
2073                    .iter()
2074                    .any(|model| model.provider == provider && model.id == id),
2075                "expected {provider:?} model {id} in registry"
2076            );
2077        }
2078    }
2079
2080    #[test]
2081    fn opencode_go_lists_only_current_chat_completions_models() {
2082        let registry = ModelRegistry::default();
2083        let listed = registry.list();
2084        let models: Vec<&str> = listed
2085            .iter()
2086            .filter(|model| model.provider == ProviderKind::OpencodeGo)
2087            .map(|model| model.id.as_str())
2088            .collect();
2089
2090        assert_eq!(
2091            models,
2092            vec![
2093                "deepseek-v4-pro",
2094                "grok-4.5",
2095                "glm-5.2",
2096                "glm-5.1",
2097                "kimi-k3",
2098                "kimi-k2.7-code",
2099                "kimi-k2.6",
2100                "deepseek-v4-flash",
2101                "mimo-v2.5",
2102                "mimo-v2.5-pro",
2103            ]
2104        );
2105
2106        let default = registry.resolve(None, Some(ProviderKind::OpencodeGo));
2107        assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
2108        assert_eq!(default.resolved.id, "deepseek-v4-pro");
2109
2110        for model in ["grok-4.5", "kimi-k3"] {
2111            for requested in [model.to_string(), format!("opencode-go/{model}")] {
2112                let resolved = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
2113                assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
2114                assert_eq!(resolved.resolved.id, model);
2115                assert!(!resolved.used_fallback);
2116            }
2117        }
2118
2119        for messages_only in [
2120            "minimax-m3",
2121            "minimax-m2.7",
2122            "minimax-m2.5",
2123            "qwen3.7-max",
2124            "qwen3.7-plus",
2125            "qwen3.6-plus",
2126        ] {
2127            for requested in [
2128                messages_only.to_string(),
2129                format!("opencode-go/{messages_only}"),
2130            ] {
2131                let rejected = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
2132                assert!(rejected.used_fallback, "{requested}");
2133                assert_eq!(
2134                    rejected.resolved.provider,
2135                    ProviderKind::OpencodeGo,
2136                    "{requested} must not cross-route"
2137                );
2138                assert_eq!(rejected.resolved.id, "deepseek-v4-pro", "{requested}");
2139            }
2140        }
2141    }
2142
2143    #[test]
2144    fn xai_grok_models_resolve_when_provider_hinted() {
2145        let registry = ModelRegistry::default();
2146
2147        let default = registry.resolve(None, Some(ProviderKind::Xai));
2148        assert_eq!(default.resolved.provider, ProviderKind::Xai);
2149        assert_eq!(default.resolved.id, "grok-4.6");
2150        assert!(default.used_fallback);
2151
2152        let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
2153        assert_eq!(alias.resolved.provider, ProviderKind::Xai);
2154        assert_eq!(alias.resolved.id, "grok-4.6");
2155        assert!(!alias.used_fallback);
2156
2157        let fast = registry.resolve(
2158            Some("grok-4.20-0309-non-reasoning"),
2159            Some(ProviderKind::Xai),
2160        );
2161        assert_eq!(fast.resolved.provider, ProviderKind::Xai);
2162        assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
2163        assert!(!fast.resolved.supports_reasoning);
2164    }
2165
2166    #[test]
2167    fn meta_muse_spark_resolves_when_provider_hinted() {
2168        let registry = ModelRegistry::default();
2169
2170        let default = registry.resolve(None, Some(ProviderKind::Meta));
2171        assert_eq!(default.resolved.provider, ProviderKind::Meta);
2172        assert_eq!(default.resolved.id, "muse-spark-1.2");
2173        assert!(default.used_fallback);
2174
2175        let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
2176        assert_eq!(alias.resolved.provider, ProviderKind::Meta);
2177        assert_eq!(alias.resolved.id, "muse-spark-1.2");
2178        assert!(!alias.used_fallback);
2179        assert_eq!(model_family("muse-spark-1.2"), ModelFamily::Meta);
2180    }
2181
2182    #[test]
2183    fn openai_gpt56_family_resolves_when_provider_hinted() {
2184        let registry = ModelRegistry::default();
2185        for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
2186            let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
2187            assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
2188            assert_eq!(resolved.resolved.id, model, "{model}");
2189            assert!(resolved.resolved.supports_tools, "{model}");
2190            assert!(resolved.resolved.supports_reasoning, "{model}");
2191            assert!(!resolved.used_fallback, "{model}");
2192        }
2193    }
2194
2195    #[test]
2196    fn grok_ids_stay_in_grok_family() {
2197        assert_eq!(model_family("grok-4.6"), ModelFamily::Grok);
2198        assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
2199        assert_eq!(
2200            model_family("grok-4.20-0309-non-reasoning"),
2201            ModelFamily::Grok
2202        );
2203    }
2204
2205    #[test]
2206    fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
2207        let registry = ModelRegistry::default();
2208
2209        let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
2210        assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
2211        assert_eq!(stepfun.resolved.id, "step-3.7-flash");
2212
2213        for (alias, expected) in [
2214            ("minimax", "MiniMax-M3"),
2215            ("minimax-m3", "MiniMax-M3"),
2216            ("minimax-m2.7", "MiniMax-M2.7"),
2217            ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
2218            ("minimax-m2.1", "MiniMax-M2.1"),
2219            ("minimax-m2", "MiniMax-M2"),
2220        ] {
2221            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
2222
2223            assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
2224            assert_eq!(resolved.resolved.id, expected);
2225            assert!(!resolved.used_fallback);
2226            assert!(resolved.resolved.supports_tools);
2227            assert!(resolved.resolved.supports_reasoning);
2228        }
2229    }
2230
2231    #[test]
2232    fn minimax_anthropic_models_resolve_when_provider_hinted() {
2233        let registry = ModelRegistry::default();
2234
2235        for (alias, expected) in [
2236            ("minimax-anthropic", "MiniMax-M3"),
2237            ("minimax-m3", "MiniMax-M3"),
2238            ("minimax-m2.7", "MiniMax-M2.7"),
2239        ] {
2240            let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
2241
2242            assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
2243            assert_eq!(resolved.resolved.id, expected);
2244            assert!(!resolved.used_fallback);
2245            assert!(resolved.resolved.supports_tools);
2246            assert!(resolved.resolved.supports_reasoning);
2247        }
2248    }
2249
2250    #[test]
2251    fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
2252        let registry = ModelRegistry::default();
2253        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
2254
2255        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2256        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2257    }
2258
2259    #[test]
2260    fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
2261        let registry = ModelRegistry::default();
2262
2263        for (alias, expected) in [
2264            ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
2265            ("qwen3.6-flash", "qwen/qwen3.6-flash"),
2266            ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
2267            ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
2268            ("qwen3.6-plus", "qwen/qwen3.6-plus"),
2269            ("gemma-4-31b-it", "google/gemma-4-31b-it"),
2270            ("glm-5.1", "z-ai/glm-5.1"),
2271            ("glm-5.2", "z-ai/glm-5.2"),
2272            ("glm-5.3", "z-ai/glm-5.3"),
2273            ("minimax-m3", "minimax/minimax-m3"),
2274            ("minimax-2.7", "minimax/minimax-m2.7"),
2275            ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
2276            ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
2277            ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
2278            ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
2279            (
2280                "nvidia/nemotron-3-ultra",
2281                "nvidia/nemotron-3-ultra-550b-a55b",
2282            ),
2283        ] {
2284            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
2285
2286            assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2287            assert_eq!(resolved.resolved.id, expected);
2288            assert!(resolved.resolved.supports_tools);
2289            assert!(resolved.resolved.supports_reasoning);
2290        }
2291    }
2292
2293    #[test]
2294    fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
2295        let registry = ModelRegistry::default();
2296        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
2297
2298        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
2299        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2300    }
2301
2302    #[test]
2303    fn together_inkling_keeps_published_wire_identity() {
2304        let registry = ModelRegistry::default();
2305        for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
2306            let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
2307
2308            assert_eq!(resolved.resolved.provider, ProviderKind::Together);
2309            assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
2310            assert!(resolved.resolved.supports_tools);
2311            assert!(resolved.resolved.supports_reasoning);
2312            assert!(!resolved.used_fallback);
2313        }
2314
2315        let unscoped = registry.resolve(Some("inkling"), None);
2316        assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
2317        assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
2318        assert!(!unscoped.used_fallback);
2319    }
2320
2321    #[test]
2322    fn registry_lists_and_resolves_every_v090_catalog_addition() {
2323        let registry = ModelRegistry::default();
2324        let advertised = [
2325            (ProviderKind::Anthropic, "claude-sonnet-5"),
2326            (ProviderKind::Anthropic, "claude-fable-5"),
2327            (ProviderKind::Openai, "gpt-5.3-codex"),
2328            (ProviderKind::Openai, "gpt-5.5"),
2329            (ProviderKind::Openai, "gpt-5.5-pro"),
2330            (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
2331            (ProviderKind::Arcee, "trinity-mini"),
2332        ];
2333
2334        let listed = registry.list();
2335        for (provider, model_id) in advertised {
2336            assert!(
2337                listed
2338                    .iter()
2339                    .any(|model| model.provider == provider && model.id == model_id),
2340                "missing {model_id} ({}) from model list",
2341                provider.as_str()
2342            );
2343            let resolved = registry.resolve(Some(model_id), Some(provider));
2344            assert_eq!(resolved.resolved.provider, provider, "{model_id}");
2345            assert_eq!(resolved.resolved.id, model_id, "{model_id}");
2346            assert!(!resolved.used_fallback, "{model_id}");
2347        }
2348    }
2349
2350    #[test]
2351    fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
2352        let registry = ModelRegistry::default();
2353
2354        let unscoped = registry.resolve(Some("gpt-5.5"), None);
2355        assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
2356        assert_eq!(unscoped.resolved.id, "gpt-5.5");
2357        assert!(!unscoped.used_fallback);
2358
2359        let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
2360        assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
2361        assert_eq!(codex.resolved.id, "gpt-5.5");
2362        assert!(!codex.used_fallback);
2363    }
2364
2365    #[test]
2366    fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
2367        let registry = ModelRegistry::default();
2368        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
2369
2370        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2371        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2372    }
2373
2374    #[test]
2375    fn vllm_default_uses_canonical_model_id() {
2376        let registry = ModelRegistry::default();
2377        let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
2378
2379        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2380        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2381    }
2382
2383    #[test]
2384    fn ollama_default_uses_small_local_model_id() {
2385        let registry = ModelRegistry::default();
2386        let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
2387
2388        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2389        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2390        assert!(resolved.resolved.supports_reasoning);
2391    }
2392
2393    #[test]
2394    fn ollama_cloud_default_uses_the_hosted_catalog_model_id() {
2395        let registry = ModelRegistry::default();
2396        let resolved = registry.resolve(None, Some(ProviderKind::OllamaCloud));
2397
2398        assert_eq!(resolved.resolved.provider, ProviderKind::OllamaCloud);
2399        assert_eq!(resolved.resolved.id, "gpt-oss:120b");
2400        assert!(resolved.resolved.supports_reasoning);
2401    }
2402
2403    #[test]
2404    fn ollama_requested_model_tag_is_preserved() {
2405        let registry = ModelRegistry::default();
2406        let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
2407
2408        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2409        assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
2410        assert!(!resolved.used_fallback);
2411    }
2412
2413    #[test]
2414    fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
2415        let registry = ModelRegistry::default();
2416        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
2417
2418        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2419        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2420    }
2421
2422    #[test]
2423    fn preserves_requested_model_casing_for_third_party_providers() {
2424        let registry = ModelRegistry::default();
2425        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
2426
2427        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2428        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2429    }
2430
2431    #[test]
2432    fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
2433        let registry = ModelRegistry::default();
2434        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
2435
2436        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2437        // Registry's canonical id is used even when user provides different casing
2438        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
2439    }
2440
2441    #[test]
2442    fn preserves_requested_model_casing_without_surrounding_whitespace() {
2443        let registry = ModelRegistry::default();
2444        let resolved = registry.resolve(Some("  DeepSeek-V4-Pro  "), None);
2445
2446        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2447        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2448    }
2449
2450    #[test]
2451    fn alias_match_does_not_override_requested_casing() {
2452        let registry = ModelRegistry::default();
2453        let resolved = registry.resolve(Some("deepseek-reasoner"), None);
2454
2455        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2456        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2457    }
2458
2459    #[test]
2460    fn model_family_classifies_known_model_ids() {
2461        assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
2462        assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
2463        assert_eq!(
2464            model_family("anthropic/claude-opus-4-7"),
2465            ModelFamily::Anthropic
2466        );
2467        assert_eq!(
2468            model_family("meta-llama/llama-3.3-70b-instruct"),
2469            ModelFamily::Meta
2470        );
2471        assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
2472    }
2473
2474    #[test]
2475    fn model_family_uses_underlying_model_for_router_ids() {
2476        assert_eq!(
2477            model_family("groq/llama-3.3-70b-versatile"),
2478            ModelFamily::Meta
2479        );
2480        assert_eq!(
2481            model_family("openrouter/openai/gpt-5.4"),
2482            ModelFamily::OpenAI
2483        );
2484        assert_eq!(
2485            model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
2486            ModelFamily::DeepSeek
2487        );
2488    }
2489
2490    #[test]
2491    fn model_family_covers_prominent_google_and_mistral_model_names() {
2492        assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
2493        assert_eq!(
2494            model_family("mistralai/mixtral-8x22b"),
2495            ModelFamily::Mistral
2496        );
2497        assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2498    }
2499
2500    #[test]
2501    fn model_family_falls_back_to_inferencer_for_unknown_models() {
2502        assert_eq!(
2503            model_family("custom-gateway/my-private-model"),
2504            ModelFamily::Inferencer
2505        );
2506        assert_eq!(model_family(""), ModelFamily::Inferencer);
2507    }
2508}