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 DeepSeek,
23 OpenRouter,
25 XAI,
27}
28
29impl Provider {
30 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 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#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
86pub enum ModelId {
87 Gemini25FlashPreview,
90 Gemini25Flash,
92 Gemini25FlashLite,
94 Gemini25Pro,
96
97 GPT5,
100 GPT5Mini,
102 GPT5Nano,
104 CodexMiniLatest,
106
107 ClaudeOpus41,
110 ClaudeSonnet45,
112 ClaudeSonnet4,
114
115 DeepSeekChat,
118 DeepSeekReasoner,
120
121 XaiGrok2Latest,
124 XaiGrok2,
126 XaiGrok2Mini,
128 XaiGrok2Reasoning,
130 XaiGrok2Vision,
132
133 OpenRouterGrokCodeFast1,
136 OpenRouterQwen3Coder,
138 OpenRouterDeepSeekChatV31,
140 OpenRouterOpenAIGPT5,
142 OpenRouterAnthropicClaudeSonnet45,
144 OpenRouterAnthropicClaudeSonnet4,
146}
147impl ModelId {
148 pub fn as_str(&self) -> &'static str {
151 use crate::config::constants::models;
152 match self {
153 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 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 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 ModelId::DeepSeekChat => models::DEEPSEEK_CHAT,
169 ModelId::DeepSeekReasoner => models::DEEPSEEK_REASONER,
170 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 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 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 pub fn display_name(&self) -> &'static str {
220 match self {
221 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 ModelId::GPT5 => "GPT-5",
228 ModelId::GPT5Mini => "GPT-5 Mini",
229 ModelId::GPT5Nano => "GPT-5 Nano",
230 ModelId::CodexMiniLatest => "Codex Mini Latest",
231 ModelId::ClaudeOpus41 => "Claude Opus 4.1",
233 ModelId::ClaudeSonnet45 => "Claude Sonnet 4.5",
234 ModelId::ClaudeSonnet4 => "Claude Sonnet 4",
235 ModelId::DeepSeekChat => "DeepSeek V3.2-Exp (Chat)",
237 ModelId::DeepSeekReasoner => "DeepSeek V3.2-Exp (Reasoner)",
238 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 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 pub fn description(&self) -> &'static str {
258 match self {
259 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 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 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 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 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 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 pub fn all_models() -> Vec<ModelId> {
314 vec![
315 ModelId::Gemini25FlashPreview,
317 ModelId::Gemini25Flash,
318 ModelId::Gemini25FlashLite,
319 ModelId::Gemini25Pro,
320 ModelId::GPT5,
322 ModelId::GPT5Mini,
323 ModelId::GPT5Nano,
324 ModelId::CodexMiniLatest,
325 ModelId::ClaudeOpus41,
327 ModelId::ClaudeSonnet45,
328 ModelId::ClaudeSonnet4,
329 ModelId::DeepSeekChat,
331 ModelId::DeepSeekReasoner,
332 ModelId::XaiGrok2Latest,
334 ModelId::XaiGrok2,
335 ModelId::XaiGrok2Mini,
336 ModelId::XaiGrok2Reasoning,
337 ModelId::XaiGrok2Vision,
338 ModelId::OpenRouterGrokCodeFast1,
340 ModelId::OpenRouterQwen3Coder,
341 ModelId::OpenRouterDeepSeekChatV31,
342 ModelId::OpenRouterOpenAIGPT5,
343 ModelId::OpenRouterAnthropicClaudeSonnet45,
344 ModelId::OpenRouterAnthropicClaudeSonnet4,
345 ]
346 }
347
348 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 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 pub fn default() -> Self {
372 ModelId::Gemini25FlashPreview
373 }
374
375 pub fn default_orchestrator() -> Self {
377 ModelId::Gemini25Pro
378 }
379
380 pub fn default_subagent() -> Self {
382 ModelId::Gemini25FlashPreview
383 }
384
385 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 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 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 pub fn is_flash_variant(&self) -> bool {
423 matches!(
424 self,
425 ModelId::Gemini25FlashPreview | ModelId::Gemini25Flash | ModelId::Gemini25FlashLite
426 )
427 }
428
429 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 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 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 pub fn generation(&self) -> &'static str {
475 match self {
476 ModelId::Gemini25FlashPreview
478 | ModelId::Gemini25Flash
479 | ModelId::Gemini25FlashLite
480 | ModelId::Gemini25Pro => "2.5",
481 ModelId::GPT5 | ModelId::GPT5Mini | ModelId::GPT5Nano | ModelId::CodexMiniLatest => "5",
483 ModelId::ClaudeSonnet45 => "4.5",
485 ModelId::ClaudeSonnet4 => "4",
486 ModelId::ClaudeOpus41 => "4.1",
487 ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => "V3.2-Exp",
489 ModelId::XaiGrok2Latest
491 | ModelId::XaiGrok2
492 | ModelId::XaiGrok2Mini
493 | ModelId::XaiGrok2Reasoning
494 | ModelId::XaiGrok2Vision => "2",
495 ModelId::OpenRouterGrokCodeFast1 | ModelId::OpenRouterQwen3Coder => "marketplace",
497 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 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 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 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 s if s == models::DEEPSEEK_CHAT => Ok(ModelId::DeepSeekChat),
534 s if s == models::DEEPSEEK_REASONER => Ok(ModelId::DeepSeekReasoner),
535 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 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#[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 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 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 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 assert_eq!(ModelId::DeepSeekChat.as_str(), models::DEEPSEEK_CHAT);
636 assert_eq!(
637 ModelId::DeepSeekReasoner.as_str(),
638 models::DEEPSEEK_REASONER
639 );
640 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(ModelId::ClaudeSonnet45.generation(), "4.5");
942 assert_eq!(ModelId::ClaudeSonnet4.generation(), "4");
943 assert_eq!(ModelId::ClaudeOpus41.generation(), "4.1");
944
945 assert_eq!(ModelId::DeepSeekChat.generation(), "V3.2-Exp");
947 assert_eq!(ModelId::DeepSeekReasoner.generation(), "V3.2-Exp");
948
949 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 assert_eq!(ModelId::OpenRouterGrokCodeFast1.generation(), "marketplace");
958 assert_eq!(ModelId::OpenRouterQwen3Coder.generation(), "marketplace");
959
960 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}