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