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            ModelInfo {
837                id: "claude-sonnet-4-6".to_string(),
838                provider: ProviderKind::Anthropic,
839                aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
840                supports_tools: true,
841                supports_reasoning: true,
842            },
843            ModelInfo {
844                id: "claude-haiku-4-5".to_string(),
845                provider: ProviderKind::Anthropic,
846                aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
847                supports_tools: true,
848                supports_reasoning: false,
849            },
850            ModelInfo {
851                id: "claude-sonnet-5".to_string(),
852                provider: ProviderKind::Anthropic,
853                aliases: vec!["sonnet-5".to_string()],
854                supports_tools: true,
855                supports_reasoning: true,
856            },
857            ModelInfo {
858                id: "claude-fable-5".to_string(),
859                provider: ProviderKind::Anthropic,
860                aliases: vec!["fable".to_string(), "fable-5".to_string()],
861                supports_tools: true,
862                supports_reasoning: true,
863            },
864            // OpenModel Anthropic-compatible Messages route
865            ModelInfo {
866                id: "deepseek-v4-flash".to_string(),
867                provider: ProviderKind::Openmodel,
868                aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
869                supports_tools: true,
870                supports_reasoning: true,
871            },
872            // MiniMax 2.7 (OpenRouter)
873            ModelInfo {
874                id: "minimax/minimax-m2.7".to_string(),
875                provider: ProviderKind::Openrouter,
876                aliases: vec![
877                    "minimax-2.7".to_string(),
878                    "minimax-2-7".to_string(),
879                    "openrouter-minimax-2.7".to_string(),
880                ],
881                supports_tools: true,
882                supports_reasoning: true,
883            },
884            ModelInfo {
885                id: "step-3.7-flash".to_string(),
886                provider: ProviderKind::Stepfun,
887                aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
888                supports_tools: true,
889                supports_reasoning: false,
890            },
891            ModelInfo {
892                id: "MiniMax-M3".to_string(),
893                provider: ProviderKind::Minimax,
894                aliases: vec![
895                    "minimax".to_string(),
896                    "minimax-m3".to_string(),
897                    "minimax-m-3".to_string(),
898                ],
899                supports_tools: true,
900                supports_reasoning: true,
901            },
902            ModelInfo {
903                id: "MiniMax-M2.7".to_string(),
904                provider: ProviderKind::Minimax,
905                aliases: vec![
906                    "minimax-m2.7".to_string(),
907                    "minimax-m2-7".to_string(),
908                    "minimax-m-2.7".to_string(),
909                    "minimax-m-2-7".to_string(),
910                ],
911                supports_tools: true,
912                supports_reasoning: true,
913            },
914            ModelInfo {
915                id: "MiniMax-M3".to_string(),
916                provider: ProviderKind::MinimaxAnthropic,
917                aliases: vec![
918                    "minimax-anthropic".to_string(),
919                    "minimax-anthropic-m3".to_string(),
920                    "minimax-m3".to_string(),
921                ],
922                supports_tools: true,
923                supports_reasoning: true,
924            },
925            ModelInfo {
926                id: "MiniMax-M2.7".to_string(),
927                provider: ProviderKind::MinimaxAnthropic,
928                aliases: vec![
929                    "minimax-anthropic-m2.7".to_string(),
930                    "minimax-anthropic-m2-7".to_string(),
931                    "minimax-m2.7".to_string(),
932                ],
933                supports_tools: true,
934                supports_reasoning: true,
935            },
936            ModelInfo {
937                id: "MiniMax-M2.7-highspeed".to_string(),
938                provider: ProviderKind::Minimax,
939                aliases: vec![
940                    "minimax-m2.7-highspeed".to_string(),
941                    "minimax-m2-7-highspeed".to_string(),
942                    "minimax-m-2.7-highspeed".to_string(),
943                    "minimax-m-2-7-highspeed".to_string(),
944                ],
945                supports_tools: true,
946                supports_reasoning: true,
947            },
948            ModelInfo {
949                id: "MiniMax-M2.5".to_string(),
950                provider: ProviderKind::Minimax,
951                aliases: vec![
952                    "minimax-m2.5".to_string(),
953                    "minimax-m2-5".to_string(),
954                    "minimax-m-2.5".to_string(),
955                    "minimax-m-2-5".to_string(),
956                ],
957                supports_tools: true,
958                supports_reasoning: true,
959            },
960            ModelInfo {
961                id: "MiniMax-M2.5-highspeed".to_string(),
962                provider: ProviderKind::Minimax,
963                aliases: vec![
964                    "minimax-m2.5-highspeed".to_string(),
965                    "minimax-m2-5-highspeed".to_string(),
966                    "minimax-m-2.5-highspeed".to_string(),
967                    "minimax-m-2-5-highspeed".to_string(),
968                ],
969                supports_tools: true,
970                supports_reasoning: true,
971            },
972            ModelInfo {
973                id: "MiniMax-M2.1".to_string(),
974                provider: ProviderKind::Minimax,
975                aliases: vec![
976                    "minimax-m2.1".to_string(),
977                    "minimax-m2-1".to_string(),
978                    "minimax-m-2.1".to_string(),
979                    "minimax-m-2-1".to_string(),
980                ],
981                supports_tools: true,
982                supports_reasoning: true,
983            },
984            ModelInfo {
985                id: "MiniMax-M2.1-highspeed".to_string(),
986                provider: ProviderKind::Minimax,
987                aliases: vec![
988                    "minimax-m2.1-highspeed".to_string(),
989                    "minimax-m2-1-highspeed".to_string(),
990                    "minimax-m-2.1-highspeed".to_string(),
991                    "minimax-m-2-1-highspeed".to_string(),
992                ],
993                supports_tools: true,
994                supports_reasoning: true,
995            },
996            ModelInfo {
997                id: "MiniMax-M2".to_string(),
998                provider: ProviderKind::Minimax,
999                aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
1000                supports_tools: true,
1001                supports_reasoning: true,
1002            },
1003            // NVIDIA Nemotron 3 Ultra (OpenRouter)
1004            ModelInfo {
1005                id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
1006                provider: ProviderKind::Openrouter,
1007                aliases: vec![
1008                    "nvidia/nemotron-3-ultra".to_string(),
1009                    "nemotron-3-ultra".to_string(),
1010                    "nemotron-3-ultra-550b-a55b".to_string(),
1011                    "nvidia-nemotron-3-ultra".to_string(),
1012                    "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
1013                ],
1014                supports_tools: true,
1015                supports_reasoning: true,
1016            },
1017            // DeepInfra (https://deepinfra.com)
1018            ModelInfo {
1019                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
1020                provider: ProviderKind::Deepinfra,
1021                aliases: vec![
1022                    "deepseek-v4-pro".to_string(),
1023                    "di-deepseek-v4-pro".to_string(),
1024                ],
1025                supports_tools: true,
1026                supports_reasoning: true,
1027            },
1028            ModelInfo {
1029                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
1030                provider: ProviderKind::Deepinfra,
1031                aliases: vec![
1032                    "deepseek-v4-flash".to_string(),
1033                    "di-deepseek-v4-flash".to_string(),
1034                ],
1035                supports_tools: true,
1036                supports_reasoning: true,
1037            },
1038            // Sakana AI Fugu (https://api.sakana.ai)
1039            ModelInfo {
1040                id: "fugu".to_string(),
1041                provider: ProviderKind::Sakana,
1042                aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
1043                supports_tools: true,
1044                supports_reasoning: false,
1045            },
1046            ModelInfo {
1047                id: "fugu-ultra-20260615".to_string(),
1048                provider: ProviderKind::Sakana,
1049                aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
1050                supports_tools: true,
1051                supports_reasoning: true,
1052            },
1053            // Meituan LongCat (https://longcat.chat/platform)
1054            ModelInfo {
1055                id: "LongCat-2.0".to_string(),
1056                provider: ProviderKind::LongCat,
1057                aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
1058                supports_tools: true,
1059                supports_reasoning: true,
1060            },
1061            // OpenCode Go Chat Completions models (https://opencode.ai/docs/go/).
1062            // Go models documented only on `/messages` are intentionally not
1063            // advertised by this OpenAI-compatible provider slice.
1064            ModelInfo {
1065                id: "deepseek-v4-pro".to_string(),
1066                provider: ProviderKind::OpencodeGo,
1067                aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
1068                supports_tools: true,
1069                supports_reasoning: true,
1070            },
1071            ModelInfo {
1072                id: "grok-4.5".to_string(),
1073                provider: ProviderKind::OpencodeGo,
1074                aliases: vec!["opencode-go/grok-4.5".to_string()],
1075                supports_tools: true,
1076                supports_reasoning: true,
1077            },
1078            // No glm-5.3 row (2026-08-03): OpenCode Go publishes no glm-5.3
1079            // model. The Z.ai/OpenRouter glm-5.3 rows inherit glm-5.2 metadata;
1080            // that inheritance is not evidence this gateway serves it.
1081            ModelInfo {
1082                id: "glm-5.2".to_string(),
1083                provider: ProviderKind::OpencodeGo,
1084                aliases: vec!["opencode-go/glm-5.2".to_string()],
1085                supports_tools: true,
1086                supports_reasoning: true,
1087            },
1088            ModelInfo {
1089                id: "glm-5.1".to_string(),
1090                provider: ProviderKind::OpencodeGo,
1091                aliases: vec!["opencode-go/glm-5.1".to_string()],
1092                supports_tools: true,
1093                supports_reasoning: true,
1094            },
1095            ModelInfo {
1096                id: "kimi-k3".to_string(),
1097                provider: ProviderKind::OpencodeGo,
1098                aliases: vec!["opencode-go/kimi-k3".to_string()],
1099                supports_tools: true,
1100                supports_reasoning: true,
1101            },
1102            ModelInfo {
1103                id: "kimi-k2.7-code".to_string(),
1104                provider: ProviderKind::OpencodeGo,
1105                aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
1106                supports_tools: true,
1107                supports_reasoning: true,
1108            },
1109            ModelInfo {
1110                id: "kimi-k2.6".to_string(),
1111                provider: ProviderKind::OpencodeGo,
1112                aliases: vec!["opencode-go/kimi-k2.6".to_string()],
1113                supports_tools: true,
1114                supports_reasoning: true,
1115            },
1116            ModelInfo {
1117                id: "deepseek-v4-flash".to_string(),
1118                provider: ProviderKind::OpencodeGo,
1119                aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
1120                supports_tools: true,
1121                supports_reasoning: true,
1122            },
1123            ModelInfo {
1124                id: "mimo-v2.5".to_string(),
1125                provider: ProviderKind::OpencodeGo,
1126                aliases: vec!["opencode-go/mimo-v2.5".to_string()],
1127                supports_tools: true,
1128                supports_reasoning: true,
1129            },
1130            ModelInfo {
1131                id: "mimo-v2.5-pro".to_string(),
1132                provider: ProviderKind::OpencodeGo,
1133                aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
1134                supports_tools: true,
1135                supports_reasoning: true,
1136            },
1137            // Meta Model API / Muse Spark. Keep these in step with
1138            // `DEFAULT_META_MODEL` in config's provider_defaults and with the
1139            // bundled models.dev catalog: this registry resolves the `muse`
1140            // aliases for the CLI and app-server, so a stale id here silently
1141            // routes them somewhere the configured default never points.
1142            ModelInfo {
1143                id: "muse-spark-1.2".to_string(),
1144                provider: ProviderKind::Meta,
1145                aliases: vec!["muse-spark".to_string(), "muse".to_string()],
1146                supports_tools: true,
1147                supports_reasoning: true,
1148            },
1149            ModelInfo {
1150                id: "muse-spark-1.2-contributor".to_string(),
1151                provider: ProviderKind::Meta,
1152                aliases: vec!["muse-spark-contributor".to_string()],
1153                supports_tools: true,
1154                supports_reasoning: true,
1155            },
1156            // xAI / Grok (https://api.x.ai/v1)
1157            ModelInfo {
1158                id: "grok-4.6".to_string(),
1159                provider: ProviderKind::Xai,
1160                aliases: vec!["grok".to_string()],
1161                supports_tools: true,
1162                supports_reasoning: true,
1163            },
1164            ModelInfo {
1165                id: "grok-4.5".to_string(),
1166                provider: ProviderKind::Xai,
1167                aliases: vec!["xai-grok-4.5".to_string()],
1168                supports_tools: true,
1169                supports_reasoning: true,
1170            },
1171            ModelInfo {
1172                id: "grok-4.3".to_string(),
1173                provider: ProviderKind::Xai,
1174                aliases: vec!["xai-grok-4.3".to_string()],
1175                supports_tools: true,
1176                supports_reasoning: true,
1177            },
1178            ModelInfo {
1179                id: "grok-build".to_string(),
1180                provider: ProviderKind::Xai,
1181                aliases: vec!["xai-grok-build".to_string()],
1182                supports_tools: true,
1183                supports_reasoning: true,
1184            },
1185            ModelInfo {
1186                id: "grok-composer-2.5-fast".to_string(),
1187                provider: ProviderKind::Xai,
1188                aliases: vec!["xai-grok-composer".to_string()],
1189                supports_tools: true,
1190                supports_reasoning: false,
1191            },
1192            ModelInfo {
1193                id: "grok-4.20-0309-reasoning".to_string(),
1194                provider: ProviderKind::Xai,
1195                aliases: vec!["xai-grok-reasoning".to_string()],
1196                supports_tools: true,
1197                supports_reasoning: true,
1198            },
1199            ModelInfo {
1200                id: "grok-4.20-0309-non-reasoning".to_string(),
1201                provider: ProviderKind::Xai,
1202                aliases: vec!["xai-grok-fast".to_string()],
1203                supports_tools: true,
1204                supports_reasoning: false,
1205            },
1206            ModelInfo {
1207                id: "gemini-3.1-pro-preview".to_string(),
1208                provider: ProviderKind::Google,
1209                aliases: vec!["gemini-3.1-pro".to_string()],
1210                supports_tools: true,
1211                supports_reasoning: true,
1212            },
1213            ModelInfo {
1214                id: "gemini-3-pro-preview".to_string(),
1215                provider: ProviderKind::Google,
1216                aliases: vec!["gemini-3-pro".to_string()],
1217                supports_tools: true,
1218                supports_reasoning: true,
1219            },
1220            ModelInfo {
1221                id: "gemini-3.6-flash".to_string(),
1222                provider: ProviderKind::Google,
1223                aliases: vec![],
1224                supports_tools: true,
1225                supports_reasoning: true,
1226            },
1227            ModelInfo {
1228                id: "gemini-3.5-flash".to_string(),
1229                provider: ProviderKind::Google,
1230                aliases: vec![],
1231                supports_tools: true,
1232                supports_reasoning: true,
1233            },
1234            ModelInfo {
1235                id: "gemini-3.5-flash-lite".to_string(),
1236                provider: ProviderKind::Google,
1237                aliases: vec![],
1238                supports_tools: true,
1239                supports_reasoning: false,
1240            },
1241            ModelInfo {
1242                id: "gemini-2.5-pro".to_string(),
1243                provider: ProviderKind::Google,
1244                aliases: vec![],
1245                supports_tools: true,
1246                supports_reasoning: true,
1247            },
1248            ModelInfo {
1249                id: "gemini-2.5-flash".to_string(),
1250                provider: ProviderKind::Google,
1251                aliases: vec![],
1252                supports_tools: true,
1253                supports_reasoning: true,
1254            },
1255            ModelInfo {
1256                id: "mistral-code-latest".to_string(),
1257                provider: ProviderKind::Mistral,
1258                aliases: vec![
1259                    "codestral".to_string(),
1260                    "codestral-latest".to_string(),
1261                    "mistral-code".to_string(),
1262                ],
1263                supports_tools: true,
1264                supports_reasoning: false,
1265            },
1266            ModelInfo {
1267                id: "mistral-medium-latest".to_string(),
1268                provider: ProviderKind::Mistral,
1269                aliases: vec![
1270                    "mistral-medium".to_string(),
1271                    "mistral-medium-3-5".to_string(),
1272                ],
1273                supports_tools: true,
1274                supports_reasoning: true,
1275            },
1276            ModelInfo {
1277                id: "mistral-small-latest".to_string(),
1278                provider: ProviderKind::Mistral,
1279                aliases: vec![
1280                    "mistral-small".to_string(),
1281                    "mistral-small-2603".to_string(),
1282                ],
1283                supports_tools: true,
1284                supports_reasoning: true,
1285            },
1286            ModelInfo {
1287                id: "magistral-small-latest".to_string(),
1288                provider: ProviderKind::Mistral,
1289                aliases: vec!["magistral".to_string(), "magistral-small".to_string()],
1290                supports_tools: true,
1291                supports_reasoning: true,
1292            },
1293            ModelInfo {
1294                id: "mistral-large-latest".to_string(),
1295                provider: ProviderKind::Mistral,
1296                aliases: vec!["mistral-large".to_string()],
1297                supports_tools: true,
1298                supports_reasoning: false,
1299            },
1300        ];
1301        Self::new(models)
1302    }
1303}
1304
1305impl ModelRegistry {
1306    /// Creates a new registry from a list of [`ModelInfo`] entries.
1307    ///
1308    /// Builds an internal alias map for fast lookup by model id or alias.
1309    /// If multiple models share the same id or alias, the first one registered
1310    /// takes priority.
1311    #[must_use]
1312    pub fn new(models: Vec<ModelInfo>) -> Self {
1313        let mut alias_map = HashMap::new();
1314        for (idx, model) in models.iter().enumerate() {
1315            alias_map.entry(normalize(&model.id)).or_insert(idx);
1316            for alias in &model.aliases {
1317                alias_map.entry(normalize(alias)).or_insert(idx);
1318            }
1319        }
1320        Self { models, alias_map }
1321    }
1322
1323    /// Returns a clone of all models in the registry.
1324    #[must_use]
1325    pub fn list(&self) -> Vec<ModelInfo> {
1326        self.models.clone()
1327    }
1328
1329    /// Resolves a user-requested model name to a concrete [`ModelInfo`].
1330    ///
1331    /// Resolution follows this priority order:
1332    /// 1. If the provider is Ollama, the requested name is used as-is (to
1333    ///    support arbitrary local model tags like `qwen2.5-coder:7b`).
1334    /// 2. If a `provider_hint` is given, search for a model matching that
1335    ///    provider whose id or alias matches the request (case-insensitive).
1336    /// 3. Look up the alias map for a case-insensitive match.
1337    /// 4. Fall back to the first model belonging to the hinted provider
1338    ///    (or DeepSeek if no hint was given).
1339    /// 5. As a last resort, fall back to the first model in the registry.
1340    #[must_use]
1341    pub fn resolve(
1342        &self,
1343        requested: Option<&str>,
1344        provider_hint: Option<ProviderKind>,
1345    ) -> ModelResolution {
1346        let mut fallback_chain = Vec::new();
1347
1348        if let Some(name) = requested {
1349            fallback_chain.push(format!("requested:{name}"));
1350            if matches!(
1351                provider_hint,
1352                Some(ProviderKind::Ollama | ProviderKind::OllamaCloud)
1353            ) {
1354                return ModelResolution {
1355                    requested: Some(name.to_string()),
1356                    resolved: ModelInfo {
1357                        id: name.trim().to_string(),
1358                        provider: provider_hint.expect("matched provider hint"),
1359                        aliases: Vec::new(),
1360                        supports_tools: true,
1361                        supports_reasoning: false,
1362                    },
1363                    used_fallback: false,
1364                    fallback_chain,
1365                };
1366            }
1367            // OpenCode Go's catalog spans Chat Completions and Anthropic
1368            // Messages, while Codewhale's provider slice intentionally speaks
1369            // Chat only. Resolve a hinted Go model through the shared Chat
1370            // allowlist and never fall through to a same-named global alias on
1371            // OpenRouter or MiniMax.
1372            if provider_hint == Some(ProviderKind::OpencodeGo)
1373                && let Some(canonical) = opencode_go_chat_model_id(name)
1374                && let Some(model) = self
1375                    .models
1376                    .iter()
1377                    .find(|model| {
1378                        model.provider == ProviderKind::OpencodeGo
1379                            && model.id.eq_ignore_ascii_case(canonical)
1380                    })
1381                    .cloned()
1382            {
1383                return ModelResolution {
1384                    requested: Some(name.to_string()),
1385                    resolved: model,
1386                    used_fallback: false,
1387                    fallback_chain,
1388                };
1389            }
1390            if provider_hint != Some(ProviderKind::OpencodeGo)
1391                && let Some(provider) = provider_hint
1392                && let Some(model) = self
1393                    .models
1394                    .iter()
1395                    .find(|m| m.provider == provider && model_matches(m, name))
1396                    .cloned()
1397            {
1398                return ModelResolution {
1399                    requested: Some(name.to_string()),
1400                    resolved: model,
1401                    used_fallback: false,
1402                    fallback_chain,
1403                };
1404            }
1405            if provider_hint == Some(ProviderKind::Atlascloud)
1406                && let Some(model) = atlascloud_passthrough_model(name)
1407            {
1408                return ModelResolution {
1409                    requested: Some(name.to_string()),
1410                    resolved: model,
1411                    used_fallback: false,
1412                    fallback_chain,
1413                };
1414            }
1415            if provider_hint == Some(ProviderKind::Arcee)
1416                && let Some(model) = arcee_passthrough_model(name)
1417            {
1418                return ModelResolution {
1419                    requested: Some(name.to_string()),
1420                    resolved: model,
1421                    used_fallback: false,
1422                    fallback_chain,
1423                };
1424            }
1425            if provider_hint == Some(ProviderKind::XiaomiMimo)
1426                && let Some(model) = xiaomi_mimo_passthrough_model(name)
1427            {
1428                return ModelResolution {
1429                    requested: Some(name.to_string()),
1430                    resolved: model,
1431                    used_fallback: false,
1432                    fallback_chain,
1433                };
1434            }
1435            // The global alias map is a *provider-less* convenience lookup. It
1436            // must never answer a provider-scoped question with another
1437            // vendor's model: before this fix, `--provider moonshot` asking for
1438            // `kimi-k3` was answered with OpenCode Go's `kimi-k3` because the
1439            // hinted provider had no such id. A hinted request that the hinted
1440            // provider cannot serve falls through to that provider's default
1441            // with `used_fallback: true`, which callers already surface.
1442            if provider_hint != Some(ProviderKind::OpencodeGo)
1443                && let Some(idx) = self.alias_map.get(&normalize(name))
1444                && provider_hint.is_none_or(|hint| self.models[*idx].provider == hint)
1445            {
1446                return ModelResolution {
1447                    requested: Some(name.to_string()),
1448                    resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1449                    used_fallback: false,
1450                    fallback_chain,
1451                };
1452            }
1453        }
1454
1455        let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1456        fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1457        if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1458            return ModelResolution {
1459                requested: requested.map(ToOwned::to_owned),
1460                resolved: model,
1461                used_fallback: true,
1462                fallback_chain,
1463            };
1464        }
1465
1466        let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1467            id: "deepseek-v4-pro".to_string(),
1468            provider: ProviderKind::Deepseek,
1469            aliases: Vec::new(),
1470            supports_tools: true,
1471            supports_reasoning: true,
1472        });
1473        fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1474        ModelResolution {
1475            requested: requested.map(ToOwned::to_owned),
1476            resolved: final_fallback,
1477            used_fallback: true,
1478            fallback_chain,
1479        }
1480    }
1481}
1482
1483fn normalize(value: &str) -> String {
1484    value.trim().to_ascii_lowercase()
1485}
1486
1487#[must_use]
1488/// Classify a model identifier by its underlying model family.
1489pub fn model_family(model_id: &str) -> ModelFamily {
1490    let normalized = normalize(model_id);
1491    if normalized.is_empty() {
1492        return ModelFamily::Inferencer;
1493    }
1494
1495    if normalized.contains("deepseek") {
1496        return ModelFamily::DeepSeek;
1497    }
1498    if normalized.contains("claude") || normalized.contains("anthropic") {
1499        return ModelFamily::Anthropic;
1500    }
1501    if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1502        return ModelFamily::GptOss;
1503    }
1504    if normalized.starts_with("gpt-")
1505        || normalized.contains("/gpt-")
1506        || normalized.contains("openai/")
1507    {
1508        return ModelFamily::OpenAI;
1509    }
1510    if normalized.contains("gemini")
1511        || normalized.contains("gemma")
1512        || normalized.contains("google/")
1513    {
1514        return ModelFamily::Google;
1515    }
1516    if normalized.contains("llama")
1517        || normalized.contains("muse-spark")
1518        || normalized.contains("meta-")
1519        || normalized.contains("meta/")
1520    {
1521        return ModelFamily::Meta;
1522    }
1523    if normalized.contains("mistral")
1524        || normalized.contains("mixtral")
1525        || normalized.contains("codestral")
1526    {
1527        return ModelFamily::Mistral;
1528    }
1529    if normalized.contains("qwen") {
1530        return ModelFamily::Qwen;
1531    }
1532    if normalized.contains("grok") {
1533        return ModelFamily::Grok;
1534    }
1535    if normalized.contains("cohere") || normalized.contains("command-r") {
1536        return ModelFamily::Cohere;
1537    }
1538
1539    ModelFamily::Inferencer
1540}
1541
1542fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1543    let requested = normalize(requested);
1544    normalize(&model.id) == requested
1545        || model
1546            .aliases
1547            .iter()
1548            .any(|alias| normalize(alias) == requested)
1549}
1550
1551fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1552    let requested = requested.trim();
1553    if model.id.eq_ignore_ascii_case(requested) {
1554        model.id = requested.to_string();
1555    }
1556    model
1557}
1558
1559fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1560    let requested = requested.trim();
1561    if requested.is_empty() || !requested.contains('/') {
1562        return None;
1563    }
1564
1565    Some(ModelInfo {
1566        id: requested.to_string(),
1567        provider: ProviderKind::Atlascloud,
1568        aliases: Vec::new(),
1569        supports_tools: true,
1570        supports_reasoning: true,
1571    })
1572}
1573
1574fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1575    let requested = requested.trim();
1576    if requested.is_empty() {
1577        return None;
1578    }
1579    let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1580
1581    Some(ModelInfo {
1582        id: requested.to_string(),
1583        provider: ProviderKind::Arcee,
1584        aliases: Vec::new(),
1585        supports_tools: true,
1586        supports_reasoning,
1587    })
1588}
1589
1590fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1591    let requested = requested.trim();
1592    if requested.is_empty() || requested.chars().any(char::is_control) {
1593        return None;
1594    }
1595
1596    Some(ModelInfo {
1597        id: requested.to_string(),
1598        provider: ProviderKind::XiaomiMimo,
1599        aliases: Vec::new(),
1600        supports_tools: true,
1601        supports_reasoning: true,
1602    })
1603}
1604
1605#[cfg(test)]
1606mod tests {
1607    use super::*;
1608
1609    #[test]
1610    fn model_registry_new_builds_alias_map_correctly() {
1611        let models = vec![
1612            ModelInfo {
1613                id: "Model-A".to_string(),
1614                provider: ProviderKind::Deepseek,
1615                aliases: vec!["alias-1".to_string(), " ALIAS-2 ".to_string()],
1616                supports_tools: true,
1617                supports_reasoning: false,
1618            },
1619            ModelInfo {
1620                id: "model-b".to_string(),
1621                provider: ProviderKind::Deepseek,
1622                aliases: vec!["alias-1".to_string()], // Duplicate alias, should not override
1623                supports_tools: true,
1624                supports_reasoning: true,
1625            },
1626        ];
1627
1628        let registry = ModelRegistry::new(models);
1629
1630        assert_eq!(registry.alias_map.len(), 4); // "model-a", "alias-1", "alias-2", "model-b"
1631        assert_eq!(registry.alias_map.get("model-a"), Some(&0));
1632        assert_eq!(registry.alias_map.get("alias-1"), Some(&0)); // First one wins
1633        assert_eq!(registry.alias_map.get("alias-2"), Some(&0)); // Normalized
1634        assert_eq!(registry.alias_map.get("model-b"), Some(&1));
1635    }
1636
1637    #[test]
1638    fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1639        let registry = ModelRegistry::default();
1640        let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1641
1642        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1643        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1644    }
1645
1646    #[test]
1647    fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1648        let registry = ModelRegistry::default();
1649        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1650
1651        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1652        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1653    }
1654
1655    #[test]
1656    fn nvidia_nim_default_uses_catalog_model_id() {
1657        let registry = ModelRegistry::default();
1658        let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1659
1660        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1661        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1662    }
1663
1664    #[test]
1665    fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1666        let registry = ModelRegistry::default();
1667        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1668
1669        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1670        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1671    }
1672
1673    #[test]
1674    fn atlascloud_default_uses_namespaced_model_id() {
1675        let registry = ModelRegistry::default();
1676        let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1677
1678        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1679        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1680        assert!(resolved.resolved.supports_reasoning);
1681    }
1682
1683    #[test]
1684    fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1685        let registry = ModelRegistry::default();
1686        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1687
1688        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1689        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1690    }
1691
1692    #[test]
1693    fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1694        let registry = ModelRegistry::default();
1695        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1696
1697        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1698        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1699    }
1700
1701    #[test]
1702    fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1703        let registry = ModelRegistry::default();
1704        let resolved =
1705            registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1706
1707        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1708        assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1709        assert!(resolved.resolved.supports_tools);
1710        assert!(resolved.resolved.supports_reasoning);
1711        assert!(!resolved.used_fallback);
1712    }
1713
1714    #[test]
1715    fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1716        let registry = ModelRegistry::default();
1717        let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1718
1719        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1720        assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1721        assert!(!resolved.used_fallback);
1722    }
1723
1724    #[test]
1725    fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1726        let registry = ModelRegistry::default();
1727        let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1728
1729        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1730        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1731        assert!(resolved.used_fallback);
1732    }
1733
1734    #[test]
1735    fn openrouter_default_uses_namespaced_model_id() {
1736        let registry = ModelRegistry::default();
1737        let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1738
1739        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1740        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1741    }
1742
1743    #[test]
1744    fn xiaomi_mimo_default_uses_canonical_model_id() {
1745        let registry = ModelRegistry::default();
1746        let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1747
1748        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1749        assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1750        assert!(resolved.resolved.supports_reasoning);
1751    }
1752
1753    #[test]
1754    fn moonshot_default_and_aliases_use_kimi_k27_code() {
1755        let registry = ModelRegistry::default();
1756
1757        for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1758            let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1759
1760            assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1761            assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1762            assert!(resolved.resolved.supports_tools);
1763            assert!(resolved.resolved.supports_reasoning);
1764        }
1765    }
1766
1767    #[test]
1768    fn moonshot_explicit_kimi_k26_remains_available() {
1769        let registry = ModelRegistry::default();
1770        let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1771
1772        assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1773        assert_eq!(resolved.resolved.id, "kimi-k2.6");
1774        assert!(resolved.resolved.supports_reasoning);
1775    }
1776
1777    /// v0.9.1 dogfood report: a user ran `--provider moonshot --model kimi-k3` and was told
1778    /// the model was `kimi-k2.7-code`. The registry knew neither Moonshot K3
1779    /// product, so the explicit request fell through to the provider default.
1780    #[test]
1781    fn moonshot_resolves_both_k3_products_without_crossing_them() {
1782        let registry = ModelRegistry::default();
1783
1784        for (requested, expected) in [("kimi-k3", "kimi-k3"), ("k3", "k3")] {
1785            let resolved = registry.resolve(Some(requested), Some(ProviderKind::Moonshot));
1786
1787            assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1788            assert_eq!(resolved.resolved.id, expected, "{resolved:?}");
1789            assert!(
1790                !resolved.used_fallback,
1791                "an explicit Moonshot K3 request is not a fallback: {resolved:?}"
1792            );
1793        }
1794    }
1795
1796    /// The bare `k3` id belongs to the Kimi Code coding-plan endpoint and
1797    /// `kimi-k3` to the direct platform endpoint. Neither may be laundered
1798    /// into the other's id by alias expansion.
1799    #[test]
1800    fn moonshot_k3_ids_are_never_rewritten_into_each_other() {
1801        let registry = ModelRegistry::default();
1802
1803        assert_eq!(
1804            registry
1805                .resolve(Some("kimi-k3"), Some(ProviderKind::Moonshot))
1806                .resolved
1807                .id,
1808            "kimi-k3"
1809        );
1810        assert_eq!(
1811            registry
1812                .resolve(Some("k3"), Some(ProviderKind::Moonshot))
1813                .resolved
1814                .id,
1815            "k3"
1816        );
1817    }
1818
1819    /// A provider-scoped question must never be answered with another
1820    /// vendor's model. `kimi-k3` also exists in the OpenCode Go catalog;
1821    /// before this fix that entry answered `--provider moonshot` requests.
1822    #[test]
1823    fn a_provider_hint_never_resolves_to_another_providers_model() {
1824        let registry = ModelRegistry::default();
1825
1826        let resolved = registry.resolve(Some("glm-5.2"), Some(ProviderKind::Moonshot));
1827        assert_eq!(
1828            resolved.resolved.provider,
1829            ProviderKind::Moonshot,
1830            "a Moonshot request must not be answered by Z.ai: {resolved:?}"
1831        );
1832        assert!(
1833            resolved.used_fallback,
1834            "an unservable id must be reported as a fallback, not as the request: {resolved:?}"
1835        );
1836
1837        let go = registry.resolve(Some("kimi-k3"), Some(ProviderKind::OpencodeGo));
1838        assert_eq!(go.resolved.provider, ProviderKind::OpencodeGo);
1839        assert_eq!(go.resolved.id, "kimi-k3");
1840    }
1841
1842    #[test]
1843    fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1844        let registry = ModelRegistry::default();
1845        let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1846        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1847        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1848        assert!(!resolved.resolved.supports_tools);
1849        assert!(!resolved.resolved.supports_reasoning);
1850
1851        let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1852        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1853
1854        let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1855        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1856    }
1857
1858    #[test]
1859    fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1860        let registry = ModelRegistry::default();
1861
1862        let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1863        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1864        assert_eq!(resolved.resolved.id, "mimo-v2.5");
1865        assert!(resolved.resolved.supports_tools);
1866    }
1867
1868    #[test]
1869    fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1870        let registry = ModelRegistry::default();
1871        let resolved =
1872            registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1873
1874        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1875        assert_eq!(resolved.resolved.id, "account-custom-mimo");
1876        assert!(!resolved.used_fallback);
1877    }
1878
1879    #[test]
1880    fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1881        let registry = ModelRegistry::default();
1882        let resolved = registry.resolve(
1883            Some("deepseek/deepseek-v4-pro"),
1884            Some(ProviderKind::XiaomiMimo),
1885        );
1886
1887        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1888        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1889        assert!(!resolved.used_fallback);
1890    }
1891
1892    #[test]
1893    fn wanjie_ark_default_uses_reasoner_model_id() {
1894        let registry = ModelRegistry::default();
1895        let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1896
1897        assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1898        assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1899        assert!(resolved.resolved.supports_reasoning);
1900    }
1901
1902    #[test]
1903    fn novita_default_uses_namespaced_model_id() {
1904        let registry = ModelRegistry::default();
1905        let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1906
1907        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1908        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1909    }
1910
1911    #[test]
1912    fn fireworks_default_uses_canonical_model_id() {
1913        let registry = ModelRegistry::default();
1914        let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1915
1916        assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1917        assert_eq!(
1918            resolved.resolved.id,
1919            "accounts/fireworks/models/deepseek-v4-pro"
1920        );
1921    }
1922
1923    #[test]
1924    fn siliconflow_default_uses_canonical_pro_model_id() {
1925        let registry = ModelRegistry::default();
1926        let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1927
1928        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1929        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1930        assert!(resolved.resolved.supports_reasoning);
1931    }
1932
1933    #[test]
1934    fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1935        let registry = ModelRegistry::default();
1936        let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1937
1938        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1939        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1940        assert!(resolved.resolved.supports_reasoning);
1941    }
1942
1943    #[test]
1944    fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1945        let registry = ModelRegistry::default();
1946        let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1947
1948        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1949        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1950        assert!(resolved.resolved.supports_reasoning);
1951    }
1952
1953    #[test]
1954    fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1955        let registry = ModelRegistry::default();
1956        let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1957
1958        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1959        assert_eq!(resolved.resolved.id, "trinity-mini");
1960        assert!(resolved.resolved.supports_reasoning);
1961        assert!(!resolved.used_fallback);
1962    }
1963
1964    #[test]
1965    fn arcee_provider_hint_preserves_explicit_future_model_id() {
1966        let registry = ModelRegistry::default();
1967        let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1968
1969        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1970        assert_eq!(resolved.resolved.id, "trinity-large-next");
1971        assert!(!resolved.resolved.supports_reasoning);
1972        assert!(!resolved.used_fallback);
1973    }
1974
1975    #[test]
1976    fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1977        let registry = ModelRegistry::default();
1978        let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1979
1980        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1981        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1982    }
1983
1984    #[test]
1985    fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1986        let registry = ModelRegistry::default();
1987        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1988
1989        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1990        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1991    }
1992
1993    #[test]
1994    fn sglang_default_uses_canonical_model_id() {
1995        let registry = ModelRegistry::default();
1996        let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1997
1998        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1999        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2000    }
2001
2002    #[test]
2003    fn zai_direct_models_resolve_when_provider_hinted() {
2004        let registry = ModelRegistry::default();
2005
2006        // Keep the agent registry fallback aligned with codewhale-config's
2007        // DEFAULT_ZAI_MODEL.
2008        let default = registry.resolve(None, Some(ProviderKind::Zai));
2009        assert_eq!(default.resolved.provider, ProviderKind::Zai);
2010        assert_eq!(default.resolved.id, "GLM-5.3");
2011        assert!(default.used_fallback);
2012        assert_eq!(default.fallback_chain, ["provider_default:zai"]);
2013
2014        for (alias, expected) in [
2015            ("GLM-5.1", "GLM-5.1"),
2016            ("glm-5-1", "GLM-5.1"),
2017            ("GLM-5.2", "GLM-5.2"),
2018            ("glm-5.2", "GLM-5.2"),
2019            ("zai-glm-5-2", "GLM-5.2"),
2020            ("GLM-5.3", "GLM-5.3"),
2021            ("glm-5.3", "GLM-5.3"),
2022            ("glm-5-3", "GLM-5.3"),
2023            ("zai-glm-5-3", "GLM-5.3"),
2024            ("GLM-5-Turbo", "GLM-5-Turbo"),
2025            ("glm-5-turbo", "GLM-5-Turbo"),
2026            ("zai-glm-5-turbo", "GLM-5-Turbo"),
2027        ] {
2028            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
2029
2030            assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
2031            assert_eq!(resolved.resolved.id, expected);
2032            assert!(!resolved.used_fallback);
2033            assert!(resolved.resolved.supports_tools);
2034            assert!(resolved.resolved.supports_reasoning);
2035        }
2036    }
2037
2038    #[test]
2039    fn first_party_recent_provider_models_are_listed() {
2040        let registry = ModelRegistry::default();
2041        let models = registry.list();
2042
2043        for (provider, id) in [
2044            (ProviderKind::Zai, "GLM-5.2"),
2045            (ProviderKind::Stepfun, "step-3.7-flash"),
2046            (ProviderKind::Minimax, "MiniMax-M2.1"),
2047            (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
2048            (ProviderKind::Openmodel, "deepseek-v4-flash"),
2049            (ProviderKind::Meta, "muse-spark-1.2"),
2050            (ProviderKind::Xai, "grok-4.6"),
2051        ] {
2052            assert!(
2053                models
2054                    .iter()
2055                    .any(|model| model.provider == provider && model.id == id),
2056                "expected {provider:?} model {id} in registry"
2057            );
2058        }
2059    }
2060
2061    #[test]
2062    fn opencode_go_lists_only_current_chat_completions_models() {
2063        let registry = ModelRegistry::default();
2064        let listed = registry.list();
2065        let models: Vec<&str> = listed
2066            .iter()
2067            .filter(|model| model.provider == ProviderKind::OpencodeGo)
2068            .map(|model| model.id.as_str())
2069            .collect();
2070
2071        assert_eq!(
2072            models,
2073            vec![
2074                "deepseek-v4-pro",
2075                "grok-4.5",
2076                "glm-5.2",
2077                "glm-5.1",
2078                "kimi-k3",
2079                "kimi-k2.7-code",
2080                "kimi-k2.6",
2081                "deepseek-v4-flash",
2082                "mimo-v2.5",
2083                "mimo-v2.5-pro",
2084            ]
2085        );
2086
2087        let default = registry.resolve(None, Some(ProviderKind::OpencodeGo));
2088        assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
2089        assert_eq!(default.resolved.id, "deepseek-v4-pro");
2090
2091        for model in ["grok-4.5", "kimi-k3"] {
2092            for requested in [model.to_string(), format!("opencode-go/{model}")] {
2093                let resolved = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
2094                assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
2095                assert_eq!(resolved.resolved.id, model);
2096                assert!(!resolved.used_fallback);
2097            }
2098        }
2099
2100        for messages_only in [
2101            "minimax-m3",
2102            "minimax-m2.7",
2103            "minimax-m2.5",
2104            "qwen3.7-max",
2105            "qwen3.7-plus",
2106            "qwen3.6-plus",
2107        ] {
2108            for requested in [
2109                messages_only.to_string(),
2110                format!("opencode-go/{messages_only}"),
2111            ] {
2112                let rejected = registry.resolve(Some(&requested), Some(ProviderKind::OpencodeGo));
2113                assert!(rejected.used_fallback, "{requested}");
2114                assert_eq!(
2115                    rejected.resolved.provider,
2116                    ProviderKind::OpencodeGo,
2117                    "{requested} must not cross-route"
2118                );
2119                assert_eq!(rejected.resolved.id, "deepseek-v4-pro", "{requested}");
2120            }
2121        }
2122    }
2123
2124    #[test]
2125    fn xai_grok_models_resolve_when_provider_hinted() {
2126        let registry = ModelRegistry::default();
2127
2128        let default = registry.resolve(None, Some(ProviderKind::Xai));
2129        assert_eq!(default.resolved.provider, ProviderKind::Xai);
2130        assert_eq!(default.resolved.id, "grok-4.6");
2131        assert!(default.used_fallback);
2132
2133        let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
2134        assert_eq!(alias.resolved.provider, ProviderKind::Xai);
2135        assert_eq!(alias.resolved.id, "grok-4.6");
2136        assert!(!alias.used_fallback);
2137
2138        let fast = registry.resolve(
2139            Some("grok-4.20-0309-non-reasoning"),
2140            Some(ProviderKind::Xai),
2141        );
2142        assert_eq!(fast.resolved.provider, ProviderKind::Xai);
2143        assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
2144        assert!(!fast.resolved.supports_reasoning);
2145    }
2146
2147    #[test]
2148    fn meta_muse_spark_resolves_when_provider_hinted() {
2149        let registry = ModelRegistry::default();
2150
2151        let default = registry.resolve(None, Some(ProviderKind::Meta));
2152        assert_eq!(default.resolved.provider, ProviderKind::Meta);
2153        assert_eq!(default.resolved.id, "muse-spark-1.2");
2154        assert!(default.used_fallback);
2155
2156        let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
2157        assert_eq!(alias.resolved.provider, ProviderKind::Meta);
2158        assert_eq!(alias.resolved.id, "muse-spark-1.2");
2159        assert!(!alias.used_fallback);
2160        assert_eq!(model_family("muse-spark-1.2"), ModelFamily::Meta);
2161    }
2162
2163    #[test]
2164    fn openai_gpt56_family_resolves_when_provider_hinted() {
2165        let registry = ModelRegistry::default();
2166        for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
2167            let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
2168            assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
2169            assert_eq!(resolved.resolved.id, model, "{model}");
2170            assert!(resolved.resolved.supports_tools, "{model}");
2171            assert!(resolved.resolved.supports_reasoning, "{model}");
2172            assert!(!resolved.used_fallback, "{model}");
2173        }
2174    }
2175
2176    #[test]
2177    fn grok_ids_stay_in_grok_family() {
2178        assert_eq!(model_family("grok-4.6"), ModelFamily::Grok);
2179        assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
2180        assert_eq!(
2181            model_family("grok-4.20-0309-non-reasoning"),
2182            ModelFamily::Grok
2183        );
2184    }
2185
2186    #[test]
2187    fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
2188        let registry = ModelRegistry::default();
2189
2190        let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
2191        assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
2192        assert_eq!(stepfun.resolved.id, "step-3.7-flash");
2193
2194        for (alias, expected) in [
2195            ("minimax", "MiniMax-M3"),
2196            ("minimax-m3", "MiniMax-M3"),
2197            ("minimax-m2.7", "MiniMax-M2.7"),
2198            ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
2199            ("minimax-m2.1", "MiniMax-M2.1"),
2200            ("minimax-m2", "MiniMax-M2"),
2201        ] {
2202            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
2203
2204            assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
2205            assert_eq!(resolved.resolved.id, expected);
2206            assert!(!resolved.used_fallback);
2207            assert!(resolved.resolved.supports_tools);
2208            assert!(resolved.resolved.supports_reasoning);
2209        }
2210    }
2211
2212    #[test]
2213    fn minimax_anthropic_models_resolve_when_provider_hinted() {
2214        let registry = ModelRegistry::default();
2215
2216        for (alias, expected) in [
2217            ("minimax-anthropic", "MiniMax-M3"),
2218            ("minimax-m3", "MiniMax-M3"),
2219            ("minimax-m2.7", "MiniMax-M2.7"),
2220        ] {
2221            let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
2222
2223            assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
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 deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
2233        let registry = ModelRegistry::default();
2234        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
2235
2236        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2237        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2238    }
2239
2240    #[test]
2241    fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
2242        let registry = ModelRegistry::default();
2243
2244        for (alias, expected) in [
2245            ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
2246            ("qwen3.6-flash", "qwen/qwen3.6-flash"),
2247            ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
2248            ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
2249            ("qwen3.6-plus", "qwen/qwen3.6-plus"),
2250            ("gemma-4-31b-it", "google/gemma-4-31b-it"),
2251            ("glm-5.1", "z-ai/glm-5.1"),
2252            ("glm-5.2", "z-ai/glm-5.2"),
2253            ("glm-5.3", "z-ai/glm-5.3"),
2254            ("minimax-m3", "minimax/minimax-m3"),
2255            ("minimax-2.7", "minimax/minimax-m2.7"),
2256            ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
2257            ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
2258            ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
2259            ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
2260            (
2261                "nvidia/nemotron-3-ultra",
2262                "nvidia/nemotron-3-ultra-550b-a55b",
2263            ),
2264        ] {
2265            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
2266
2267            assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
2268            assert_eq!(resolved.resolved.id, expected);
2269            assert!(resolved.resolved.supports_tools);
2270            assert!(resolved.resolved.supports_reasoning);
2271        }
2272    }
2273
2274    #[test]
2275    fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
2276        let registry = ModelRegistry::default();
2277        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
2278
2279        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
2280        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
2281    }
2282
2283    #[test]
2284    fn together_inkling_keeps_published_wire_identity() {
2285        let registry = ModelRegistry::default();
2286        for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
2287            let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
2288
2289            assert_eq!(resolved.resolved.provider, ProviderKind::Together);
2290            assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
2291            assert!(resolved.resolved.supports_tools);
2292            assert!(resolved.resolved.supports_reasoning);
2293            assert!(!resolved.used_fallback);
2294        }
2295
2296        let unscoped = registry.resolve(Some("inkling"), None);
2297        assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
2298        assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
2299        assert!(!unscoped.used_fallback);
2300    }
2301
2302    #[test]
2303    fn registry_lists_and_resolves_every_v090_catalog_addition() {
2304        let registry = ModelRegistry::default();
2305        let advertised = [
2306            (ProviderKind::Anthropic, "claude-sonnet-5"),
2307            (ProviderKind::Anthropic, "claude-fable-5"),
2308            (ProviderKind::Openai, "gpt-5.3-codex"),
2309            (ProviderKind::Openai, "gpt-5.5"),
2310            (ProviderKind::Openai, "gpt-5.5-pro"),
2311            (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
2312            (ProviderKind::Arcee, "trinity-mini"),
2313        ];
2314
2315        let listed = registry.list();
2316        for (provider, model_id) in advertised {
2317            assert!(
2318                listed
2319                    .iter()
2320                    .any(|model| model.provider == provider && model.id == model_id),
2321                "missing {model_id} ({}) from model list",
2322                provider.as_str()
2323            );
2324            let resolved = registry.resolve(Some(model_id), Some(provider));
2325            assert_eq!(resolved.resolved.provider, provider, "{model_id}");
2326            assert_eq!(resolved.resolved.id, model_id, "{model_id}");
2327            assert!(!resolved.used_fallback, "{model_id}");
2328        }
2329    }
2330
2331    #[test]
2332    fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
2333        let registry = ModelRegistry::default();
2334
2335        let unscoped = registry.resolve(Some("gpt-5.5"), None);
2336        assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
2337        assert_eq!(unscoped.resolved.id, "gpt-5.5");
2338        assert!(!unscoped.used_fallback);
2339
2340        let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
2341        assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
2342        assert_eq!(codex.resolved.id, "gpt-5.5");
2343        assert!(!codex.used_fallback);
2344    }
2345
2346    #[test]
2347    fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
2348        let registry = ModelRegistry::default();
2349        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
2350
2351        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
2352        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2353    }
2354
2355    #[test]
2356    fn vllm_default_uses_canonical_model_id() {
2357        let registry = ModelRegistry::default();
2358        let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
2359
2360        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2361        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
2362    }
2363
2364    #[test]
2365    fn ollama_default_uses_small_local_model_id() {
2366        let registry = ModelRegistry::default();
2367        let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
2368
2369        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2370        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2371        assert!(resolved.resolved.supports_reasoning);
2372    }
2373
2374    #[test]
2375    fn ollama_cloud_default_uses_the_hosted_catalog_model_id() {
2376        let registry = ModelRegistry::default();
2377        let resolved = registry.resolve(None, Some(ProviderKind::OllamaCloud));
2378
2379        assert_eq!(resolved.resolved.provider, ProviderKind::OllamaCloud);
2380        assert_eq!(resolved.resolved.id, "gpt-oss:120b");
2381        assert!(resolved.resolved.supports_reasoning);
2382    }
2383
2384    #[test]
2385    fn ollama_requested_model_tag_is_preserved() {
2386        let registry = ModelRegistry::default();
2387        let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
2388
2389        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
2390        assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
2391        assert!(!resolved.used_fallback);
2392    }
2393
2394    #[test]
2395    fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
2396        let registry = ModelRegistry::default();
2397        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
2398
2399        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
2400        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
2401    }
2402
2403    #[test]
2404    fn preserves_requested_model_casing_for_third_party_providers() {
2405        let registry = ModelRegistry::default();
2406        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
2407
2408        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2409        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
2410    }
2411
2412    #[test]
2413    fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
2414        let registry = ModelRegistry::default();
2415        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
2416
2417        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2418        // Registry's canonical id is used even when user provides different casing
2419        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
2420    }
2421
2422    #[test]
2423    fn preserves_requested_model_casing_without_surrounding_whitespace() {
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 alias_match_does_not_override_requested_casing() {
2433        let registry = ModelRegistry::default();
2434        let resolved = registry.resolve(Some("deepseek-reasoner"), None);
2435
2436        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
2437        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
2438    }
2439
2440    #[test]
2441    fn model_family_classifies_known_model_ids() {
2442        assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
2443        assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
2444        assert_eq!(
2445            model_family("anthropic/claude-opus-4-7"),
2446            ModelFamily::Anthropic
2447        );
2448        assert_eq!(
2449            model_family("meta-llama/llama-3.3-70b-instruct"),
2450            ModelFamily::Meta
2451        );
2452        assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
2453    }
2454
2455    #[test]
2456    fn model_family_uses_underlying_model_for_router_ids() {
2457        assert_eq!(
2458            model_family("groq/llama-3.3-70b-versatile"),
2459            ModelFamily::Meta
2460        );
2461        assert_eq!(
2462            model_family("openrouter/openai/gpt-5.4"),
2463            ModelFamily::OpenAI
2464        );
2465        assert_eq!(
2466            model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
2467            ModelFamily::DeepSeek
2468        );
2469    }
2470
2471    #[test]
2472    fn model_family_covers_prominent_google_and_mistral_model_names() {
2473        assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
2474        assert_eq!(
2475            model_family("mistralai/mixtral-8x22b"),
2476            ModelFamily::Mistral
2477        );
2478        assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2479    }
2480
2481    #[test]
2482    fn model_family_falls_back_to_inferencer_for_unknown_models() {
2483        assert_eq!(
2484            model_family("custom-gateway/my-private-model"),
2485            ModelFamily::Inferencer
2486        );
2487        assert_eq!(model_family(""), ModelFamily::Inferencer);
2488    }
2489}