1use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13pub enum Provider {
14 #[default]
16 Gemini,
17 OpenAI,
19 Anthropic,
21 OpenRouter,
23 XAI,
25}
26
27impl Provider {
28 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 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#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
80pub enum ModelId {
81 Gemini25FlashPreview,
84 Gemini25Flash,
86 Gemini25FlashLite,
88 Gemini25Pro,
90
91 GPT5,
94 GPT5Mini,
96 GPT5Nano,
98 CodexMiniLatest,
100
101 ClaudeOpus41,
104 ClaudeSonnet4,
106
107 XaiGrok2Latest,
110 XaiGrok2,
112 XaiGrok2Mini,
114 XaiGrok2Reasoning,
116 XaiGrok2Vision,
118
119 OpenRouterGrokCodeFast1,
122 OpenRouterQwen3Coder,
124 OpenRouterDeepSeekChatV31,
126 OpenRouterOpenAIGPT5,
128 OpenRouterAnthropicClaudeSonnet4,
130}
131impl ModelId {
132 pub fn as_str(&self) -> &'static str {
135 use crate::config::constants::models;
136 match self {
137 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 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 ModelId::ClaudeOpus41 => models::CLAUDE_OPUS_4_1_20250805,
149 ModelId::ClaudeSonnet4 => models::CLAUDE_SONNET_4_20250514,
150 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 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 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 pub fn display_name(&self) -> &'static str {
193 match self {
194 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 ModelId::GPT5 => "GPT-5",
201 ModelId::GPT5Mini => "GPT-5 Mini",
202 ModelId::GPT5Nano => "GPT-5 Nano",
203 ModelId::CodexMiniLatest => "Codex Mini Latest",
204 ModelId::ClaudeOpus41 => "Claude Opus 4.1",
206 ModelId::ClaudeSonnet4 => "Claude Sonnet 4",
207 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 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 pub fn description(&self) -> &'static str {
224 match self {
225 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 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 ModelId::ClaudeOpus41 => "Latest most capable Anthropic model with advanced reasoning",
243 ModelId::ClaudeSonnet4 => "Latest balanced Anthropic model for general tasks",
244 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 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 pub fn all_models() -> Vec<ModelId> {
267 vec![
268 ModelId::Gemini25FlashPreview,
270 ModelId::Gemini25Flash,
271 ModelId::Gemini25FlashLite,
272 ModelId::Gemini25Pro,
273 ModelId::GPT5,
275 ModelId::GPT5Mini,
276 ModelId::GPT5Nano,
277 ModelId::CodexMiniLatest,
278 ModelId::ClaudeOpus41,
280 ModelId::ClaudeSonnet4,
281 ModelId::XaiGrok2Latest,
283 ModelId::XaiGrok2,
284 ModelId::XaiGrok2Mini,
285 ModelId::XaiGrok2Reasoning,
286 ModelId::XaiGrok2Vision,
287 ModelId::OpenRouterGrokCodeFast1,
289 ModelId::OpenRouterQwen3Coder,
290 ModelId::OpenRouterDeepSeekChatV31,
291 ModelId::OpenRouterOpenAIGPT5,
292 ModelId::OpenRouterAnthropicClaudeSonnet4,
293 ]
294 }
295
296 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 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 pub fn default() -> Self {
318 ModelId::Gemini25FlashPreview
319 }
320
321 pub fn default_orchestrator() -> Self {
323 ModelId::Gemini25Pro
324 }
325
326 pub fn default_subagent() -> Self {
328 ModelId::Gemini25FlashPreview
329 }
330
331 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 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 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 pub fn is_flash_variant(&self) -> bool {
366 matches!(
367 self,
368 ModelId::Gemini25FlashPreview | ModelId::Gemini25Flash | ModelId::Gemini25FlashLite
369 )
370 }
371
372 pub fn is_pro_variant(&self) -> bool {
374 matches!(
375 self,
376 ModelId::Gemini25Pro | ModelId::GPT5 | ModelId::ClaudeOpus41 | ModelId::XaiGrok2Latest
377 )
378 }
379
380 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 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 pub fn generation(&self) -> &'static str {
410 match self {
411 ModelId::Gemini25FlashPreview
413 | ModelId::Gemini25Flash
414 | ModelId::Gemini25FlashLite
415 | ModelId::Gemini25Pro => "2.5",
416 ModelId::GPT5 | ModelId::GPT5Mini | ModelId::GPT5Nano | ModelId::CodexMiniLatest => "5",
418 ModelId::ClaudeSonnet4 => "4",
420 ModelId::ClaudeOpus41 => "4.1",
421 ModelId::XaiGrok2Latest
423 | ModelId::XaiGrok2
424 | ModelId::XaiGrok2Mini
425 | ModelId::XaiGrok2Reasoning
426 | ModelId::XaiGrok2Vision => "2",
427 ModelId::OpenRouterGrokCodeFast1 | ModelId::OpenRouterQwen3Coder => "marketplace",
429 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(ModelId::Gemini25Pro.is_pro_variant());
773 assert!(ModelId::GPT5.is_pro_variant());
774 assert!(!ModelId::Gemini25FlashPreview.is_pro_variant());
775
776 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 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 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 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 assert_eq!(ModelId::ClaudeSonnet4.generation(), "4");
811 assert_eq!(ModelId::ClaudeOpus41.generation(), "4.1");
812
813 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 assert_eq!(ModelId::OpenRouterGrokCodeFast1.generation(), "marketplace");
822 assert_eq!(ModelId::OpenRouterQwen3Coder.generation(), "marketplace");
823
824 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}