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