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