vtcode_core/config/
models.rs

1//! Model configuration and identification module
2//!
3//! This module provides a centralized enum for model identifiers and their configurations,
4//! replacing hardcoded model strings throughout the codebase for better maintainability.
5//! Read the model list in `docs/models.json`.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10
11/// Supported AI model providers
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13pub enum Provider {
14    /// Google Gemini models
15    #[default]
16    Gemini,
17    /// OpenAI GPT models
18    OpenAI,
19    /// Anthropic Claude models
20    Anthropic,
21    /// DeepSeek native models
22    DeepSeek,
23    /// OpenRouter marketplace models
24    OpenRouter,
25    /// xAI Grok models
26    XAI,
27}
28
29impl Provider {
30    /// Get the default API key environment variable for this provider
31    pub fn default_api_key_env(&self) -> &'static str {
32        match self {
33            Provider::Gemini => "GEMINI_API_KEY",
34            Provider::OpenAI => "OPENAI_API_KEY",
35            Provider::Anthropic => "ANTHROPIC_API_KEY",
36            Provider::DeepSeek => "DEEPSEEK_API_KEY",
37            Provider::OpenRouter => "OPENROUTER_API_KEY",
38            Provider::XAI => "XAI_API_KEY",
39        }
40    }
41
42    /// Get all supported providers
43    pub fn all_providers() -> Vec<Provider> {
44        vec![
45            Provider::Gemini,
46            Provider::OpenAI,
47            Provider::Anthropic,
48            Provider::DeepSeek,
49            Provider::OpenRouter,
50            Provider::XAI,
51        ]
52    }
53
54    /// Human-friendly label for display purposes
55    pub fn label(&self) -> &'static str {
56        match self {
57            Provider::Gemini => "Gemini",
58            Provider::OpenAI => "OpenAI",
59            Provider::Anthropic => "Anthropic",
60            Provider::DeepSeek => "DeepSeek",
61            Provider::OpenRouter => "OpenRouter",
62            Provider::XAI => "xAI",
63        }
64    }
65
66    /// Determine if the provider supports configurable reasoning effort for the model
67    pub fn supports_reasoning_effort(&self, model: &str) -> bool {
68        use crate::config::constants::models;
69
70        match self {
71            Provider::Gemini => model == models::google::GEMINI_2_5_PRO,
72            Provider::OpenAI => models::openai::REASONING_MODELS.contains(&model),
73            Provider::Anthropic => models::anthropic::SUPPORTED_MODELS.contains(&model),
74            Provider::DeepSeek => model == models::deepseek::DEEPSEEK_REASONER,
75            Provider::OpenRouter => models::openrouter::REASONING_MODELS.contains(&model),
76            Provider::XAI => model == models::xai::GROK_2_REASONING,
77        }
78    }
79}
80
81impl fmt::Display for Provider {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Provider::Gemini => write!(f, "gemini"),
85            Provider::OpenAI => write!(f, "openai"),
86            Provider::Anthropic => write!(f, "anthropic"),
87            Provider::DeepSeek => write!(f, "deepseek"),
88            Provider::OpenRouter => write!(f, "openrouter"),
89            Provider::XAI => write!(f, "xai"),
90        }
91    }
92}
93
94impl FromStr for Provider {
95    type Err = ModelParseError;
96
97    fn from_str(s: &str) -> Result<Self, Self::Err> {
98        match s.to_lowercase().as_str() {
99            "gemini" => Ok(Provider::Gemini),
100            "openai" => Ok(Provider::OpenAI),
101            "anthropic" => Ok(Provider::Anthropic),
102            "deepseek" => Ok(Provider::DeepSeek),
103            "openrouter" => Ok(Provider::OpenRouter),
104            "xai" => Ok(Provider::XAI),
105            _ => Err(ModelParseError::InvalidProvider(s.to_string())),
106        }
107    }
108}
109
110/// Centralized enum for all supported model identifiers
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
112pub enum ModelId {
113    // Gemini models
114    /// Gemini 2.5 Flash Preview - Latest fast model with advanced capabilities
115    Gemini25FlashPreview,
116    /// Gemini 2.5 Flash - Legacy alias for flash preview
117    Gemini25Flash,
118    /// Gemini 2.5 Flash Lite - Legacy alias for flash preview (lite)
119    Gemini25FlashLite,
120    /// Gemini 2.5 Pro - Latest most capable Gemini model
121    Gemini25Pro,
122
123    // OpenAI models
124    /// GPT-5 - Latest most capable OpenAI model (2025-08-07)
125    GPT5,
126    /// GPT-5 Mini - Latest efficient OpenAI model (2025-08-07)
127    GPT5Mini,
128    /// GPT-5 Nano - Latest most cost-effective OpenAI model (2025-08-07)
129    GPT5Nano,
130    /// Codex Mini Latest - Latest Codex model for code generation (2025-05-16)
131    CodexMiniLatest,
132
133    // Anthropic models
134    /// Claude Opus 4.1 - Latest most capable Anthropic model (2025-08-05)
135    ClaudeOpus41,
136    /// Claude Sonnet 4.5 - Latest balanced Anthropic model (2025-09-29)
137    ClaudeSonnet45,
138    /// Claude Sonnet 4 - Previous balanced Anthropic model (2025-05-14)
139    ClaudeSonnet4,
140
141    // DeepSeek models
142    /// DeepSeek V3.2-Exp Chat - Non-thinking mode
143    DeepSeekChat,
144    /// DeepSeek V3.2-Exp Reasoner - Thinking mode with deliberate reasoning output
145    DeepSeekReasoner,
146
147    // xAI models
148    /// Grok-2 Latest - Flagship xAI model with advanced reasoning
149    XaiGrok2Latest,
150    /// Grok-2 - Stable xAI model variant
151    XaiGrok2,
152    /// Grok-2 Mini - Efficient xAI model
153    XaiGrok2Mini,
154    /// Grok-2 Reasoning - Enhanced reasoning trace variant
155    XaiGrok2Reasoning,
156    /// Grok-2 Vision - Multimodal xAI model
157    XaiGrok2Vision,
158
159    // OpenRouter models
160    /// Grok Code Fast 1 - Fast OpenRouter coding model
161    OpenRouterGrokCodeFast1,
162    /// Qwen3 Coder - Balanced OpenRouter coding model
163    OpenRouterQwen3Coder,
164    /// DeepSeek Chat v3.1 - Advanced DeepSeek model via OpenRouter
165    OpenRouterDeepSeekChatV31,
166    /// OpenAI GPT-5 via OpenRouter
167    OpenRouterOpenAIGPT5,
168    /// Anthropic Claude Sonnet 4.5 via OpenRouter
169    OpenRouterAnthropicClaudeSonnet45,
170    /// Anthropic Claude Sonnet 4 via OpenRouter
171    OpenRouterAnthropicClaudeSonnet4,
172}
173impl ModelId {
174    /// Convert the model identifier to its string representation
175    /// used in API calls and configurations
176    pub fn as_str(&self) -> &'static str {
177        use crate::config::constants::models;
178        match self {
179            // Gemini models
180            ModelId::Gemini25FlashPreview => models::GEMINI_2_5_FLASH_PREVIEW,
181            ModelId::Gemini25Flash => models::GEMINI_2_5_FLASH,
182            ModelId::Gemini25FlashLite => models::GEMINI_2_5_FLASH_LITE,
183            ModelId::Gemini25Pro => models::GEMINI_2_5_PRO,
184            // OpenAI models
185            ModelId::GPT5 => models::GPT_5,
186            ModelId::GPT5Mini => models::GPT_5_MINI,
187            ModelId::GPT5Nano => models::GPT_5_NANO,
188            ModelId::CodexMiniLatest => models::CODEX_MINI_LATEST,
189            // Anthropic models
190            ModelId::ClaudeOpus41 => models::CLAUDE_OPUS_4_1_20250805,
191            ModelId::ClaudeSonnet45 => models::CLAUDE_SONNET_4_5,
192            ModelId::ClaudeSonnet4 => models::CLAUDE_SONNET_4_20250514,
193            // DeepSeek models
194            ModelId::DeepSeekChat => models::DEEPSEEK_CHAT,
195            ModelId::DeepSeekReasoner => models::DEEPSEEK_REASONER,
196            // xAI models
197            ModelId::XaiGrok2Latest => models::xai::GROK_2_LATEST,
198            ModelId::XaiGrok2 => models::xai::GROK_2,
199            ModelId::XaiGrok2Mini => models::xai::GROK_2_MINI,
200            ModelId::XaiGrok2Reasoning => models::xai::GROK_2_REASONING,
201            ModelId::XaiGrok2Vision => models::xai::GROK_2_VISION,
202            // OpenRouter models
203            ModelId::OpenRouterGrokCodeFast1 => models::OPENROUTER_X_AI_GROK_CODE_FAST_1,
204            ModelId::OpenRouterQwen3Coder => models::OPENROUTER_QWEN3_CODER,
205            ModelId::OpenRouterDeepSeekChatV31 => models::OPENROUTER_DEEPSEEK_CHAT_V3_1,
206            ModelId::OpenRouterOpenAIGPT5 => models::OPENROUTER_OPENAI_GPT_5,
207            ModelId::OpenRouterAnthropicClaudeSonnet45 => {
208                models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4_5
209            }
210            ModelId::OpenRouterAnthropicClaudeSonnet4 => {
211                models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4
212            }
213        }
214    }
215
216    /// Get the provider for this model
217    pub fn provider(&self) -> Provider {
218        match self {
219            ModelId::Gemini25FlashPreview
220            | ModelId::Gemini25Flash
221            | ModelId::Gemini25FlashLite
222            | ModelId::Gemini25Pro => Provider::Gemini,
223            ModelId::GPT5 | ModelId::GPT5Mini | ModelId::GPT5Nano | ModelId::CodexMiniLatest => {
224                Provider::OpenAI
225            }
226            ModelId::ClaudeOpus41 | ModelId::ClaudeSonnet45 | ModelId::ClaudeSonnet4 => {
227                Provider::Anthropic
228            }
229            ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => Provider::DeepSeek,
230            ModelId::XaiGrok2Latest
231            | ModelId::XaiGrok2
232            | ModelId::XaiGrok2Mini
233            | ModelId::XaiGrok2Reasoning
234            | ModelId::XaiGrok2Vision => Provider::XAI,
235            ModelId::OpenRouterGrokCodeFast1
236            | ModelId::OpenRouterQwen3Coder
237            | ModelId::OpenRouterDeepSeekChatV31
238            | ModelId::OpenRouterOpenAIGPT5
239            | ModelId::OpenRouterAnthropicClaudeSonnet45
240            | ModelId::OpenRouterAnthropicClaudeSonnet4 => Provider::OpenRouter,
241        }
242    }
243
244    /// Whether this model supports configurable reasoning effort levels
245    pub fn supports_reasoning_effort(&self) -> bool {
246        self.provider().supports_reasoning_effort(self.as_str())
247    }
248
249    /// Get the display name for the model (human-readable)
250    pub fn display_name(&self) -> &'static str {
251        match self {
252            // Gemini models
253            ModelId::Gemini25FlashPreview => "Gemini 2.5 Flash Preview",
254            ModelId::Gemini25Flash => "Gemini 2.5 Flash",
255            ModelId::Gemini25FlashLite => "Gemini 2.5 Flash Lite",
256            ModelId::Gemini25Pro => "Gemini 2.5 Pro",
257            // OpenAI models
258            ModelId::GPT5 => "GPT-5",
259            ModelId::GPT5Mini => "GPT-5 Mini",
260            ModelId::GPT5Nano => "GPT-5 Nano",
261            ModelId::CodexMiniLatest => "Codex Mini Latest",
262            // Anthropic models
263            ModelId::ClaudeOpus41 => "Claude Opus 4.1",
264            ModelId::ClaudeSonnet45 => "Claude Sonnet 4.5",
265            ModelId::ClaudeSonnet4 => "Claude Sonnet 4",
266            // DeepSeek models
267            ModelId::DeepSeekChat => "DeepSeek V3.2-Exp (Chat)",
268            ModelId::DeepSeekReasoner => "DeepSeek V3.2-Exp (Reasoner)",
269            // xAI models
270            ModelId::XaiGrok2Latest => "Grok-2 Latest",
271            ModelId::XaiGrok2 => "Grok-2",
272            ModelId::XaiGrok2Mini => "Grok-2 Mini",
273            ModelId::XaiGrok2Reasoning => "Grok-2 Reasoning",
274            ModelId::XaiGrok2Vision => "Grok-2 Vision",
275            // OpenRouter models
276            ModelId::OpenRouterGrokCodeFast1 => "Grok Code Fast 1",
277            ModelId::OpenRouterQwen3Coder => "Qwen3 Coder",
278            ModelId::OpenRouterDeepSeekChatV31 => "DeepSeek Chat v3.1",
279            ModelId::OpenRouterOpenAIGPT5 => "OpenAI GPT-5 via OpenRouter",
280            ModelId::OpenRouterAnthropicClaudeSonnet45 => {
281                "Anthropic Claude Sonnet 4.5 via OpenRouter"
282            }
283            ModelId::OpenRouterAnthropicClaudeSonnet4 => "Anthropic Claude Sonnet 4 via OpenRouter",
284        }
285    }
286
287    /// Get a description of the model's characteristics
288    pub fn description(&self) -> &'static str {
289        match self {
290            // Gemini models
291            ModelId::Gemini25FlashPreview => {
292                "Latest fast Gemini model with advanced multimodal capabilities"
293            }
294            ModelId::Gemini25Flash => {
295                "Legacy alias for Gemini 2.5 Flash Preview (same capabilities)"
296            }
297            ModelId::Gemini25FlashLite => {
298                "Legacy alias for Gemini 2.5 Flash Preview optimized for efficiency"
299            }
300            ModelId::Gemini25Pro => "Latest most capable Gemini model with reasoning",
301            // OpenAI models
302            ModelId::GPT5 => "Latest most capable OpenAI model with advanced reasoning",
303            ModelId::GPT5Mini => "Latest efficient OpenAI model, great for most tasks",
304            ModelId::GPT5Nano => "Latest most cost-effective OpenAI model",
305            ModelId::CodexMiniLatest => "Latest Codex model optimized for code generation",
306            // Anthropic models
307            ModelId::ClaudeOpus41 => "Latest most capable Anthropic model with advanced reasoning",
308            ModelId::ClaudeSonnet45 => "Latest balanced Anthropic model for general tasks",
309            ModelId::ClaudeSonnet4 => {
310                "Previous balanced Anthropic model maintained for compatibility"
311            }
312            // DeepSeek models
313            ModelId::DeepSeekChat => {
314                "DeepSeek V3.2-Exp non-thinking mode optimized for fast coding responses"
315            }
316            ModelId::DeepSeekReasoner => {
317                "DeepSeek V3.2-Exp thinking mode with structured reasoning output"
318            }
319            // xAI models
320            ModelId::XaiGrok2Latest => "Flagship xAI Grok model with long context and tool use",
321            ModelId::XaiGrok2 => "Stable Grok 2 release tuned for general coding tasks",
322            ModelId::XaiGrok2Mini => "Efficient Grok 2 variant optimized for latency",
323            ModelId::XaiGrok2Reasoning => {
324                "Grok 2 variant that surfaces structured reasoning traces"
325            }
326            ModelId::XaiGrok2Vision => "Multimodal Grok 2 model with image understanding",
327            // OpenRouter models
328            ModelId::OpenRouterGrokCodeFast1 => "Fast OpenRouter coding model powered by xAI Grok",
329            ModelId::OpenRouterQwen3Coder => {
330                "Qwen3-based OpenRouter model tuned for IDE-style coding workflows"
331            }
332            ModelId::OpenRouterDeepSeekChatV31 => "Advanced DeepSeek model via OpenRouter",
333            ModelId::OpenRouterOpenAIGPT5 => "OpenAI GPT-5 model accessed through OpenRouter",
334            ModelId::OpenRouterAnthropicClaudeSonnet45 => {
335                "Anthropic Claude Sonnet 4.5 model accessed through OpenRouter"
336            }
337            ModelId::OpenRouterAnthropicClaudeSonnet4 => {
338                "Anthropic Claude Sonnet 4 model accessed through OpenRouter"
339            }
340        }
341    }
342
343    /// Get all available models as a vector
344    pub fn all_models() -> Vec<ModelId> {
345        vec![
346            // Gemini models
347            ModelId::Gemini25FlashPreview,
348            ModelId::Gemini25Flash,
349            ModelId::Gemini25FlashLite,
350            ModelId::Gemini25Pro,
351            // OpenAI models
352            ModelId::GPT5,
353            ModelId::GPT5Mini,
354            ModelId::GPT5Nano,
355            ModelId::CodexMiniLatest,
356            // Anthropic models
357            ModelId::ClaudeOpus41,
358            ModelId::ClaudeSonnet45,
359            ModelId::ClaudeSonnet4,
360            // DeepSeek models
361            ModelId::DeepSeekChat,
362            ModelId::DeepSeekReasoner,
363            // xAI models
364            ModelId::XaiGrok2Latest,
365            ModelId::XaiGrok2,
366            ModelId::XaiGrok2Mini,
367            ModelId::XaiGrok2Reasoning,
368            ModelId::XaiGrok2Vision,
369            // OpenRouter models
370            ModelId::OpenRouterGrokCodeFast1,
371            ModelId::OpenRouterQwen3Coder,
372            ModelId::OpenRouterDeepSeekChatV31,
373            ModelId::OpenRouterOpenAIGPT5,
374            ModelId::OpenRouterAnthropicClaudeSonnet45,
375            ModelId::OpenRouterAnthropicClaudeSonnet4,
376        ]
377    }
378
379    /// Get all models for a specific provider
380    pub fn models_for_provider(provider: Provider) -> Vec<ModelId> {
381        Self::all_models()
382            .into_iter()
383            .filter(|model| model.provider() == provider)
384            .collect()
385    }
386
387    /// Get recommended fallback models in order of preference
388    pub fn fallback_models() -> Vec<ModelId> {
389        vec![
390            ModelId::Gemini25FlashPreview,
391            ModelId::Gemini25Pro,
392            ModelId::GPT5,
393            ModelId::ClaudeOpus41,
394            ModelId::ClaudeSonnet45,
395            ModelId::DeepSeekReasoner,
396            ModelId::XaiGrok2Latest,
397            ModelId::OpenRouterGrokCodeFast1,
398        ]
399    }
400
401    /// Get the default model for general use
402    pub fn default() -> Self {
403        ModelId::Gemini25FlashPreview
404    }
405
406    /// Get the default orchestrator model (more capable)
407    pub fn default_orchestrator() -> Self {
408        ModelId::Gemini25Pro
409    }
410
411    /// Get the default subagent model (fast and efficient)
412    pub fn default_subagent() -> Self {
413        ModelId::Gemini25FlashPreview
414    }
415
416    /// Get provider-specific defaults for orchestrator
417    pub fn default_orchestrator_for_provider(provider: Provider) -> Self {
418        match provider {
419            Provider::Gemini => ModelId::Gemini25Pro,
420            Provider::OpenAI => ModelId::GPT5,
421            Provider::Anthropic => ModelId::ClaudeOpus41,
422            Provider::DeepSeek => ModelId::DeepSeekReasoner,
423            Provider::XAI => ModelId::XaiGrok2Latest,
424            Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
425        }
426    }
427
428    /// Get provider-specific defaults for subagent
429    pub fn default_subagent_for_provider(provider: Provider) -> Self {
430        match provider {
431            Provider::Gemini => ModelId::Gemini25FlashPreview,
432            Provider::OpenAI => ModelId::GPT5Mini,
433            Provider::Anthropic => ModelId::ClaudeSonnet45,
434            Provider::DeepSeek => ModelId::DeepSeekChat,
435            Provider::XAI => ModelId::XaiGrok2Mini,
436            Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
437        }
438    }
439
440    /// Get provider-specific defaults for single agent
441    pub fn default_single_for_provider(provider: Provider) -> Self {
442        match provider {
443            Provider::Gemini => ModelId::Gemini25FlashPreview,
444            Provider::OpenAI => ModelId::GPT5,
445            Provider::Anthropic => ModelId::ClaudeOpus41,
446            Provider::DeepSeek => ModelId::DeepSeekReasoner,
447            Provider::XAI => ModelId::XaiGrok2Latest,
448            Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
449        }
450    }
451
452    /// Check if this is a "flash" variant (optimized for speed)
453    pub fn is_flash_variant(&self) -> bool {
454        matches!(
455            self,
456            ModelId::Gemini25FlashPreview | ModelId::Gemini25Flash | ModelId::Gemini25FlashLite
457        )
458    }
459
460    /// Check if this is a "pro" variant (optimized for capability)
461    pub fn is_pro_variant(&self) -> bool {
462        matches!(
463            self,
464            ModelId::Gemini25Pro
465                | ModelId::GPT5
466                | ModelId::ClaudeOpus41
467                | ModelId::DeepSeekReasoner
468                | ModelId::XaiGrok2Latest
469        )
470    }
471
472    /// Check if this is an optimized/efficient variant
473    pub fn is_efficient_variant(&self) -> bool {
474        matches!(
475            self,
476            ModelId::Gemini25FlashPreview
477                | ModelId::Gemini25Flash
478                | ModelId::Gemini25FlashLite
479                | ModelId::GPT5Mini
480                | ModelId::GPT5Nano
481                | ModelId::OpenRouterGrokCodeFast1
482                | ModelId::DeepSeekChat
483                | ModelId::XaiGrok2Mini
484        )
485    }
486
487    /// Check if this is a top-tier model
488    pub fn is_top_tier(&self) -> bool {
489        matches!(
490            self,
491            ModelId::Gemini25Pro
492                | ModelId::GPT5
493                | ModelId::ClaudeOpus41
494                | ModelId::ClaudeSonnet45
495                | ModelId::ClaudeSonnet4
496                | ModelId::DeepSeekReasoner
497                | ModelId::OpenRouterQwen3Coder
498                | ModelId::OpenRouterAnthropicClaudeSonnet45
499                | ModelId::XaiGrok2Latest
500                | ModelId::XaiGrok2Reasoning
501        )
502    }
503
504    /// Get the generation/version string for this model
505    pub fn generation(&self) -> &'static str {
506        match self {
507            // Gemini generations
508            ModelId::Gemini25FlashPreview
509            | ModelId::Gemini25Flash
510            | ModelId::Gemini25FlashLite
511            | ModelId::Gemini25Pro => "2.5",
512            // OpenAI generations
513            ModelId::GPT5 | ModelId::GPT5Mini | ModelId::GPT5Nano | ModelId::CodexMiniLatest => "5",
514            // Anthropic generations
515            ModelId::ClaudeSonnet45 => "4.5",
516            ModelId::ClaudeSonnet4 => "4",
517            ModelId::ClaudeOpus41 => "4.1",
518            // DeepSeek generations
519            ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => "V3.2-Exp",
520            // xAI generations
521            ModelId::XaiGrok2Latest
522            | ModelId::XaiGrok2
523            | ModelId::XaiGrok2Mini
524            | ModelId::XaiGrok2Reasoning
525            | ModelId::XaiGrok2Vision => "2",
526            // OpenRouter marketplace listings
527            ModelId::OpenRouterGrokCodeFast1 | ModelId::OpenRouterQwen3Coder => "marketplace",
528            // New OpenRouter models
529            ModelId::OpenRouterDeepSeekChatV31
530            | ModelId::OpenRouterOpenAIGPT5
531            | ModelId::OpenRouterAnthropicClaudeSonnet4 => "2025-08-07",
532            ModelId::OpenRouterAnthropicClaudeSonnet45 => "2025-09-29",
533        }
534    }
535}
536
537impl fmt::Display for ModelId {
538    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539        write!(f, "{}", self.as_str())
540    }
541}
542
543impl FromStr for ModelId {
544    type Err = ModelParseError;
545
546    fn from_str(s: &str) -> Result<Self, Self::Err> {
547        use crate::config::constants::models;
548        match s {
549            // Gemini models
550            s if s == models::GEMINI_2_5_FLASH_PREVIEW => Ok(ModelId::Gemini25FlashPreview),
551            s if s == models::GEMINI_2_5_FLASH => Ok(ModelId::Gemini25Flash),
552            s if s == models::GEMINI_2_5_FLASH_LITE => Ok(ModelId::Gemini25FlashLite),
553            s if s == models::GEMINI_2_5_PRO => Ok(ModelId::Gemini25Pro),
554            // OpenAI models
555            s if s == models::GPT_5 => Ok(ModelId::GPT5),
556            s if s == models::GPT_5_MINI => Ok(ModelId::GPT5Mini),
557            s if s == models::GPT_5_NANO => Ok(ModelId::GPT5Nano),
558            s if s == models::CODEX_MINI_LATEST => Ok(ModelId::CodexMiniLatest),
559            // Anthropic models
560            s if s == models::CLAUDE_OPUS_4_1_20250805 => Ok(ModelId::ClaudeOpus41),
561            s if s == models::CLAUDE_SONNET_4_5 => Ok(ModelId::ClaudeSonnet45),
562            s if s == models::CLAUDE_SONNET_4_20250514 => Ok(ModelId::ClaudeSonnet4),
563            // DeepSeek models
564            s if s == models::DEEPSEEK_CHAT => Ok(ModelId::DeepSeekChat),
565            s if s == models::DEEPSEEK_REASONER => Ok(ModelId::DeepSeekReasoner),
566            // xAI models
567            s if s == models::xai::GROK_2_LATEST => Ok(ModelId::XaiGrok2Latest),
568            s if s == models::xai::GROK_2 => Ok(ModelId::XaiGrok2),
569            s if s == models::xai::GROK_2_MINI => Ok(ModelId::XaiGrok2Mini),
570            s if s == models::xai::GROK_2_REASONING => Ok(ModelId::XaiGrok2Reasoning),
571            s if s == models::xai::GROK_2_VISION => Ok(ModelId::XaiGrok2Vision),
572            // OpenRouter models
573            s if s == models::OPENROUTER_X_AI_GROK_CODE_FAST_1 => {
574                Ok(ModelId::OpenRouterGrokCodeFast1)
575            }
576            s if s == models::OPENROUTER_QWEN3_CODER => Ok(ModelId::OpenRouterQwen3Coder),
577            s if s == models::OPENROUTER_DEEPSEEK_CHAT_V3_1 => {
578                Ok(ModelId::OpenRouterDeepSeekChatV31)
579            }
580            s if s == models::OPENROUTER_OPENAI_GPT_5 => Ok(ModelId::OpenRouterOpenAIGPT5),
581            s if s == models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4_5 => {
582                Ok(ModelId::OpenRouterAnthropicClaudeSonnet45)
583            }
584            s if s == models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4 => {
585                Ok(ModelId::OpenRouterAnthropicClaudeSonnet4)
586            }
587            _ => Err(ModelParseError::InvalidModel(s.to_string())),
588        }
589    }
590}
591
592/// Error type for model parsing failures
593#[derive(Debug, Clone, PartialEq)]
594pub enum ModelParseError {
595    InvalidModel(String),
596    InvalidProvider(String),
597}
598
599impl fmt::Display for ModelParseError {
600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601        match self {
602            ModelParseError::InvalidModel(model) => {
603                write!(
604                    f,
605                    "Invalid model identifier: '{}'. Supported models: {}",
606                    model,
607                    ModelId::all_models()
608                        .iter()
609                        .map(|m| m.as_str())
610                        .collect::<Vec<_>>()
611                        .join(", ")
612                )
613            }
614            ModelParseError::InvalidProvider(provider) => {
615                write!(
616                    f,
617                    "Invalid provider: '{}'. Supported providers: {}",
618                    provider,
619                    Provider::all_providers()
620                        .iter()
621                        .map(|p| p.to_string())
622                        .collect::<Vec<_>>()
623                        .join(", ")
624                )
625            }
626        }
627    }
628}
629
630impl std::error::Error for ModelParseError {}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635    use crate::config::constants::models;
636
637    #[test]
638    fn test_model_string_conversion() {
639        // Gemini models
640        assert_eq!(
641            ModelId::Gemini25FlashPreview.as_str(),
642            models::GEMINI_2_5_FLASH_PREVIEW
643        );
644        assert_eq!(ModelId::Gemini25Flash.as_str(), models::GEMINI_2_5_FLASH);
645        assert_eq!(
646            ModelId::Gemini25FlashLite.as_str(),
647            models::GEMINI_2_5_FLASH_LITE
648        );
649        assert_eq!(ModelId::Gemini25Pro.as_str(), models::GEMINI_2_5_PRO);
650        // OpenAI models
651        assert_eq!(ModelId::GPT5.as_str(), models::GPT_5);
652        assert_eq!(ModelId::GPT5Mini.as_str(), models::GPT_5_MINI);
653        assert_eq!(ModelId::GPT5Nano.as_str(), models::GPT_5_NANO);
654        assert_eq!(ModelId::CodexMiniLatest.as_str(), models::CODEX_MINI_LATEST);
655        // Anthropic models
656        assert_eq!(ModelId::ClaudeSonnet45.as_str(), models::CLAUDE_SONNET_4_5);
657        assert_eq!(
658            ModelId::ClaudeSonnet4.as_str(),
659            models::CLAUDE_SONNET_4_20250514
660        );
661        assert_eq!(
662            ModelId::ClaudeOpus41.as_str(),
663            models::CLAUDE_OPUS_4_1_20250805
664        );
665        // DeepSeek models
666        assert_eq!(ModelId::DeepSeekChat.as_str(), models::DEEPSEEK_CHAT);
667        assert_eq!(
668            ModelId::DeepSeekReasoner.as_str(),
669            models::DEEPSEEK_REASONER
670        );
671        // xAI models
672        assert_eq!(ModelId::XaiGrok2Latest.as_str(), models::xai::GROK_2_LATEST);
673        assert_eq!(ModelId::XaiGrok2.as_str(), models::xai::GROK_2);
674        assert_eq!(ModelId::XaiGrok2Mini.as_str(), models::xai::GROK_2_MINI);
675        assert_eq!(
676            ModelId::XaiGrok2Reasoning.as_str(),
677            models::xai::GROK_2_REASONING
678        );
679        assert_eq!(ModelId::XaiGrok2Vision.as_str(), models::xai::GROK_2_VISION);
680        // OpenRouter models
681        assert_eq!(
682            ModelId::OpenRouterGrokCodeFast1.as_str(),
683            models::OPENROUTER_X_AI_GROK_CODE_FAST_1
684        );
685        assert_eq!(
686            ModelId::OpenRouterQwen3Coder.as_str(),
687            models::OPENROUTER_QWEN3_CODER
688        );
689        assert_eq!(
690            ModelId::OpenRouterDeepSeekChatV31.as_str(),
691            models::OPENROUTER_DEEPSEEK_CHAT_V3_1
692        );
693        assert_eq!(
694            ModelId::OpenRouterOpenAIGPT5.as_str(),
695            models::OPENROUTER_OPENAI_GPT_5
696        );
697        assert_eq!(
698            ModelId::OpenRouterAnthropicClaudeSonnet45.as_str(),
699            models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4_5
700        );
701        assert_eq!(
702            ModelId::OpenRouterAnthropicClaudeSonnet4.as_str(),
703            models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4
704        );
705    }
706
707    #[test]
708    fn test_model_from_string() {
709        // Gemini models
710        assert_eq!(
711            models::GEMINI_2_5_FLASH_PREVIEW.parse::<ModelId>().unwrap(),
712            ModelId::Gemini25FlashPreview
713        );
714        assert_eq!(
715            models::GEMINI_2_5_FLASH.parse::<ModelId>().unwrap(),
716            ModelId::Gemini25Flash
717        );
718        assert_eq!(
719            models::GEMINI_2_5_FLASH_LITE.parse::<ModelId>().unwrap(),
720            ModelId::Gemini25FlashLite
721        );
722        assert_eq!(
723            models::GEMINI_2_5_PRO.parse::<ModelId>().unwrap(),
724            ModelId::Gemini25Pro
725        );
726        // OpenAI models
727        assert_eq!(models::GPT_5.parse::<ModelId>().unwrap(), ModelId::GPT5);
728        assert_eq!(
729            models::GPT_5_MINI.parse::<ModelId>().unwrap(),
730            ModelId::GPT5Mini
731        );
732        assert_eq!(
733            models::GPT_5_NANO.parse::<ModelId>().unwrap(),
734            ModelId::GPT5Nano
735        );
736        assert_eq!(
737            models::CODEX_MINI_LATEST.parse::<ModelId>().unwrap(),
738            ModelId::CodexMiniLatest
739        );
740        // Anthropic models
741        assert_eq!(
742            models::CLAUDE_SONNET_4_5.parse::<ModelId>().unwrap(),
743            ModelId::ClaudeSonnet45
744        );
745        assert_eq!(
746            models::CLAUDE_SONNET_4_20250514.parse::<ModelId>().unwrap(),
747            ModelId::ClaudeSonnet4
748        );
749        assert_eq!(
750            models::CLAUDE_OPUS_4_1_20250805.parse::<ModelId>().unwrap(),
751            ModelId::ClaudeOpus41
752        );
753        // DeepSeek models
754        assert_eq!(
755            models::DEEPSEEK_CHAT.parse::<ModelId>().unwrap(),
756            ModelId::DeepSeekChat
757        );
758        assert_eq!(
759            models::DEEPSEEK_REASONER.parse::<ModelId>().unwrap(),
760            ModelId::DeepSeekReasoner
761        );
762        // xAI models
763        assert_eq!(
764            models::xai::GROK_2_LATEST.parse::<ModelId>().unwrap(),
765            ModelId::XaiGrok2Latest
766        );
767        assert_eq!(
768            models::xai::GROK_2.parse::<ModelId>().unwrap(),
769            ModelId::XaiGrok2
770        );
771        assert_eq!(
772            models::xai::GROK_2_MINI.parse::<ModelId>().unwrap(),
773            ModelId::XaiGrok2Mini
774        );
775        assert_eq!(
776            models::xai::GROK_2_REASONING.parse::<ModelId>().unwrap(),
777            ModelId::XaiGrok2Reasoning
778        );
779        assert_eq!(
780            models::xai::GROK_2_VISION.parse::<ModelId>().unwrap(),
781            ModelId::XaiGrok2Vision
782        );
783        // OpenRouter models
784        assert_eq!(
785            models::OPENROUTER_X_AI_GROK_CODE_FAST_1
786                .parse::<ModelId>()
787                .unwrap(),
788            ModelId::OpenRouterGrokCodeFast1
789        );
790        assert_eq!(
791            models::OPENROUTER_QWEN3_CODER.parse::<ModelId>().unwrap(),
792            ModelId::OpenRouterQwen3Coder
793        );
794        assert_eq!(
795            models::OPENROUTER_DEEPSEEK_CHAT_V3_1
796                .parse::<ModelId>()
797                .unwrap(),
798            ModelId::OpenRouterDeepSeekChatV31
799        );
800        assert_eq!(
801            models::OPENROUTER_OPENAI_GPT_5.parse::<ModelId>().unwrap(),
802            ModelId::OpenRouterOpenAIGPT5
803        );
804        assert_eq!(
805            models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4_5
806                .parse::<ModelId>()
807                .unwrap(),
808            ModelId::OpenRouterAnthropicClaudeSonnet45
809        );
810        assert_eq!(
811            models::OPENROUTER_ANTHROPIC_CLAUDE_SONNET_4
812                .parse::<ModelId>()
813                .unwrap(),
814            ModelId::OpenRouterAnthropicClaudeSonnet4
815        );
816        // Invalid model
817        assert!("invalid-model".parse::<ModelId>().is_err());
818    }
819
820    #[test]
821    fn test_provider_parsing() {
822        assert_eq!("gemini".parse::<Provider>().unwrap(), Provider::Gemini);
823        assert_eq!("openai".parse::<Provider>().unwrap(), Provider::OpenAI);
824        assert_eq!(
825            "anthropic".parse::<Provider>().unwrap(),
826            Provider::Anthropic
827        );
828        assert_eq!("deepseek".parse::<Provider>().unwrap(), Provider::DeepSeek);
829        assert_eq!(
830            "openrouter".parse::<Provider>().unwrap(),
831            Provider::OpenRouter
832        );
833        assert_eq!("xai".parse::<Provider>().unwrap(), Provider::XAI);
834        assert!("invalid-provider".parse::<Provider>().is_err());
835    }
836
837    #[test]
838    fn test_model_providers() {
839        assert_eq!(ModelId::Gemini25FlashPreview.provider(), Provider::Gemini);
840        assert_eq!(ModelId::GPT5.provider(), Provider::OpenAI);
841        assert_eq!(ModelId::ClaudeSonnet45.provider(), Provider::Anthropic);
842        assert_eq!(ModelId::ClaudeSonnet4.provider(), Provider::Anthropic);
843        assert_eq!(ModelId::DeepSeekChat.provider(), Provider::DeepSeek);
844        assert_eq!(ModelId::XaiGrok2Latest.provider(), Provider::XAI);
845        assert_eq!(
846            ModelId::OpenRouterGrokCodeFast1.provider(),
847            Provider::OpenRouter
848        );
849        assert_eq!(
850            ModelId::OpenRouterAnthropicClaudeSonnet45.provider(),
851            Provider::OpenRouter
852        );
853    }
854
855    #[test]
856    fn test_provider_defaults() {
857        assert_eq!(
858            ModelId::default_orchestrator_for_provider(Provider::Gemini),
859            ModelId::Gemini25Pro
860        );
861        assert_eq!(
862            ModelId::default_orchestrator_for_provider(Provider::OpenAI),
863            ModelId::GPT5
864        );
865        assert_eq!(
866            ModelId::default_orchestrator_for_provider(Provider::Anthropic),
867            ModelId::ClaudeSonnet4
868        );
869        assert_eq!(
870            ModelId::default_orchestrator_for_provider(Provider::DeepSeek),
871            ModelId::DeepSeekReasoner
872        );
873        assert_eq!(
874            ModelId::default_orchestrator_for_provider(Provider::OpenRouter),
875            ModelId::OpenRouterGrokCodeFast1
876        );
877        assert_eq!(
878            ModelId::default_orchestrator_for_provider(Provider::XAI),
879            ModelId::XaiGrok2Latest
880        );
881
882        assert_eq!(
883            ModelId::default_subagent_for_provider(Provider::Gemini),
884            ModelId::Gemini25FlashPreview
885        );
886        assert_eq!(
887            ModelId::default_subagent_for_provider(Provider::OpenAI),
888            ModelId::GPT5Mini
889        );
890        assert_eq!(
891            ModelId::default_subagent_for_provider(Provider::Anthropic),
892            ModelId::ClaudeSonnet45
893        );
894        assert_eq!(
895            ModelId::default_subagent_for_provider(Provider::DeepSeek),
896            ModelId::DeepSeekChat
897        );
898        assert_eq!(
899            ModelId::default_subagent_for_provider(Provider::OpenRouter),
900            ModelId::OpenRouterGrokCodeFast1
901        );
902        assert_eq!(
903            ModelId::default_subagent_for_provider(Provider::XAI),
904            ModelId::XaiGrok2Mini
905        );
906
907        assert_eq!(
908            ModelId::default_single_for_provider(Provider::DeepSeek),
909            ModelId::DeepSeekReasoner
910        );
911    }
912
913    #[test]
914    fn test_model_defaults() {
915        assert_eq!(ModelId::default(), ModelId::Gemini25FlashPreview);
916        assert_eq!(ModelId::default_orchestrator(), ModelId::Gemini25Pro);
917        assert_eq!(ModelId::default_subagent(), ModelId::Gemini25FlashPreview);
918    }
919
920    #[test]
921    fn test_model_variants() {
922        // Flash variants
923        assert!(ModelId::Gemini25FlashPreview.is_flash_variant());
924        assert!(ModelId::Gemini25Flash.is_flash_variant());
925        assert!(ModelId::Gemini25FlashLite.is_flash_variant());
926        assert!(!ModelId::GPT5.is_flash_variant());
927
928        // Pro variants
929        assert!(ModelId::Gemini25Pro.is_pro_variant());
930        assert!(ModelId::GPT5.is_pro_variant());
931        assert!(ModelId::DeepSeekReasoner.is_pro_variant());
932        assert!(!ModelId::Gemini25FlashPreview.is_pro_variant());
933
934        // Efficient variants
935        assert!(ModelId::Gemini25FlashPreview.is_efficient_variant());
936        assert!(ModelId::Gemini25Flash.is_efficient_variant());
937        assert!(ModelId::Gemini25FlashLite.is_efficient_variant());
938        assert!(ModelId::GPT5Mini.is_efficient_variant());
939        assert!(ModelId::OpenRouterGrokCodeFast1.is_efficient_variant());
940        assert!(ModelId::XaiGrok2Mini.is_efficient_variant());
941        assert!(ModelId::DeepSeekChat.is_efficient_variant());
942        assert!(!ModelId::GPT5.is_efficient_variant());
943
944        // Top tier models
945        assert!(ModelId::Gemini25Pro.is_top_tier());
946        assert!(ModelId::GPT5.is_top_tier());
947        assert!(ModelId::ClaudeSonnet45.is_top_tier());
948        assert!(ModelId::ClaudeSonnet4.is_top_tier());
949        assert!(ModelId::OpenRouterQwen3Coder.is_top_tier());
950        assert!(ModelId::OpenRouterAnthropicClaudeSonnet45.is_top_tier());
951        assert!(ModelId::XaiGrok2Latest.is_top_tier());
952        assert!(ModelId::XaiGrok2Reasoning.is_top_tier());
953        assert!(ModelId::DeepSeekReasoner.is_top_tier());
954        assert!(!ModelId::Gemini25FlashPreview.is_top_tier());
955    }
956
957    #[test]
958    fn test_model_generation() {
959        // Gemini generations
960        assert_eq!(ModelId::Gemini25FlashPreview.generation(), "2.5");
961        assert_eq!(ModelId::Gemini25Flash.generation(), "2.5");
962        assert_eq!(ModelId::Gemini25FlashLite.generation(), "2.5");
963        assert_eq!(ModelId::Gemini25Pro.generation(), "2.5");
964
965        // OpenAI generations
966        assert_eq!(ModelId::GPT5.generation(), "5");
967        assert_eq!(ModelId::GPT5Mini.generation(), "5");
968        assert_eq!(ModelId::GPT5Nano.generation(), "5");
969        assert_eq!(ModelId::CodexMiniLatest.generation(), "5");
970
971        // Anthropic generations
972        assert_eq!(ModelId::ClaudeSonnet45.generation(), "4.5");
973        assert_eq!(ModelId::ClaudeSonnet4.generation(), "4");
974        assert_eq!(ModelId::ClaudeOpus41.generation(), "4.1");
975
976        // DeepSeek generations
977        assert_eq!(ModelId::DeepSeekChat.generation(), "V3.2-Exp");
978        assert_eq!(ModelId::DeepSeekReasoner.generation(), "V3.2-Exp");
979
980        // xAI generations
981        assert_eq!(ModelId::XaiGrok2Latest.generation(), "2");
982        assert_eq!(ModelId::XaiGrok2.generation(), "2");
983        assert_eq!(ModelId::XaiGrok2Mini.generation(), "2");
984        assert_eq!(ModelId::XaiGrok2Reasoning.generation(), "2");
985        assert_eq!(ModelId::XaiGrok2Vision.generation(), "2");
986
987        // OpenRouter marketplace entries
988        assert_eq!(ModelId::OpenRouterGrokCodeFast1.generation(), "marketplace");
989        assert_eq!(ModelId::OpenRouterQwen3Coder.generation(), "marketplace");
990
991        // New OpenRouter models
992        assert_eq!(
993            ModelId::OpenRouterDeepSeekChatV31.generation(),
994            "2025-08-07"
995        );
996        assert_eq!(ModelId::OpenRouterOpenAIGPT5.generation(), "2025-08-07");
997        assert_eq!(
998            ModelId::OpenRouterAnthropicClaudeSonnet4.generation(),
999            "2025-08-07"
1000        );
1001        assert_eq!(
1002            ModelId::OpenRouterAnthropicClaudeSonnet45.generation(),
1003            "2025-09-29"
1004        );
1005    }
1006
1007    #[test]
1008    fn test_models_for_provider() {
1009        let gemini_models = ModelId::models_for_provider(Provider::Gemini);
1010        assert!(gemini_models.contains(&ModelId::Gemini25Pro));
1011        assert!(!gemini_models.contains(&ModelId::GPT5));
1012
1013        let openai_models = ModelId::models_for_provider(Provider::OpenAI);
1014        assert!(openai_models.contains(&ModelId::GPT5));
1015        assert!(!openai_models.contains(&ModelId::Gemini25Pro));
1016
1017        let anthropic_models = ModelId::models_for_provider(Provider::Anthropic);
1018        assert!(anthropic_models.contains(&ModelId::ClaudeSonnet45));
1019        assert!(anthropic_models.contains(&ModelId::ClaudeSonnet4));
1020        assert!(!anthropic_models.contains(&ModelId::GPT5));
1021
1022        let deepseek_models = ModelId::models_for_provider(Provider::DeepSeek);
1023        assert!(deepseek_models.contains(&ModelId::DeepSeekChat));
1024        assert!(deepseek_models.contains(&ModelId::DeepSeekReasoner));
1025
1026        let openrouter_models = ModelId::models_for_provider(Provider::OpenRouter);
1027        assert!(openrouter_models.contains(&ModelId::OpenRouterGrokCodeFast1));
1028        assert!(openrouter_models.contains(&ModelId::OpenRouterQwen3Coder));
1029        assert!(openrouter_models.contains(&ModelId::OpenRouterDeepSeekChatV31));
1030        assert!(openrouter_models.contains(&ModelId::OpenRouterOpenAIGPT5));
1031        assert!(openrouter_models.contains(&ModelId::OpenRouterAnthropicClaudeSonnet45));
1032        assert!(openrouter_models.contains(&ModelId::OpenRouterAnthropicClaudeSonnet4));
1033
1034        let xai_models = ModelId::models_for_provider(Provider::XAI);
1035        assert!(xai_models.contains(&ModelId::XaiGrok2Latest));
1036        assert!(xai_models.contains(&ModelId::XaiGrok2));
1037        assert!(xai_models.contains(&ModelId::XaiGrok2Mini));
1038        assert!(xai_models.contains(&ModelId::XaiGrok2Reasoning));
1039        assert!(xai_models.contains(&ModelId::XaiGrok2Vision));
1040    }
1041
1042    #[test]
1043    fn test_fallback_models() {
1044        let fallbacks = ModelId::fallback_models();
1045        assert!(!fallbacks.is_empty());
1046        assert!(fallbacks.contains(&ModelId::Gemini25Pro));
1047        assert!(fallbacks.contains(&ModelId::GPT5));
1048        assert!(fallbacks.contains(&ModelId::ClaudeOpus41));
1049        assert!(fallbacks.contains(&ModelId::ClaudeSonnet45));
1050        assert!(fallbacks.contains(&ModelId::DeepSeekReasoner));
1051        assert!(fallbacks.contains(&ModelId::XaiGrok2Latest));
1052        assert!(fallbacks.contains(&ModelId::OpenRouterGrokCodeFast1));
1053    }
1054}