Skip to main content

codewhale_agent/
lib.rs

1use std::collections::HashMap;
2
3use codewhale_config::ProviderKind;
4use serde::{Deserialize, Serialize};
5
6/// High-level model family used for shared identity affordances across clients.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ModelFamily {
9    DeepSeek,
10    Anthropic,
11    OpenAI,
12    Google,
13    Meta,
14    Mistral,
15    Qwen,
16    Grok,
17    Cohere,
18    GptOss,
19    Inferencer,
20}
21
22/// Metadata for a single model entry in the registry.
23///
24/// Each model has a canonical `id` used by the provider, a list of `aliases`
25/// that users may reference, and capability flags indicating whether the model
26/// supports tool use and reasoning.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ModelInfo {
29    /// The canonical model identifier used by the provider (e.g. `"deepseek-v4-pro"`).
30    pub id: String,
31    /// The provider that serves this model.
32    pub provider: ProviderKind,
33    /// Alternative names that users can use to reference this model (case-insensitive).
34    pub aliases: Vec<String>,
35    /// Whether this model supports tool/function calling.
36    pub supports_tools: bool,
37    /// Whether this model supports extended reasoning.
38    pub supports_reasoning: bool,
39}
40
41/// The result of resolving a user-requested model name to a concrete model entry.
42///
43/// Contains the resolved [`ModelInfo`], whether a fallback was used, and the
44/// chain of resolution strategies that were attempted.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelResolution {
47    /// The original model name requested by the user, if any.
48    pub requested: Option<String>,
49    /// The concrete model that was resolved.
50    pub resolved: ModelInfo,
51    /// Whether a fallback was used because the requested model was not found.
52    pub used_fallback: bool,
53    /// The ordered list of resolution strategies that were attempted.
54    pub fallback_chain: Vec<String>,
55}
56
57/// A registry of supported models and their aliases, used to resolve user-facing
58/// model names to concrete provider-specific model entries.
59///
60/// The default registry is populated with all built-in models across supported
61/// providers (DeepSeek, NVIDIA NIM, OpenAI-compatible, and others).
62#[derive(Debug, Clone)]
63pub struct ModelRegistry {
64    models: Vec<ModelInfo>,
65    alias_map: HashMap<String, usize>,
66}
67
68/// Creates a registry pre-populated with all built-in models and their aliases.
69impl Default for ModelRegistry {
70    fn default() -> Self {
71        let models = vec![
72            ModelInfo {
73                id: "deepseek-v4-pro".to_string(),
74                provider: ProviderKind::Deepseek,
75                aliases: vec![],
76                supports_tools: true,
77                supports_reasoning: true,
78            },
79            ModelInfo {
80                id: "deepseek-v4-flash".to_string(),
81                provider: ProviderKind::Deepseek,
82                aliases: vec![
83                    "deepseek-chat".to_string(),
84                    "deepseek-reasoner".to_string(),
85                    "deepseek-r1".to_string(),
86                    "deepseek-v3".to_string(),
87                    "deepseek-v3.2".to_string(),
88                ],
89                supports_tools: true,
90                supports_reasoning: true,
91            },
92            ModelInfo {
93                id: "deepseek-ai/deepseek-v4-pro".to_string(),
94                provider: ProviderKind::NvidiaNim,
95                aliases: vec![
96                    "deepseek-v4-pro".to_string(),
97                    "nvidia-deepseek-v4-pro".to_string(),
98                    "nim-deepseek-v4-pro".to_string(),
99                ],
100                supports_tools: true,
101                supports_reasoning: true,
102            },
103            ModelInfo {
104                id: "deepseek-ai/deepseek-v4-flash".to_string(),
105                provider: ProviderKind::NvidiaNim,
106                aliases: vec![
107                    "deepseek-v4-flash".to_string(),
108                    "deepseek-chat".to_string(),
109                    "deepseek-reasoner".to_string(),
110                    "nvidia-deepseek-v4-flash".to_string(),
111                    "nim-deepseek-v4-flash".to_string(),
112                ],
113                supports_tools: true,
114                supports_reasoning: true,
115            },
116            ModelInfo {
117                id: "deepseek-v4-pro".to_string(),
118                provider: ProviderKind::Openai,
119                aliases: vec!["openai-compatible-deepseek-v4-pro".to_string()],
120                supports_tools: true,
121                supports_reasoning: true,
122            },
123            ModelInfo {
124                id: "deepseek-v4-flash".to_string(),
125                provider: ProviderKind::Openai,
126                aliases: vec!["openai-compatible-deepseek-v4-flash".to_string()],
127                supports_tools: true,
128                supports_reasoning: true,
129            },
130            // 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: "arcee-ai/trinity-large-thinking".to_string(),
276                provider: ProviderKind::Openrouter,
277                aliases: vec![
278                    "trinity".to_string(),
279                    "trinity-large-thinking".to_string(),
280                    "arcee-trinity-large-thinking".to_string(),
281                ],
282                supports_tools: true,
283                supports_reasoning: true,
284            },
285            ModelInfo {
286                id: "xiaomi/mimo-v2.5-pro".to_string(),
287                provider: ProviderKind::Openrouter,
288                aliases: vec![
289                    "openrouter-mimo-v2.5-pro".to_string(),
290                    "openrouter-xiaomi-mimo-v2.5-pro".to_string(),
291                ],
292                supports_tools: true,
293                supports_reasoning: true,
294            },
295            ModelInfo {
296                id: "xiaomi/mimo-v2.5".to_string(),
297                provider: ProviderKind::Openrouter,
298                aliases: vec![
299                    "openrouter-mimo-v2.5".to_string(),
300                    "openrouter-xiaomi-mimo-v2.5".to_string(),
301                ],
302                supports_tools: true,
303                supports_reasoning: true,
304            },
305            ModelInfo {
306                id: "qwen/qwen3.6-flash".to_string(),
307                provider: ProviderKind::Openrouter,
308                aliases: vec!["qwen3.6-flash".to_string(), "qwen-3.6-flash".to_string()],
309                supports_tools: true,
310                supports_reasoning: true,
311            },
312            ModelInfo {
313                id: "qwen/qwen3.6-35b-a3b".to_string(),
314                provider: ProviderKind::Openrouter,
315                aliases: vec![
316                    "qwen3.6-35b-a3b".to_string(),
317                    "qwen-3.6-35b-a3b".to_string(),
318                ],
319                supports_tools: true,
320                supports_reasoning: true,
321            },
322            ModelInfo {
323                id: "qwen/qwen3.6-max-preview".to_string(),
324                provider: ProviderKind::Openrouter,
325                aliases: vec![
326                    "qwen3.6-max-preview".to_string(),
327                    "qwen-3.6-max-preview".to_string(),
328                    "qwen-max-preview".to_string(),
329                ],
330                supports_tools: true,
331                supports_reasoning: true,
332            },
333            ModelInfo {
334                id: "qwen/qwen3.6-27b".to_string(),
335                provider: ProviderKind::Openrouter,
336                aliases: vec!["qwen3.6-27b".to_string(), "qwen-3.6-27b".to_string()],
337                supports_tools: true,
338                supports_reasoning: true,
339            },
340            ModelInfo {
341                id: "qwen/qwen3.6-plus".to_string(),
342                provider: ProviderKind::Openrouter,
343                aliases: vec!["qwen3.6-plus".to_string(), "qwen-3.6-plus".to_string()],
344                supports_tools: true,
345                supports_reasoning: true,
346            },
347            ModelInfo {
348                id: "qwen/qwen3.7-plus".to_string(),
349                provider: ProviderKind::Openrouter,
350                aliases: vec!["qwen3.7-plus".to_string(), "qwen-3.7-plus".to_string()],
351                supports_tools: true,
352                supports_reasoning: true,
353            },
354            ModelInfo {
355                id: "moonshotai/kimi-k2.7-code".to_string(),
356                provider: ProviderKind::Openrouter,
357                aliases: vec![
358                    "kimi-k2.7-code".to_string(),
359                    "openrouter-kimi-k2.7-code".to_string(),
360                ],
361                supports_tools: true,
362                supports_reasoning: true,
363            },
364            ModelInfo {
365                id: "moonshotai/kimi-k2.6".to_string(),
366                provider: ProviderKind::Openrouter,
367                aliases: vec!["openrouter-kimi-k2.6".to_string()],
368                supports_tools: true,
369                supports_reasoning: true,
370            },
371            ModelInfo {
372                id: "minimax/minimax-m3".to_string(),
373                provider: ProviderKind::Openrouter,
374                aliases: vec![
375                    "minimax-m3".to_string(),
376                    "minimax-m-3".to_string(),
377                    "openrouter-minimax-m3".to_string(),
378                ],
379                supports_tools: true,
380                supports_reasoning: true,
381            },
382            ModelInfo {
383                id: "z-ai/glm-5.1".to_string(),
384                provider: ProviderKind::Openrouter,
385                aliases: vec!["glm-5.1".to_string(), "zai-glm-5.1".to_string()],
386                supports_tools: true,
387                supports_reasoning: true,
388            },
389            ModelInfo {
390                id: "z-ai/glm-5.2".to_string(),
391                provider: ProviderKind::Openrouter,
392                aliases: vec!["glm-5.2".to_string(), "zai-glm-5.2".to_string()],
393                supports_tools: true,
394                supports_reasoning: true,
395            },
396            ModelInfo {
397                id: "z-ai/glm-5-turbo".to_string(),
398                provider: ProviderKind::Openrouter,
399                aliases: vec!["glm-5-turbo".to_string(), "zai-glm-5-turbo".to_string()],
400                supports_tools: true,
401                supports_reasoning: true,
402            },
403            ModelInfo {
404                id: "GLM-5.2".to_string(),
405                provider: ProviderKind::Zai,
406                aliases: vec![
407                    "glm-5.2".to_string(),
408                    "glm-5-2".to_string(),
409                    "zai-glm-5.2".to_string(),
410                    "zai-glm-5-2".to_string(),
411                ],
412                supports_tools: true,
413                supports_reasoning: true,
414            },
415            ModelInfo {
416                id: "GLM-5.1".to_string(),
417                provider: ProviderKind::Zai,
418                aliases: vec![
419                    "glm-5.1".to_string(),
420                    "glm-5-1".to_string(),
421                    "zai-glm-5.1".to_string(),
422                    "zai-glm-5-1".to_string(),
423                ],
424                supports_tools: true,
425                supports_reasoning: true,
426            },
427            ModelInfo {
428                id: "GLM-5-Turbo".to_string(),
429                provider: ProviderKind::Zai,
430                aliases: vec![
431                    "glm-5-turbo".to_string(),
432                    "glm-5turbo".to_string(),
433                    "zai-glm-5-turbo".to_string(),
434                ],
435                supports_tools: true,
436                supports_reasoning: true,
437            },
438            ModelInfo {
439                id: "tencent/hy3-preview".to_string(),
440                provider: ProviderKind::Openrouter,
441                aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
442                supports_tools: true,
443                supports_reasoning: true,
444            },
445            ModelInfo {
446                id: "google/gemma-4-31b-it".to_string(),
447                provider: ProviderKind::Openrouter,
448                aliases: vec!["gemma-4-31b".to_string(), "gemma-4-31b-it".to_string()],
449                supports_tools: true,
450                supports_reasoning: true,
451            },
452            ModelInfo {
453                id: "google/gemma-4-26b-a4b-it".to_string(),
454                provider: ProviderKind::Openrouter,
455                aliases: vec![
456                    "gemma-4-26b-a4b".to_string(),
457                    "gemma-4-26b-a4b-it".to_string(),
458                ],
459                supports_tools: true,
460                supports_reasoning: true,
461            },
462            ModelInfo {
463                id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free".to_string(),
464                provider: ProviderKind::Openrouter,
465                aliases: vec![
466                    "nemotron-3-nano-omni".to_string(),
467                    "nemotron-3-nano-omni-reasoning".to_string(),
468                ],
469                supports_tools: true,
470                supports_reasoning: true,
471            },
472            ModelInfo {
473                id: "mimo-v2.5-pro".to_string(),
474                provider: ProviderKind::XiaomiMimo,
475                aliases: vec![
476                    "mimo".to_string(),
477                    "pro".to_string(),
478                    "xiaomi-mimo-v2.5-pro".to_string(),
479                    "xiaomi-mimo-v2-5-pro".to_string(),
480                ],
481                supports_tools: true,
482                supports_reasoning: true,
483            },
484            ModelInfo {
485                id: "mimo-v2.5".to_string(),
486                provider: ProviderKind::XiaomiMimo,
487                aliases: vec![
488                    "omni".to_string(),
489                    "mimo-omni".to_string(),
490                    "v2.5-omni".to_string(),
491                    "mimo-v2.5-omni".to_string(),
492                    "xiaomi-mimo-v2.5".to_string(),
493                    "xiaomi-mimo-v2.5-omni".to_string(),
494                ],
495                supports_tools: true,
496                supports_reasoning: true,
497            },
498            ModelInfo {
499                id: "mimo-v2.5-asr".to_string(),
500                provider: ProviderKind::XiaomiMimo,
501                aliases: vec![
502                    "asr".to_string(),
503                    "speech-to-text".to_string(),
504                    "transcribe".to_string(),
505                ],
506                supports_tools: false,
507                supports_reasoning: false,
508            },
509            ModelInfo {
510                id: "mimo-v2.5-tts".to_string(),
511                provider: ProviderKind::XiaomiMimo,
512                aliases: vec![
513                    "tts".to_string(),
514                    "speech".to_string(),
515                    "mimo-tts".to_string(),
516                ],
517                supports_tools: false,
518                supports_reasoning: false,
519            },
520            ModelInfo {
521                id: "mimo-v2.5-tts-voicedesign".to_string(),
522                provider: ProviderKind::XiaomiMimo,
523                aliases: vec![
524                    "voicedesign".to_string(),
525                    "voice-design".to_string(),
526                    "mimo-voice-design".to_string(),
527                ],
528                supports_tools: false,
529                supports_reasoning: false,
530            },
531            ModelInfo {
532                id: "mimo-v2.5-tts-voiceclone".to_string(),
533                provider: ProviderKind::XiaomiMimo,
534                aliases: vec![
535                    "voiceclone".to_string(),
536                    "voice-clone".to_string(),
537                    "mimo-voice-clone".to_string(),
538                ],
539                supports_tools: false,
540                supports_reasoning: false,
541            },
542            ModelInfo {
543                id: "mimo-v2-tts".to_string(),
544                provider: ProviderKind::XiaomiMimo,
545                aliases: vec!["mimo-v2-speech".to_string()],
546                supports_tools: false,
547                supports_reasoning: false,
548            },
549            ModelInfo {
550                id: "deepseek/deepseek-v4-pro".to_string(),
551                provider: ProviderKind::Novita,
552                aliases: vec![
553                    "deepseek-v4-pro".to_string(),
554                    "novita-deepseek-v4-pro".to_string(),
555                ],
556                supports_tools: true,
557                supports_reasoning: true,
558            },
559            ModelInfo {
560                id: "deepseek/deepseek-v4-flash".to_string(),
561                provider: ProviderKind::Novita,
562                aliases: vec![
563                    "deepseek-v4-flash".to_string(),
564                    "deepseek-chat".to_string(),
565                    "deepseek-reasoner".to_string(),
566                    "novita-deepseek-v4-flash".to_string(),
567                ],
568                supports_tools: true,
569                supports_reasoning: true,
570            },
571            ModelInfo {
572                id: "accounts/fireworks/models/deepseek-v4-pro".to_string(),
573                provider: ProviderKind::Fireworks,
574                aliases: vec![
575                    "deepseek-v4-pro".to_string(),
576                    "fireworks-deepseek-v4-pro".to_string(),
577                ],
578                supports_tools: true,
579                supports_reasoning: true,
580            },
581            ModelInfo {
582                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
583                provider: ProviderKind::Siliconflow,
584                aliases: vec![
585                    "deepseek-v4-pro".to_string(),
586                    "deepseek-reasoner".to_string(),
587                    "deepseek-r1".to_string(),
588                    "siliconflow-deepseek-v4-pro".to_string(),
589                ],
590                supports_tools: true,
591                supports_reasoning: true,
592            },
593            ModelInfo {
594                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
595                provider: ProviderKind::Siliconflow,
596                aliases: vec![
597                    "deepseek-v4-flash".to_string(),
598                    "deepseek-chat".to_string(),
599                    "deepseek-v3".to_string(),
600                    "siliconflow-deepseek-v4-flash".to_string(),
601                ],
602                supports_tools: true,
603                supports_reasoning: true,
604            },
605            ModelInfo {
606                id: "trinity-large-preview".to_string(),
607                provider: ProviderKind::Arcee,
608                aliases: vec!["arcee-trinity-large-preview".to_string()],
609                supports_tools: true,
610                supports_reasoning: false,
611            },
612            ModelInfo {
613                id: "kimi-k2.7-code".to_string(),
614                provider: ProviderKind::Moonshot,
615                aliases: vec![
616                    "kimi".to_string(),
617                    "kimi-k2".to_string(),
618                    "kimi-k2.7".to_string(),
619                    "kimi-code".to_string(),
620                    "moonshot-kimi-k2.7-code".to_string(),
621                ],
622                supports_tools: true,
623                supports_reasoning: true,
624            },
625            ModelInfo {
626                id: "kimi-k2.6".to_string(),
627                provider: ProviderKind::Moonshot,
628                aliases: vec!["kimi-k2.6".to_string(), "moonshot-kimi-k2.6".to_string()],
629                supports_tools: true,
630                supports_reasoning: true,
631            },
632            ModelInfo {
633                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
634                provider: ProviderKind::Sglang,
635                aliases: vec![
636                    "deepseek-v4-pro".to_string(),
637                    "sglang-deepseek-v4-pro".to_string(),
638                ],
639                supports_tools: true,
640                supports_reasoning: true,
641            },
642            ModelInfo {
643                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
644                provider: ProviderKind::Sglang,
645                aliases: vec![
646                    "deepseek-v4-flash".to_string(),
647                    "deepseek-chat".to_string(),
648                    "deepseek-reasoner".to_string(),
649                    "sglang-deepseek-v4-flash".to_string(),
650                ],
651                supports_tools: true,
652                supports_reasoning: true,
653            },
654            ModelInfo {
655                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
656                provider: ProviderKind::Vllm,
657                aliases: vec![
658                    "deepseek-v4-pro".to_string(),
659                    "vllm-deepseek-v4-pro".to_string(),
660                ],
661                supports_tools: true,
662                supports_reasoning: true,
663            },
664            ModelInfo {
665                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
666                provider: ProviderKind::Vllm,
667                aliases: vec![
668                    "deepseek-v4-flash".to_string(),
669                    "deepseek-chat".to_string(),
670                    "deepseek-reasoner".to_string(),
671                    "vllm-deepseek-v4-flash".to_string(),
672                ],
673                supports_tools: true,
674                supports_reasoning: true,
675            },
676            ModelInfo {
677                id: "deepseek-v4-flash".to_string(),
678                provider: ProviderKind::Ollama,
679                aliases: vec![],
680                supports_tools: true,
681                supports_reasoning: true,
682            },
683            ModelInfo {
684                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
685                provider: ProviderKind::Huggingface,
686                aliases: vec![
687                    "deepseek-v4-pro".to_string(),
688                    "hf-deepseek-v4-pro".to_string(),
689                ],
690                supports_tools: true,
691                supports_reasoning: true,
692            },
693            ModelInfo {
694                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
695                provider: ProviderKind::Huggingface,
696                aliases: vec![
697                    "deepseek-v4-flash".to_string(),
698                    "deepseek-chat".to_string(),
699                    "deepseek-reasoner".to_string(),
700                    "hf-deepseek-v4-flash".to_string(),
701                ],
702                supports_tools: true,
703                supports_reasoning: true,
704            },
705            // Together AI provider models
706            ModelInfo {
707                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
708                provider: ProviderKind::Together,
709                aliases: vec![
710                    "deepseek-v4-pro".to_string(),
711                    "together-deepseek-v4-pro".to_string(),
712                ],
713                supports_tools: true,
714                supports_reasoning: true,
715            },
716            ModelInfo {
717                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
718                provider: ProviderKind::Together,
719                aliases: vec![
720                    "deepseek-v4-flash".to_string(),
721                    "deepseek-chat".to_string(),
722                    "together-deepseek-v4-flash".to_string(),
723                ],
724                supports_tools: true,
725                supports_reasoning: true,
726            },
727            ModelInfo {
728                // Together's published hosted endpoint is lowercase even though
729                // the open-weight Hugging Face repository uses `Inkling`.
730                id: "thinkingmachines/inkling".to_string(),
731                provider: ProviderKind::Together,
732                aliases: vec!["inkling".to_string(), "together-inkling".to_string()],
733                supports_tools: true,
734                supports_reasoning: true,
735            },
736            // Qwen 3.7 Max (OpenRouter)
737            ModelInfo {
738                id: "qwen/qwen3.7-max".to_string(),
739                provider: ProviderKind::Openrouter,
740                aliases: vec!["qwen3.7-max".to_string(), "qwen-3.7-max".to_string()],
741                supports_tools: true,
742                supports_reasoning: true,
743            },
744            // OpenAI Codex (ChatGPT OAuth) models
745            ModelInfo {
746                id: "gpt-5.5".to_string(),
747                provider: ProviderKind::OpenaiCodex,
748                aliases: vec!["codex-gpt-5.5".to_string(), "chatgpt-gpt-5.5".to_string()],
749                supports_tools: true,
750                supports_reasoning: true,
751            },
752            // Anthropic native Messages API models (#3014)
753            ModelInfo {
754                id: "claude-opus-4-8".to_string(),
755                provider: ProviderKind::Anthropic,
756                aliases: vec!["opus".to_string(), "claude-opus".to_string()],
757                supports_tools: true,
758                supports_reasoning: true,
759            },
760            ModelInfo {
761                id: "claude-sonnet-4-6".to_string(),
762                provider: ProviderKind::Anthropic,
763                aliases: vec!["sonnet".to_string(), "claude-sonnet".to_string()],
764                supports_tools: true,
765                supports_reasoning: true,
766            },
767            ModelInfo {
768                id: "claude-haiku-4-5".to_string(),
769                provider: ProviderKind::Anthropic,
770                aliases: vec!["haiku".to_string(), "claude-haiku".to_string()],
771                supports_tools: true,
772                supports_reasoning: false,
773            },
774            ModelInfo {
775                id: "claude-sonnet-5".to_string(),
776                provider: ProviderKind::Anthropic,
777                aliases: vec!["sonnet-5".to_string()],
778                supports_tools: true,
779                supports_reasoning: true,
780            },
781            ModelInfo {
782                id: "claude-fable-5".to_string(),
783                provider: ProviderKind::Anthropic,
784                aliases: vec!["fable".to_string(), "fable-5".to_string()],
785                supports_tools: true,
786                supports_reasoning: true,
787            },
788            // OpenModel Anthropic-compatible Messages route
789            ModelInfo {
790                id: "deepseek-v4-flash".to_string(),
791                provider: ProviderKind::Openmodel,
792                aliases: vec!["openmodel".to_string(), "openmodel-deepseek".to_string()],
793                supports_tools: true,
794                supports_reasoning: true,
795            },
796            // MiniMax 2.7 (OpenRouter)
797            ModelInfo {
798                id: "minimax/minimax-m2.7".to_string(),
799                provider: ProviderKind::Openrouter,
800                aliases: vec![
801                    "minimax-2.7".to_string(),
802                    "minimax-2-7".to_string(),
803                    "openrouter-minimax-2.7".to_string(),
804                ],
805                supports_tools: true,
806                supports_reasoning: true,
807            },
808            ModelInfo {
809                id: "step-3.7-flash".to_string(),
810                provider: ProviderKind::Stepfun,
811                aliases: vec!["stepfun".to_string(), "stepflash".to_string()],
812                supports_tools: true,
813                supports_reasoning: false,
814            },
815            ModelInfo {
816                id: "MiniMax-M3".to_string(),
817                provider: ProviderKind::Minimax,
818                aliases: vec![
819                    "minimax".to_string(),
820                    "minimax-m3".to_string(),
821                    "minimax-m-3".to_string(),
822                ],
823                supports_tools: true,
824                supports_reasoning: true,
825            },
826            ModelInfo {
827                id: "MiniMax-M2.7".to_string(),
828                provider: ProviderKind::Minimax,
829                aliases: vec![
830                    "minimax-m2.7".to_string(),
831                    "minimax-m2-7".to_string(),
832                    "minimax-m-2.7".to_string(),
833                    "minimax-m-2-7".to_string(),
834                ],
835                supports_tools: true,
836                supports_reasoning: true,
837            },
838            ModelInfo {
839                id: "MiniMax-M3".to_string(),
840                provider: ProviderKind::MinimaxAnthropic,
841                aliases: vec![
842                    "minimax-anthropic".to_string(),
843                    "minimax-anthropic-m3".to_string(),
844                    "minimax-m3".to_string(),
845                ],
846                supports_tools: true,
847                supports_reasoning: true,
848            },
849            ModelInfo {
850                id: "MiniMax-M2.7".to_string(),
851                provider: ProviderKind::MinimaxAnthropic,
852                aliases: vec![
853                    "minimax-anthropic-m2.7".to_string(),
854                    "minimax-anthropic-m2-7".to_string(),
855                    "minimax-m2.7".to_string(),
856                ],
857                supports_tools: true,
858                supports_reasoning: true,
859            },
860            ModelInfo {
861                id: "MiniMax-M2.7-highspeed".to_string(),
862                provider: ProviderKind::Minimax,
863                aliases: vec![
864                    "minimax-m2.7-highspeed".to_string(),
865                    "minimax-m2-7-highspeed".to_string(),
866                    "minimax-m-2.7-highspeed".to_string(),
867                    "minimax-m-2-7-highspeed".to_string(),
868                ],
869                supports_tools: true,
870                supports_reasoning: true,
871            },
872            ModelInfo {
873                id: "MiniMax-M2.5".to_string(),
874                provider: ProviderKind::Minimax,
875                aliases: vec![
876                    "minimax-m2.5".to_string(),
877                    "minimax-m2-5".to_string(),
878                    "minimax-m-2.5".to_string(),
879                    "minimax-m-2-5".to_string(),
880                ],
881                supports_tools: true,
882                supports_reasoning: true,
883            },
884            ModelInfo {
885                id: "MiniMax-M2.5-highspeed".to_string(),
886                provider: ProviderKind::Minimax,
887                aliases: vec![
888                    "minimax-m2.5-highspeed".to_string(),
889                    "minimax-m2-5-highspeed".to_string(),
890                    "minimax-m-2.5-highspeed".to_string(),
891                    "minimax-m-2-5-highspeed".to_string(),
892                ],
893                supports_tools: true,
894                supports_reasoning: true,
895            },
896            ModelInfo {
897                id: "MiniMax-M2.1".to_string(),
898                provider: ProviderKind::Minimax,
899                aliases: vec![
900                    "minimax-m2.1".to_string(),
901                    "minimax-m2-1".to_string(),
902                    "minimax-m-2.1".to_string(),
903                    "minimax-m-2-1".to_string(),
904                ],
905                supports_tools: true,
906                supports_reasoning: true,
907            },
908            ModelInfo {
909                id: "MiniMax-M2.1-highspeed".to_string(),
910                provider: ProviderKind::Minimax,
911                aliases: vec![
912                    "minimax-m2.1-highspeed".to_string(),
913                    "minimax-m2-1-highspeed".to_string(),
914                    "minimax-m-2.1-highspeed".to_string(),
915                    "minimax-m-2-1-highspeed".to_string(),
916                ],
917                supports_tools: true,
918                supports_reasoning: true,
919            },
920            ModelInfo {
921                id: "MiniMax-M2".to_string(),
922                provider: ProviderKind::Minimax,
923                aliases: vec!["minimax-m2".to_string(), "minimax-m-2".to_string()],
924                supports_tools: true,
925                supports_reasoning: true,
926            },
927            // NVIDIA Nemotron 3 Ultra (OpenRouter)
928            ModelInfo {
929                id: "nvidia/nemotron-3-ultra-550b-a55b".to_string(),
930                provider: ProviderKind::Openrouter,
931                aliases: vec![
932                    "nvidia/nemotron-3-ultra".to_string(),
933                    "nemotron-3-ultra".to_string(),
934                    "nemotron-3-ultra-550b-a55b".to_string(),
935                    "nvidia-nemotron-3-ultra".to_string(),
936                    "nvidia-nemotron-3-ultra-550b-a55b".to_string(),
937                ],
938                supports_tools: true,
939                supports_reasoning: true,
940            },
941            // DeepInfra (https://deepinfra.com)
942            ModelInfo {
943                id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
944                provider: ProviderKind::Deepinfra,
945                aliases: vec![
946                    "deepseek-v4-pro".to_string(),
947                    "di-deepseek-v4-pro".to_string(),
948                ],
949                supports_tools: true,
950                supports_reasoning: true,
951            },
952            ModelInfo {
953                id: "deepseek-ai/DeepSeek-V4-Flash".to_string(),
954                provider: ProviderKind::Deepinfra,
955                aliases: vec![
956                    "deepseek-v4-flash".to_string(),
957                    "di-deepseek-v4-flash".to_string(),
958                ],
959                supports_tools: true,
960                supports_reasoning: true,
961            },
962            // Sakana AI Fugu (https://api.sakana.ai)
963            ModelInfo {
964                id: "fugu".to_string(),
965                provider: ProviderKind::Sakana,
966                aliases: vec!["sakana-fugu".to_string(), "sakana/fugu".to_string()],
967                supports_tools: true,
968                supports_reasoning: false,
969            },
970            ModelInfo {
971                id: "fugu-ultra-20260615".to_string(),
972                provider: ProviderKind::Sakana,
973                aliases: vec!["fugu-ultra".to_string(), "sakana-fugu-ultra".to_string()],
974                supports_tools: true,
975                supports_reasoning: true,
976            },
977            // Meituan LongCat (https://longcat.chat/platform)
978            ModelInfo {
979                id: "LongCat-2.0".to_string(),
980                provider: ProviderKind::LongCat,
981                aliases: vec!["longcat".to_string(), "longcat-2.0".to_string()],
982                supports_tools: true,
983                supports_reasoning: true,
984            },
985            // Meta Model API / Muse Spark.
986            ModelInfo {
987                id: "muse-spark-1.1".to_string(),
988                provider: ProviderKind::Meta,
989                aliases: vec!["muse-spark".to_string(), "muse".to_string()],
990                supports_tools: true,
991                supports_reasoning: true,
992            },
993            // xAI / Grok (https://api.x.ai/v1)
994            ModelInfo {
995                id: "grok-4.5".to_string(),
996                provider: ProviderKind::Xai,
997                aliases: vec!["grok".to_string(), "xai-grok-4.5".to_string()],
998                supports_tools: true,
999                supports_reasoning: true,
1000            },
1001            ModelInfo {
1002                id: "grok-4.3".to_string(),
1003                provider: ProviderKind::Xai,
1004                aliases: vec!["xai-grok-4.3".to_string()],
1005                supports_tools: true,
1006                supports_reasoning: true,
1007            },
1008            ModelInfo {
1009                id: "grok-build".to_string(),
1010                provider: ProviderKind::Xai,
1011                aliases: vec!["xai-grok-build".to_string()],
1012                supports_tools: true,
1013                supports_reasoning: true,
1014            },
1015            ModelInfo {
1016                id: "grok-composer-2.5-fast".to_string(),
1017                provider: ProviderKind::Xai,
1018                aliases: vec!["xai-grok-composer".to_string()],
1019                supports_tools: true,
1020                supports_reasoning: false,
1021            },
1022            ModelInfo {
1023                id: "grok-4.20-0309-reasoning".to_string(),
1024                provider: ProviderKind::Xai,
1025                aliases: vec!["xai-grok-reasoning".to_string()],
1026                supports_tools: true,
1027                supports_reasoning: true,
1028            },
1029            ModelInfo {
1030                id: "grok-4.20-0309-non-reasoning".to_string(),
1031                provider: ProviderKind::Xai,
1032                aliases: vec!["xai-grok-fast".to_string()],
1033                supports_tools: true,
1034                supports_reasoning: false,
1035            },
1036        ];
1037        Self::new(models)
1038    }
1039}
1040
1041impl ModelRegistry {
1042    /// Creates a new registry from a list of [`ModelInfo`] entries.
1043    ///
1044    /// Builds an internal alias map for fast lookup by model id or alias.
1045    /// If multiple models share the same id or alias, the first one registered
1046    /// takes priority.
1047    #[must_use]
1048    pub fn new(models: Vec<ModelInfo>) -> Self {
1049        let mut alias_map = HashMap::new();
1050        for (idx, model) in models.iter().enumerate() {
1051            alias_map.entry(normalize(&model.id)).or_insert(idx);
1052            for alias in &model.aliases {
1053                alias_map.entry(normalize(alias)).or_insert(idx);
1054            }
1055        }
1056        Self { models, alias_map }
1057    }
1058
1059    /// Returns a clone of all models in the registry.
1060    #[must_use]
1061    pub fn list(&self) -> Vec<ModelInfo> {
1062        self.models.clone()
1063    }
1064
1065    /// Resolves a user-requested model name to a concrete [`ModelInfo`].
1066    ///
1067    /// Resolution follows this priority order:
1068    /// 1. If the provider is Ollama, the requested name is used as-is (to
1069    ///    support arbitrary local model tags like `qwen2.5-coder:7b`).
1070    /// 2. If a `provider_hint` is given, search for a model matching that
1071    ///    provider whose id or alias matches the request (case-insensitive).
1072    /// 3. Look up the alias map for a case-insensitive match.
1073    /// 4. Fall back to the first model belonging to the hinted provider
1074    ///    (or DeepSeek if no hint was given).
1075    /// 5. As a last resort, fall back to the first model in the registry.
1076    #[must_use]
1077    pub fn resolve(
1078        &self,
1079        requested: Option<&str>,
1080        provider_hint: Option<ProviderKind>,
1081    ) -> ModelResolution {
1082        let mut fallback_chain = Vec::new();
1083
1084        if let Some(name) = requested {
1085            fallback_chain.push(format!("requested:{name}"));
1086            if provider_hint == Some(ProviderKind::Ollama) {
1087                return ModelResolution {
1088                    requested: Some(name.to_string()),
1089                    resolved: ModelInfo {
1090                        id: name.trim().to_string(),
1091                        provider: ProviderKind::Ollama,
1092                        aliases: Vec::new(),
1093                        supports_tools: true,
1094                        supports_reasoning: false,
1095                    },
1096                    used_fallback: false,
1097                    fallback_chain,
1098                };
1099            }
1100            if let Some(provider) = provider_hint
1101                && let Some(model) = self
1102                    .models
1103                    .iter()
1104                    .find(|m| m.provider == provider && model_matches(m, name))
1105                    .cloned()
1106            {
1107                return ModelResolution {
1108                    requested: Some(name.to_string()),
1109                    resolved: model,
1110                    used_fallback: false,
1111                    fallback_chain,
1112                };
1113            }
1114            if provider_hint == Some(ProviderKind::Atlascloud)
1115                && let Some(model) = atlascloud_passthrough_model(name)
1116            {
1117                return ModelResolution {
1118                    requested: Some(name.to_string()),
1119                    resolved: model,
1120                    used_fallback: false,
1121                    fallback_chain,
1122                };
1123            }
1124            if provider_hint == Some(ProviderKind::Arcee)
1125                && let Some(model) = arcee_passthrough_model(name)
1126            {
1127                return ModelResolution {
1128                    requested: Some(name.to_string()),
1129                    resolved: model,
1130                    used_fallback: false,
1131                    fallback_chain,
1132                };
1133            }
1134            if provider_hint == Some(ProviderKind::XiaomiMimo)
1135                && let Some(model) = xiaomi_mimo_passthrough_model(name)
1136            {
1137                return ModelResolution {
1138                    requested: Some(name.to_string()),
1139                    resolved: model,
1140                    used_fallback: false,
1141                    fallback_chain,
1142                };
1143            }
1144            if let Some(idx) = self.alias_map.get(&normalize(name)) {
1145                return ModelResolution {
1146                    requested: Some(name.to_string()),
1147                    resolved: preserve_requested_model_id_case(self.models[*idx].clone(), name),
1148                    used_fallback: false,
1149                    fallback_chain,
1150                };
1151            }
1152        }
1153
1154        let provider = provider_hint.unwrap_or(ProviderKind::Deepseek);
1155        fallback_chain.push(format!("provider_default:{}", provider.as_str()));
1156        if let Some(model) = self.models.iter().find(|m| m.provider == provider).cloned() {
1157            return ModelResolution {
1158                requested: requested.map(ToOwned::to_owned),
1159                resolved: model,
1160                used_fallback: true,
1161                fallback_chain,
1162            };
1163        }
1164
1165        let final_fallback = self.models.first().cloned().unwrap_or(ModelInfo {
1166            id: "deepseek-v4-pro".to_string(),
1167            provider: ProviderKind::Deepseek,
1168            aliases: Vec::new(),
1169            supports_tools: true,
1170            supports_reasoning: true,
1171        });
1172        fallback_chain.push("global_default:deepseek-v4-pro".to_string());
1173        ModelResolution {
1174            requested: requested.map(ToOwned::to_owned),
1175            resolved: final_fallback,
1176            used_fallback: true,
1177            fallback_chain,
1178        }
1179    }
1180}
1181
1182fn normalize(value: &str) -> String {
1183    value.trim().to_ascii_lowercase()
1184}
1185
1186#[must_use]
1187/// Classify a model identifier by its underlying model family.
1188pub fn model_family(model_id: &str) -> ModelFamily {
1189    let normalized = normalize(model_id);
1190    if normalized.is_empty() {
1191        return ModelFamily::Inferencer;
1192    }
1193
1194    if normalized.contains("deepseek") {
1195        return ModelFamily::DeepSeek;
1196    }
1197    if normalized.contains("claude") || normalized.contains("anthropic") {
1198        return ModelFamily::Anthropic;
1199    }
1200    if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
1201        return ModelFamily::GptOss;
1202    }
1203    if normalized.starts_with("gpt-")
1204        || normalized.contains("/gpt-")
1205        || normalized.contains("openai/")
1206    {
1207        return ModelFamily::OpenAI;
1208    }
1209    if normalized.contains("gemini")
1210        || normalized.contains("gemma")
1211        || normalized.contains("google/")
1212    {
1213        return ModelFamily::Google;
1214    }
1215    if normalized.contains("llama")
1216        || normalized.contains("muse-spark")
1217        || normalized.contains("meta-")
1218        || normalized.contains("meta/")
1219    {
1220        return ModelFamily::Meta;
1221    }
1222    if normalized.contains("mistral")
1223        || normalized.contains("mixtral")
1224        || normalized.contains("codestral")
1225    {
1226        return ModelFamily::Mistral;
1227    }
1228    if normalized.contains("qwen") {
1229        return ModelFamily::Qwen;
1230    }
1231    if normalized.contains("grok") {
1232        return ModelFamily::Grok;
1233    }
1234    if normalized.contains("cohere") || normalized.contains("command-r") {
1235        return ModelFamily::Cohere;
1236    }
1237
1238    ModelFamily::Inferencer
1239}
1240
1241fn model_matches(model: &ModelInfo, requested: &str) -> bool {
1242    let requested = normalize(requested);
1243    normalize(&model.id) == requested
1244        || model
1245            .aliases
1246            .iter()
1247            .any(|alias| normalize(alias) == requested)
1248}
1249
1250fn preserve_requested_model_id_case(mut model: ModelInfo, requested: &str) -> ModelInfo {
1251    let requested = requested.trim();
1252    if model.id.eq_ignore_ascii_case(requested) {
1253        model.id = requested.to_string();
1254    }
1255    model
1256}
1257
1258fn atlascloud_passthrough_model(requested: &str) -> Option<ModelInfo> {
1259    let requested = requested.trim();
1260    if requested.is_empty() || !requested.contains('/') {
1261        return None;
1262    }
1263
1264    Some(ModelInfo {
1265        id: requested.to_string(),
1266        provider: ProviderKind::Atlascloud,
1267        aliases: Vec::new(),
1268        supports_tools: true,
1269        supports_reasoning: true,
1270    })
1271}
1272
1273fn arcee_passthrough_model(requested: &str) -> Option<ModelInfo> {
1274    let requested = requested.trim();
1275    if requested.is_empty() {
1276        return None;
1277    }
1278    let supports_reasoning = requested.to_ascii_lowercase().contains("thinking");
1279
1280    Some(ModelInfo {
1281        id: requested.to_string(),
1282        provider: ProviderKind::Arcee,
1283        aliases: Vec::new(),
1284        supports_tools: true,
1285        supports_reasoning,
1286    })
1287}
1288
1289fn xiaomi_mimo_passthrough_model(requested: &str) -> Option<ModelInfo> {
1290    let requested = requested.trim();
1291    if requested.is_empty() || requested.chars().any(char::is_control) {
1292        return None;
1293    }
1294
1295    Some(ModelInfo {
1296        id: requested.to_string(),
1297        provider: ProviderKind::XiaomiMimo,
1298        aliases: Vec::new(),
1299        supports_tools: true,
1300        supports_reasoning: true,
1301    })
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306    use super::*;
1307
1308    #[test]
1309    fn deepseek_v4_pro_alias_stays_deepseek_by_default() {
1310        let registry = ModelRegistry::default();
1311        let resolved = registry.resolve(Some("deepseek-v4-pro"), None);
1312
1313        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1314        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1315    }
1316
1317    #[test]
1318    fn deepseek_v4_pro_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1319        let registry = ModelRegistry::default();
1320        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::NvidiaNim));
1321
1322        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1323        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1324    }
1325
1326    #[test]
1327    fn nvidia_nim_default_uses_catalog_model_id() {
1328        let registry = ModelRegistry::default();
1329        let resolved = registry.resolve(None, Some(ProviderKind::NvidiaNim));
1330
1331        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1332        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1333    }
1334
1335    #[test]
1336    fn deepseek_v4_flash_alias_resolves_to_nvidia_nim_when_provider_hinted() {
1337        let registry = ModelRegistry::default();
1338        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::NvidiaNim));
1339
1340        assert_eq!(resolved.resolved.provider, ProviderKind::NvidiaNim);
1341        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1342    }
1343
1344    #[test]
1345    fn atlascloud_default_uses_namespaced_model_id() {
1346        let registry = ModelRegistry::default();
1347        let resolved = registry.resolve(None, Some(ProviderKind::Atlascloud));
1348
1349        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1350        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1351        assert!(resolved.resolved.supports_reasoning);
1352    }
1353
1354    #[test]
1355    fn deepseek_v4_flash_alias_resolves_to_atlascloud_when_provider_hinted() {
1356        let registry = ModelRegistry::default();
1357        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Atlascloud));
1358
1359        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1360        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1361    }
1362
1363    #[test]
1364    fn deepseek_v4_pro_alias_resolves_to_atlascloud_when_provider_hinted() {
1365        let registry = ModelRegistry::default();
1366        let resolved = registry.resolve(Some("deepseek-v4-pro"), Some(ProviderKind::Atlascloud));
1367
1368        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1369        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-pro");
1370    }
1371
1372    #[test]
1373    fn atlascloud_provider_hint_passes_through_explicit_model_id() {
1374        let registry = ModelRegistry::default();
1375        let resolved =
1376            registry.resolve(Some("openai/gpt-5.2-chat"), Some(ProviderKind::Atlascloud));
1377
1378        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1379        assert_eq!(resolved.resolved.id, "openai/gpt-5.2-chat");
1380        assert!(resolved.resolved.supports_tools);
1381        assert!(resolved.resolved.supports_reasoning);
1382        assert!(!resolved.used_fallback);
1383    }
1384
1385    #[test]
1386    fn atlascloud_provider_hint_preserves_explicit_model_id_case() {
1387        let registry = ModelRegistry::default();
1388        let resolved = registry.resolve(Some("Qwen/Qwen3-Coder"), Some(ProviderKind::Atlascloud));
1389
1390        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1391        assert_eq!(resolved.resolved.id, "Qwen/Qwen3-Coder");
1392        assert!(!resolved.used_fallback);
1393    }
1394
1395    #[test]
1396    fn atlascloud_plain_unknown_model_still_uses_provider_default() {
1397        let registry = ModelRegistry::default();
1398        let resolved = registry.resolve(Some("not-in-atlas"), Some(ProviderKind::Atlascloud));
1399
1400        assert_eq!(resolved.resolved.provider, ProviderKind::Atlascloud);
1401        assert_eq!(resolved.resolved.id, "deepseek-ai/deepseek-v4-flash");
1402        assert!(resolved.used_fallback);
1403    }
1404
1405    #[test]
1406    fn openrouter_default_uses_namespaced_model_id() {
1407        let registry = ModelRegistry::default();
1408        let resolved = registry.resolve(None, Some(ProviderKind::Openrouter));
1409
1410        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1411        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1412    }
1413
1414    #[test]
1415    fn xiaomi_mimo_default_uses_canonical_model_id() {
1416        let registry = ModelRegistry::default();
1417        let resolved = registry.resolve(None, Some(ProviderKind::XiaomiMimo));
1418
1419        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1420        assert_eq!(resolved.resolved.id, "mimo-v2.5-pro");
1421        assert!(resolved.resolved.supports_reasoning);
1422    }
1423
1424    #[test]
1425    fn moonshot_default_and_aliases_use_kimi_k27_code() {
1426        let registry = ModelRegistry::default();
1427
1428        for requested in [None, Some("kimi"), Some("kimi-k2.7-code")] {
1429            let resolved = registry.resolve(requested, Some(ProviderKind::Moonshot));
1430
1431            assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1432            assert_eq!(resolved.resolved.id, "kimi-k2.7-code");
1433            assert!(resolved.resolved.supports_tools);
1434            assert!(resolved.resolved.supports_reasoning);
1435        }
1436    }
1437
1438    #[test]
1439    fn moonshot_explicit_kimi_k26_remains_available() {
1440        let registry = ModelRegistry::default();
1441        let resolved = registry.resolve(Some("kimi-k2.6"), Some(ProviderKind::Moonshot));
1442
1443        assert_eq!(resolved.resolved.provider, ProviderKind::Moonshot);
1444        assert_eq!(resolved.resolved.id, "kimi-k2.6");
1445        assert!(resolved.resolved.supports_reasoning);
1446    }
1447
1448    #[test]
1449    fn xiaomi_mimo_tts_aliases_resolve_when_provider_hinted() {
1450        let registry = ModelRegistry::default();
1451        let resolved = registry.resolve(Some("tts"), Some(ProviderKind::XiaomiMimo));
1452        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1453        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts");
1454        assert!(!resolved.resolved.supports_tools);
1455        assert!(!resolved.resolved.supports_reasoning);
1456
1457        let resolved = registry.resolve(Some("voice-design"), Some(ProviderKind::XiaomiMimo));
1458        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voicedesign");
1459
1460        let resolved = registry.resolve(Some("voiceclone"), Some(ProviderKind::XiaomiMimo));
1461        assert_eq!(resolved.resolved.id, "mimo-v2.5-tts-voiceclone");
1462    }
1463
1464    #[test]
1465    fn xiaomi_mimo_chat_aliases_resolve_when_provider_hinted() {
1466        let registry = ModelRegistry::default();
1467
1468        let resolved = registry.resolve(Some("omni"), Some(ProviderKind::XiaomiMimo));
1469        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1470        assert_eq!(resolved.resolved.id, "mimo-v2.5");
1471        assert!(resolved.resolved.supports_tools);
1472    }
1473
1474    #[test]
1475    fn xiaomi_mimo_provider_hint_preserves_custom_model_id() {
1476        let registry = ModelRegistry::default();
1477        let resolved =
1478            registry.resolve(Some("account-custom-mimo"), Some(ProviderKind::XiaomiMimo));
1479
1480        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1481        assert_eq!(resolved.resolved.id, "account-custom-mimo");
1482        assert!(!resolved.used_fallback);
1483    }
1484
1485    #[test]
1486    fn xiaomi_mimo_provider_hint_does_not_reclassify_openrouter_model_id() {
1487        let registry = ModelRegistry::default();
1488        let resolved = registry.resolve(
1489            Some("deepseek/deepseek-v4-pro"),
1490            Some(ProviderKind::XiaomiMimo),
1491        );
1492
1493        assert_eq!(resolved.resolved.provider, ProviderKind::XiaomiMimo);
1494        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1495        assert!(!resolved.used_fallback);
1496    }
1497
1498    #[test]
1499    fn wanjie_ark_default_uses_reasoner_model_id() {
1500        let registry = ModelRegistry::default();
1501        let resolved = registry.resolve(None, Some(ProviderKind::WanjieArk));
1502
1503        assert_eq!(resolved.resolved.provider, ProviderKind::WanjieArk);
1504        assert_eq!(resolved.resolved.id, "deepseek-reasoner");
1505        assert!(resolved.resolved.supports_reasoning);
1506    }
1507
1508    #[test]
1509    fn novita_default_uses_namespaced_model_id() {
1510        let registry = ModelRegistry::default();
1511        let resolved = registry.resolve(None, Some(ProviderKind::Novita));
1512
1513        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1514        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-pro");
1515    }
1516
1517    #[test]
1518    fn fireworks_default_uses_canonical_model_id() {
1519        let registry = ModelRegistry::default();
1520        let resolved = registry.resolve(None, Some(ProviderKind::Fireworks));
1521
1522        assert_eq!(resolved.resolved.provider, ProviderKind::Fireworks);
1523        assert_eq!(
1524            resolved.resolved.id,
1525            "accounts/fireworks/models/deepseek-v4-pro"
1526        );
1527    }
1528
1529    #[test]
1530    fn siliconflow_default_uses_canonical_pro_model_id() {
1531        let registry = ModelRegistry::default();
1532        let resolved = registry.resolve(None, Some(ProviderKind::Siliconflow));
1533
1534        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1535        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1536        assert!(resolved.resolved.supports_reasoning);
1537    }
1538
1539    #[test]
1540    fn arcee_default_uses_direct_trinity_large_thinking_model_id() {
1541        let registry = ModelRegistry::default();
1542        let resolved = registry.resolve(None, Some(ProviderKind::Arcee));
1543
1544        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1545        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1546        assert!(resolved.resolved.supports_reasoning);
1547    }
1548
1549    #[test]
1550    fn arcee_trinity_alias_resolves_to_direct_large_thinking_not_openrouter() {
1551        let registry = ModelRegistry::default();
1552        let resolved = registry.resolve(Some("trinity"), Some(ProviderKind::Arcee));
1553
1554        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1555        assert_eq!(resolved.resolved.id, "trinity-large-thinking");
1556        assert!(resolved.resolved.supports_reasoning);
1557    }
1558
1559    #[test]
1560    fn arcee_trinity_mini_remains_explicit_compatibility_model() {
1561        let registry = ModelRegistry::default();
1562        let resolved = registry.resolve(Some("trinity-mini"), Some(ProviderKind::Arcee));
1563
1564        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1565        assert_eq!(resolved.resolved.id, "trinity-mini");
1566        assert!(resolved.resolved.supports_reasoning);
1567        assert!(!resolved.used_fallback);
1568    }
1569
1570    #[test]
1571    fn arcee_provider_hint_preserves_explicit_future_model_id() {
1572        let registry = ModelRegistry::default();
1573        let resolved = registry.resolve(Some("trinity-large-next"), Some(ProviderKind::Arcee));
1574
1575        assert_eq!(resolved.resolved.provider, ProviderKind::Arcee);
1576        assert_eq!(resolved.resolved.id, "trinity-large-next");
1577        assert!(!resolved.resolved.supports_reasoning);
1578        assert!(!resolved.used_fallback);
1579    }
1580
1581    #[test]
1582    fn deepseek_reasoner_alias_resolves_to_siliconflow_pro_when_provider_hinted() {
1583        let registry = ModelRegistry::default();
1584        let resolved = registry.resolve(Some("deepseek-reasoner"), Some(ProviderKind::Siliconflow));
1585
1586        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1587        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1588    }
1589
1590    #[test]
1591    fn deepseek_v4_flash_alias_resolves_to_siliconflow_flash_when_provider_hinted() {
1592        let registry = ModelRegistry::default();
1593        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Siliconflow));
1594
1595        assert_eq!(resolved.resolved.provider, ProviderKind::Siliconflow);
1596        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1597    }
1598
1599    #[test]
1600    fn sglang_default_uses_canonical_model_id() {
1601        let registry = ModelRegistry::default();
1602        let resolved = registry.resolve(None, Some(ProviderKind::Sglang));
1603
1604        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1605        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1606    }
1607
1608    #[test]
1609    fn zai_direct_models_resolve_when_provider_hinted() {
1610        let registry = ModelRegistry::default();
1611
1612        // GLM-5.2 is now the default direct Z.AI model.
1613        let default = registry.resolve(None, Some(ProviderKind::Zai));
1614        assert_eq!(default.resolved.provider, ProviderKind::Zai);
1615        assert_eq!(default.resolved.id, "GLM-5.2");
1616
1617        for (alias, expected) in [
1618            ("GLM-5.1", "GLM-5.1"),
1619            ("glm-5-1", "GLM-5.1"),
1620            ("GLM-5.2", "GLM-5.2"),
1621            ("glm-5.2", "GLM-5.2"),
1622            ("zai-glm-5-2", "GLM-5.2"),
1623            ("GLM-5-Turbo", "GLM-5-Turbo"),
1624            ("glm-5-turbo", "GLM-5-Turbo"),
1625            ("zai-glm-5-turbo", "GLM-5-Turbo"),
1626        ] {
1627            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Zai));
1628
1629            assert_eq!(resolved.resolved.provider, ProviderKind::Zai);
1630            assert_eq!(resolved.resolved.id, expected);
1631            assert!(!resolved.used_fallback);
1632            assert!(resolved.resolved.supports_tools);
1633            assert!(resolved.resolved.supports_reasoning);
1634        }
1635    }
1636
1637    #[test]
1638    fn first_party_recent_provider_models_are_listed() {
1639        let registry = ModelRegistry::default();
1640        let models = registry.list();
1641
1642        for (provider, id) in [
1643            (ProviderKind::Zai, "GLM-5.2"),
1644            (ProviderKind::Stepfun, "step-3.7-flash"),
1645            (ProviderKind::Minimax, "MiniMax-M2.1"),
1646            (ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
1647            (ProviderKind::Openmodel, "deepseek-v4-flash"),
1648            (ProviderKind::Meta, "muse-spark-1.1"),
1649            (ProviderKind::Xai, "grok-4.5"),
1650        ] {
1651            assert!(
1652                models
1653                    .iter()
1654                    .any(|model| model.provider == provider && model.id == id),
1655                "expected {provider:?} model {id} in registry"
1656            );
1657        }
1658    }
1659
1660    #[test]
1661    fn xai_grok_models_resolve_when_provider_hinted() {
1662        let registry = ModelRegistry::default();
1663
1664        let default = registry.resolve(None, Some(ProviderKind::Xai));
1665        assert_eq!(default.resolved.provider, ProviderKind::Xai);
1666        assert_eq!(default.resolved.id, "grok-4.5");
1667        assert!(default.used_fallback);
1668
1669        let alias = registry.resolve(Some("grok"), Some(ProviderKind::Xai));
1670        assert_eq!(alias.resolved.provider, ProviderKind::Xai);
1671        assert_eq!(alias.resolved.id, "grok-4.5");
1672        assert!(!alias.used_fallback);
1673
1674        let fast = registry.resolve(
1675            Some("grok-4.20-0309-non-reasoning"),
1676            Some(ProviderKind::Xai),
1677        );
1678        assert_eq!(fast.resolved.provider, ProviderKind::Xai);
1679        assert_eq!(fast.resolved.id, "grok-4.20-0309-non-reasoning");
1680        assert!(!fast.resolved.supports_reasoning);
1681    }
1682
1683    #[test]
1684    fn meta_muse_spark_resolves_when_provider_hinted() {
1685        let registry = ModelRegistry::default();
1686
1687        let default = registry.resolve(None, Some(ProviderKind::Meta));
1688        assert_eq!(default.resolved.provider, ProviderKind::Meta);
1689        assert_eq!(default.resolved.id, "muse-spark-1.1");
1690        assert!(default.used_fallback);
1691
1692        let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
1693        assert_eq!(alias.resolved.provider, ProviderKind::Meta);
1694        assert_eq!(alias.resolved.id, "muse-spark-1.1");
1695        assert!(!alias.used_fallback);
1696        assert_eq!(model_family("muse-spark-1.1"), ModelFamily::Meta);
1697    }
1698
1699    #[test]
1700    fn openai_gpt56_family_resolves_when_provider_hinted() {
1701        let registry = ModelRegistry::default();
1702        for model in ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] {
1703            let resolved = registry.resolve(Some(model), Some(ProviderKind::Openai));
1704            assert_eq!(resolved.resolved.provider, ProviderKind::Openai, "{model}");
1705            assert_eq!(resolved.resolved.id, model, "{model}");
1706            assert!(resolved.resolved.supports_tools, "{model}");
1707            assert!(resolved.resolved.supports_reasoning, "{model}");
1708            assert!(!resolved.used_fallback, "{model}");
1709        }
1710    }
1711
1712    #[test]
1713    fn grok_ids_stay_in_grok_family() {
1714        assert_eq!(model_family("grok-4.5"), ModelFamily::Grok);
1715        assert_eq!(
1716            model_family("grok-4.20-0309-non-reasoning"),
1717            ModelFamily::Grok
1718        );
1719    }
1720
1721    #[test]
1722    fn stepfun_and_minimax_direct_models_resolve_when_provider_hinted() {
1723        let registry = ModelRegistry::default();
1724
1725        let stepfun = registry.resolve(None, Some(ProviderKind::Stepfun));
1726        assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
1727        assert_eq!(stepfun.resolved.id, "step-3.7-flash");
1728
1729        for (alias, expected) in [
1730            ("minimax", "MiniMax-M3"),
1731            ("minimax-m3", "MiniMax-M3"),
1732            ("minimax-m2.7", "MiniMax-M2.7"),
1733            ("minimax-m2-7-highspeed", "MiniMax-M2.7-highspeed"),
1734            ("minimax-m2.1", "MiniMax-M2.1"),
1735            ("minimax-m2", "MiniMax-M2"),
1736        ] {
1737            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Minimax));
1738
1739            assert_eq!(resolved.resolved.provider, ProviderKind::Minimax);
1740            assert_eq!(resolved.resolved.id, expected);
1741            assert!(!resolved.used_fallback);
1742            assert!(resolved.resolved.supports_tools);
1743            assert!(resolved.resolved.supports_reasoning);
1744        }
1745    }
1746
1747    #[test]
1748    fn minimax_anthropic_models_resolve_when_provider_hinted() {
1749        let registry = ModelRegistry::default();
1750
1751        for (alias, expected) in [
1752            ("minimax-anthropic", "MiniMax-M3"),
1753            ("minimax-m3", "MiniMax-M3"),
1754            ("minimax-m2.7", "MiniMax-M2.7"),
1755        ] {
1756            let resolved = registry.resolve(Some(alias), Some(ProviderKind::MinimaxAnthropic));
1757
1758            assert_eq!(resolved.resolved.provider, ProviderKind::MinimaxAnthropic);
1759            assert_eq!(resolved.resolved.id, expected);
1760            assert!(!resolved.used_fallback);
1761            assert!(resolved.resolved.supports_tools);
1762            assert!(resolved.resolved.supports_reasoning);
1763        }
1764    }
1765
1766    #[test]
1767    fn deepseek_v4_flash_alias_resolves_to_openrouter_when_provider_hinted() {
1768        let registry = ModelRegistry::default();
1769        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Openrouter));
1770
1771        assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1772        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1773    }
1774
1775    #[test]
1776    fn recent_openrouter_large_model_aliases_resolve_when_provider_hinted() {
1777        let registry = ModelRegistry::default();
1778
1779        for (alias, expected) in [
1780            ("trinity-large-thinking", "arcee-ai/trinity-large-thinking"),
1781            ("qwen3.6-flash", "qwen/qwen3.6-flash"),
1782            ("qwen3.6-35b-a3b", "qwen/qwen3.6-35b-a3b"),
1783            ("qwen3.6-max-preview", "qwen/qwen3.6-max-preview"),
1784            ("qwen3.6-plus", "qwen/qwen3.6-plus"),
1785            ("gemma-4-31b-it", "google/gemma-4-31b-it"),
1786            ("glm-5.1", "z-ai/glm-5.1"),
1787            ("glm-5.2", "z-ai/glm-5.2"),
1788            ("minimax-m3", "minimax/minimax-m3"),
1789            ("minimax-2.7", "minimax/minimax-m2.7"),
1790            ("openrouter-mimo-v2.5-pro", "xiaomi/mimo-v2.5-pro"),
1791            ("openrouter-kimi-k2.7-code", "moonshotai/kimi-k2.7-code"),
1792            ("openrouter-kimi-k2.6", "moonshotai/kimi-k2.6"),
1793            ("nemotron-3-ultra", "nvidia/nemotron-3-ultra-550b-a55b"),
1794            (
1795                "nvidia/nemotron-3-ultra",
1796                "nvidia/nemotron-3-ultra-550b-a55b",
1797            ),
1798        ] {
1799            let resolved = registry.resolve(Some(alias), Some(ProviderKind::Openrouter));
1800
1801            assert_eq!(resolved.resolved.provider, ProviderKind::Openrouter);
1802            assert_eq!(resolved.resolved.id, expected);
1803            assert!(resolved.resolved.supports_tools);
1804            assert!(resolved.resolved.supports_reasoning);
1805        }
1806    }
1807
1808    #[test]
1809    fn deepseek_v4_flash_alias_resolves_to_novita_when_provider_hinted() {
1810        let registry = ModelRegistry::default();
1811        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Novita));
1812
1813        assert_eq!(resolved.resolved.provider, ProviderKind::Novita);
1814        assert_eq!(resolved.resolved.id, "deepseek/deepseek-v4-flash");
1815    }
1816
1817    #[test]
1818    fn together_inkling_keeps_published_wire_identity() {
1819        let registry = ModelRegistry::default();
1820        for requested in ["thinkingmachines/inkling", "inkling", "together-inkling"] {
1821            let resolved = registry.resolve(Some(requested), Some(ProviderKind::Together));
1822
1823            assert_eq!(resolved.resolved.provider, ProviderKind::Together);
1824            assert_eq!(resolved.resolved.id, "thinkingmachines/inkling");
1825            assert!(resolved.resolved.supports_tools);
1826            assert!(resolved.resolved.supports_reasoning);
1827            assert!(!resolved.used_fallback);
1828        }
1829
1830        let unscoped = registry.resolve(Some("inkling"), None);
1831        assert_eq!(unscoped.resolved.provider, ProviderKind::Together);
1832        assert_eq!(unscoped.resolved.id, "thinkingmachines/inkling");
1833        assert!(!unscoped.used_fallback);
1834    }
1835
1836    #[test]
1837    fn registry_lists_and_resolves_every_v090_catalog_addition() {
1838        let registry = ModelRegistry::default();
1839        let advertised = [
1840            (ProviderKind::Anthropic, "claude-sonnet-5"),
1841            (ProviderKind::Anthropic, "claude-fable-5"),
1842            (ProviderKind::Openai, "gpt-5.3-codex"),
1843            (ProviderKind::Openai, "gpt-5.5"),
1844            (ProviderKind::Openai, "gpt-5.5-pro"),
1845            (ProviderKind::Openrouter, "qwen/qwen3.7-plus"),
1846            (ProviderKind::Arcee, "trinity-mini"),
1847        ];
1848
1849        let listed = registry.list();
1850        for (provider, model_id) in advertised {
1851            assert!(
1852                listed
1853                    .iter()
1854                    .any(|model| model.provider == provider && model.id == model_id),
1855                "missing {model_id} ({}) from model list",
1856                provider.as_str()
1857            );
1858            let resolved = registry.resolve(Some(model_id), Some(provider));
1859            assert_eq!(resolved.resolved.provider, provider, "{model_id}");
1860            assert_eq!(resolved.resolved.id, model_id, "{model_id}");
1861            assert!(!resolved.used_fallback, "{model_id}");
1862        }
1863    }
1864
1865    #[test]
1866    fn gpt_55_stays_provider_scoped_between_openai_and_codex() {
1867        let registry = ModelRegistry::default();
1868
1869        let unscoped = registry.resolve(Some("gpt-5.5"), None);
1870        assert_eq!(unscoped.resolved.provider, ProviderKind::Openai);
1871        assert_eq!(unscoped.resolved.id, "gpt-5.5");
1872        assert!(!unscoped.used_fallback);
1873
1874        let codex = registry.resolve(Some("gpt-5.5"), Some(ProviderKind::OpenaiCodex));
1875        assert_eq!(codex.resolved.provider, ProviderKind::OpenaiCodex);
1876        assert_eq!(codex.resolved.id, "gpt-5.5");
1877        assert!(!codex.used_fallback);
1878    }
1879
1880    #[test]
1881    fn deepseek_v4_flash_alias_resolves_to_sglang_when_provider_hinted() {
1882        let registry = ModelRegistry::default();
1883        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Sglang));
1884
1885        assert_eq!(resolved.resolved.provider, ProviderKind::Sglang);
1886        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1887    }
1888
1889    #[test]
1890    fn vllm_default_uses_canonical_model_id() {
1891        let registry = ModelRegistry::default();
1892        let resolved = registry.resolve(None, Some(ProviderKind::Vllm));
1893
1894        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1895        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Pro");
1896    }
1897
1898    #[test]
1899    fn ollama_default_uses_small_local_model_id() {
1900        let registry = ModelRegistry::default();
1901        let resolved = registry.resolve(None, Some(ProviderKind::Ollama));
1902
1903        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1904        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1905        assert!(resolved.resolved.supports_reasoning);
1906    }
1907
1908    #[test]
1909    fn ollama_requested_model_tag_is_preserved() {
1910        let registry = ModelRegistry::default();
1911        let resolved = registry.resolve(Some("qwen2.5-coder:7b"), Some(ProviderKind::Ollama));
1912
1913        assert_eq!(resolved.resolved.provider, ProviderKind::Ollama);
1914        assert_eq!(resolved.resolved.id, "qwen2.5-coder:7b");
1915        assert!(!resolved.used_fallback);
1916    }
1917
1918    #[test]
1919    fn deepseek_v4_flash_alias_resolves_to_vllm_when_provider_hinted() {
1920        let registry = ModelRegistry::default();
1921        let resolved = registry.resolve(Some("deepseek-v4-flash"), Some(ProviderKind::Vllm));
1922
1923        assert_eq!(resolved.resolved.provider, ProviderKind::Vllm);
1924        assert_eq!(resolved.resolved.id, "deepseek-ai/DeepSeek-V4-Flash");
1925    }
1926
1927    #[test]
1928    fn preserves_requested_model_casing_for_third_party_providers() {
1929        let registry = ModelRegistry::default();
1930        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), None);
1931
1932        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1933        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1934    }
1935
1936    #[test]
1937    fn registry_casing_takes_priority_over_requested_casing_with_provider_hint() {
1938        let registry = ModelRegistry::default();
1939        let resolved = registry.resolve(Some("DeepSeek-V4-Pro"), Some(ProviderKind::Deepseek));
1940
1941        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1942        // Registry's canonical id is used even when user provides different casing
1943        assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
1944    }
1945
1946    #[test]
1947    fn preserves_requested_model_casing_without_surrounding_whitespace() {
1948        let registry = ModelRegistry::default();
1949        let resolved = registry.resolve(Some("  DeepSeek-V4-Pro  "), None);
1950
1951        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1952        assert_eq!(resolved.resolved.id, "DeepSeek-V4-Pro");
1953    }
1954
1955    #[test]
1956    fn alias_match_does_not_override_requested_casing() {
1957        let registry = ModelRegistry::default();
1958        let resolved = registry.resolve(Some("deepseek-reasoner"), None);
1959
1960        assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
1961        assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
1962    }
1963
1964    #[test]
1965    fn model_family_classifies_known_model_ids() {
1966        assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
1967        assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
1968        assert_eq!(
1969            model_family("anthropic/claude-opus-4-7"),
1970            ModelFamily::Anthropic
1971        );
1972        assert_eq!(
1973            model_family("meta-llama/llama-3.3-70b-instruct"),
1974            ModelFamily::Meta
1975        );
1976        assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
1977    }
1978
1979    #[test]
1980    fn model_family_uses_underlying_model_for_router_ids() {
1981        assert_eq!(
1982            model_family("groq/llama-3.3-70b-versatile"),
1983            ModelFamily::Meta
1984        );
1985        assert_eq!(
1986            model_family("openrouter/openai/gpt-5.4"),
1987            ModelFamily::OpenAI
1988        );
1989        assert_eq!(
1990            model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
1991            ModelFamily::DeepSeek
1992        );
1993    }
1994
1995    #[test]
1996    fn model_family_covers_prominent_google_and_mistral_model_names() {
1997        assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
1998        assert_eq!(
1999            model_family("mistralai/mixtral-8x22b"),
2000            ModelFamily::Mistral
2001        );
2002        assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
2003    }
2004
2005    #[test]
2006    fn model_family_falls_back_to_inferencer_for_unknown_models() {
2007        assert_eq!(
2008            model_family("custom-gateway/my-private-model"),
2009            ModelFamily::Inferencer
2010        );
2011        assert_eq!(model_family(""), ModelFamily::Inferencer);
2012    }
2013}