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