1use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Clone, Copy)]
12pub struct OpenRouterMetadata {
13 id: &'static str,
14 vendor: &'static str,
15 display: &'static str,
16 description: &'static str,
17 efficient: bool,
18 top_tier: bool,
19 generation: &'static str,
20 reasoning: bool,
21 tool_call: bool,
22}
23
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
27pub enum Provider {
28 #[default]
30 Gemini,
31 OpenAI,
33 Anthropic,
35 DeepSeek,
37 OpenRouter,
39 Ollama,
41 LmStudio,
43 Moonshot,
45 XAI,
47 ZAI,
49}
50
51impl Provider {
52 pub fn default_api_key_env(&self) -> &'static str {
54 match self {
55 Provider::Gemini => "GEMINI_API_KEY",
56 Provider::OpenAI => "OPENAI_API_KEY",
57 Provider::Anthropic => "ANTHROPIC_API_KEY",
58 Provider::DeepSeek => "DEEPSEEK_API_KEY",
59 Provider::OpenRouter => "OPENROUTER_API_KEY",
60 Provider::Ollama => "OLLAMA_API_KEY",
61 Provider::LmStudio => "LMSTUDIO_API_KEY",
62 Provider::Moonshot => "MOONSHOT_API_KEY",
63 Provider::XAI => "XAI_API_KEY",
64 Provider::ZAI => "ZAI_API_KEY",
65 }
66 }
67
68 pub fn all_providers() -> Vec<Provider> {
70 vec![
71 Provider::OpenAI,
72 Provider::Anthropic,
73 Provider::Gemini,
74 Provider::DeepSeek,
75 Provider::OpenRouter,
76 Provider::Ollama,
77 Provider::LmStudio,
78 Provider::Moonshot,
79 Provider::XAI,
80 Provider::ZAI,
81 ]
82 }
83
84 pub fn label(&self) -> &'static str {
86 match self {
87 Provider::Gemini => "Gemini",
88 Provider::OpenAI => "OpenAI",
89 Provider::Anthropic => "Anthropic",
90 Provider::DeepSeek => "DeepSeek",
91 Provider::OpenRouter => "OpenRouter",
92 Provider::Ollama => "Ollama",
93 Provider::LmStudio => "LM Studio",
94 Provider::Moonshot => "Moonshot",
95 Provider::XAI => "xAI",
96 Provider::ZAI => "Z.AI",
97 }
98 }
99
100 pub fn supports_reasoning_effort(&self, model: &str) -> bool {
102 use crate::constants::models;
103
104 match self {
105 Provider::Gemini => models::google::REASONING_MODELS.contains(&model),
106 Provider::OpenAI => models::openai::REASONING_MODELS.contains(&model),
107 Provider::Anthropic => models::anthropic::REASONING_MODELS.contains(&model),
108 Provider::DeepSeek => model == models::deepseek::DEEPSEEK_REASONER,
109 Provider::OpenRouter => {
110 if let Ok(model_id) = ModelId::from_str(model) {
111 return model_id.is_reasoning_variant();
112 }
113 models::openrouter::REASONING_MODELS.contains(&model)
114 }
115 Provider::Ollama => false,
116 Provider::LmStudio => false,
117 Provider::Moonshot => model == models::moonshot::KIMI_K2_THINKING,
118 Provider::XAI => model == models::xai::GROK_4 || model == models::xai::GROK_4_CODE,
119 Provider::ZAI => model == models::zai::GLM_4_6,
120 }
121 }
122}
123
124impl fmt::Display for Provider {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 Provider::Gemini => write!(f, "gemini"),
128 Provider::OpenAI => write!(f, "openai"),
129 Provider::Anthropic => write!(f, "anthropic"),
130 Provider::DeepSeek => write!(f, "deepseek"),
131 Provider::OpenRouter => write!(f, "openrouter"),
132 Provider::Ollama => write!(f, "ollama"),
133 Provider::LmStudio => write!(f, "lmstudio"),
134 Provider::Moonshot => write!(f, "moonshot"),
135 Provider::XAI => write!(f, "xai"),
136 Provider::ZAI => write!(f, "zai"),
137 }
138 }
139}
140
141impl FromStr for Provider {
142 type Err = ModelParseError;
143
144 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 match s.to_lowercase().as_str() {
146 "gemini" => Ok(Provider::Gemini),
147 "openai" => Ok(Provider::OpenAI),
148 "anthropic" => Ok(Provider::Anthropic),
149 "deepseek" => Ok(Provider::DeepSeek),
150 "openrouter" => Ok(Provider::OpenRouter),
151 "ollama" => Ok(Provider::Ollama),
152 "lmstudio" => Ok(Provider::LmStudio),
153 "moonshot" => Ok(Provider::Moonshot),
154 "xai" => Ok(Provider::XAI),
155 "zai" => Ok(Provider::ZAI),
156 _ => Err(ModelParseError::InvalidProvider(s.to_string())),
157 }
158 }
159}
160
161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
164pub enum ModelId {
165 #[default]
168 Gemini25FlashPreview,
169 Gemini25Flash,
171 Gemini25FlashLite,
173 Gemini25Pro,
175 Gemini3ProPreview,
177
178 GPT5,
181 GPT5Codex,
183 GPT5Mini,
185 GPT5Nano,
187 CodexMiniLatest,
189 OpenAIGptOss20b,
191 OpenAIGptOss120b,
193
194 ClaudeOpus41,
197 ClaudeSonnet45,
199 ClaudeHaiku45,
201 ClaudeSonnet4,
203
204 DeepSeekChat,
207 DeepSeekReasoner,
209
210 XaiGrok4,
213 XaiGrok4Mini,
215 XaiGrok4Code,
217 XaiGrok4CodeLatest,
219 XaiGrok4Vision,
221
222 ZaiGlm46,
225 ZaiGlm45,
227 ZaiGlm45Air,
229 ZaiGlm45X,
231 ZaiGlm45Airx,
233 ZaiGlm45Flash,
235 ZaiGlm432b0414128k,
237
238 MoonshotKimiK2TurboPreview,
241 MoonshotKimiK2Thinking,
243 MoonshotKimiK20905Preview,
245 MoonshotKimiK20711Preview,
247 MoonshotKimiLatest,
249 MoonshotKimiLatest8k,
251 MoonshotKimiLatest32k,
253 MoonshotKimiLatest128k,
255
256 OllamaGptOss20b,
259 OllamaGptOss20bCloud,
261 OllamaGptOss120bCloud,
263 OllamaQwen317b,
265 OllamaDeepseekV31671bCloud,
267
268 OllamaKimiK21tCloud,
270 OllamaQwen3Coder480bCloud,
272 OllamaGlm46Cloud,
274 OllamaMinimaxM2Cloud,
276
277 LmStudioMetaLlama38BInstruct,
280 LmStudioMetaLlama318BInstruct,
282 LmStudioQwen257BInstruct,
284 LmStudioGemma22BIt,
286 LmStudioGemma29BIt,
288 LmStudioPhi31Mini4kInstruct,
290
291 OpenRouterGrokCodeFast1,
294 OpenRouterGrok4Fast,
296 OpenRouterGrok4,
298 OpenRouterZaiGlm46,
300 OpenRouterMoonshotaiKimiK20905,
302 OpenRouterMoonshotaiKimiK2Thinking,
304 OpenRouterMoonshotaiKimiK2Free,
306 OpenRouterQwen3Max,
308 OpenRouterQwen3235bA22b,
310 OpenRouterQwen3235bA22bFree,
312 OpenRouterQwen3235bA22b2507,
314 OpenRouterQwen3235bA22bThinking2507,
316 OpenRouterQwen332b,
318 OpenRouterQwen330bA3b,
320 OpenRouterQwen330bA3bFree,
322 OpenRouterQwen330bA3bInstruct2507,
324 OpenRouterQwen330bA3bThinking2507,
326 OpenRouterQwen314b,
328 OpenRouterQwen314bFree,
330 OpenRouterQwen38b,
332 OpenRouterQwen38bFree,
334 OpenRouterQwen34bFree,
336 OpenRouterQwen3Next80bA3bInstruct,
338 OpenRouterQwen3Next80bA3bThinking,
340 OpenRouterQwen3Coder,
342 OpenRouterQwen3CoderFree,
344 OpenRouterQwen3CoderPlus,
346 OpenRouterQwen3CoderFlash,
348 OpenRouterQwen3Coder30bA3bInstruct,
350 OpenRouterDeepSeekV32Exp,
352 OpenRouterDeepSeekChatV31,
354 OpenRouterDeepSeekR1,
356 OpenRouterDeepSeekChatV31Free,
358 OpenRouterNvidiaNemotronNano9bV2Free,
360 OpenRouterOpenAIGptOss120b,
362 OpenRouterOpenAIGptOss20b,
364 OpenRouterOpenAIGptOss20bFree,
366 OpenRouterOpenAIGpt5,
368 OpenRouterOpenAIGpt5Codex,
370 OpenRouterOpenAIGpt5Chat,
372 OpenRouterOpenAIGpt4oSearchPreview,
374 OpenRouterOpenAIGpt4oMiniSearchPreview,
376 OpenRouterOpenAIChatgpt4oLatest,
378 OpenRouterAnthropicClaudeSonnet45,
380 OpenRouterAnthropicClaudeHaiku45,
382 OpenRouterAnthropicClaudeOpus41,
384 OpenRouterMinimaxM2Free,
386}
387
388#[cfg(not(docsrs))]
389pub mod openrouter_generated {
390 include!(concat!(env!("OUT_DIR"), "/openrouter_metadata.rs"));
391}
392
393#[cfg(docsrs)]
394pub mod openrouter_generated {
395 #[derive(Clone, Copy)]
396 pub struct Entry {
397 pub variant: super::ModelId,
398 pub id: &'static str,
399 pub vendor: &'static str,
400 pub display: &'static str,
401 pub description: &'static str,
402 pub efficient: bool,
403 pub top_tier: bool,
404 pub generation: &'static str,
405 pub reasoning: bool,
406 pub tool_call: bool,
407 }
408
409 pub const ENTRIES: &[Entry] = &[];
410
411 #[derive(Clone, Copy)]
412 pub struct VendorModels {
413 pub vendor: &'static str,
414 pub models: &'static [super::ModelId],
415 }
416
417 pub const VENDOR_MODELS: &[VendorModels] = &[];
418
419 pub fn metadata_for(_model: super::ModelId) -> Option<super::OpenRouterMetadata> {
420 None
421 }
422
423 pub fn parse_model(_value: &str) -> Option<super::ModelId> {
424 None
425 }
426
427 pub fn vendor_groups() -> &'static [VendorModels] {
428 VENDOR_MODELS
429 }
430}
431
432impl ModelId {
433 fn openrouter_metadata(&self) -> Option<OpenRouterMetadata> {
434 #[cfg(not(docsrs))]
435 {
436 openrouter_generated::metadata_for(*self)
437 }
438 #[cfg(docsrs)]
439 {
440 None
441 }
442 }
443
444 fn parse_openrouter_model(value: &str) -> Option<Self> {
445 #[cfg(not(docsrs))]
446 {
447 openrouter_generated::parse_model(value)
448 }
449 #[cfg(docsrs)]
450 {
451 None
452 }
453 }
454
455 fn openrouter_vendor_groups() -> Vec<(&'static str, &'static [Self])> {
456 #[cfg(not(docsrs))]
457 {
458 openrouter_generated::vendor_groups()
459 .iter()
460 .map(|group| (group.vendor, group.models))
461 .collect()
462 }
463 #[cfg(docsrs)]
464 {
465 Vec::new()
466 }
467 }
468
469 fn openrouter_models() -> Vec<Self> {
470 Self::openrouter_vendor_groups()
471 .into_iter()
472 .flat_map(|(_, models)| models.iter().copied())
473 .collect()
474 }
475
476 pub fn as_str(&self) -> &'static str {
479 use crate::constants::models;
480 if let Some(meta) = self.openrouter_metadata() {
481 return meta.id;
482 }
483 match self {
484 ModelId::Gemini25FlashPreview => models::GEMINI_2_5_FLASH_PREVIEW,
486 ModelId::Gemini25Flash => models::GEMINI_2_5_FLASH,
487 ModelId::Gemini25FlashLite => models::GEMINI_2_5_FLASH_LITE,
488 ModelId::Gemini25Pro => models::GEMINI_2_5_PRO,
489 ModelId::Gemini3ProPreview => models::GEMINI_3_PRO_PREVIEW,
490 ModelId::GPT5 => models::GPT_5,
492 ModelId::GPT5Codex => models::GPT_5_CODEX,
493 ModelId::GPT5Mini => models::GPT_5_MINI,
494 ModelId::GPT5Nano => models::GPT_5_NANO,
495 ModelId::CodexMiniLatest => models::CODEX_MINI_LATEST,
496 ModelId::ClaudeOpus41 => models::CLAUDE_OPUS_4_1,
498 ModelId::ClaudeSonnet45 => models::CLAUDE_SONNET_4_5,
499 ModelId::ClaudeHaiku45 => models::CLAUDE_HAIKU_4_5,
500 ModelId::ClaudeSonnet4 => models::CLAUDE_SONNET_4_5_20250929,
501 ModelId::DeepSeekChat => models::DEEPSEEK_CHAT,
503 ModelId::DeepSeekReasoner => models::DEEPSEEK_REASONER,
504 ModelId::XaiGrok4 => models::xai::GROK_4,
506 ModelId::XaiGrok4Mini => models::xai::GROK_4_MINI,
507 ModelId::XaiGrok4Code => models::xai::GROK_4_CODE,
508 ModelId::XaiGrok4CodeLatest => models::xai::GROK_4_CODE_LATEST,
509 ModelId::XaiGrok4Vision => models::xai::GROK_4_VISION,
510 ModelId::ZaiGlm46 => models::zai::GLM_4_6,
512 ModelId::ZaiGlm45 => models::zai::GLM_4_5,
513 ModelId::ZaiGlm45Air => models::zai::GLM_4_5_AIR,
514 ModelId::ZaiGlm45X => models::zai::GLM_4_5_X,
515 ModelId::ZaiGlm45Airx => models::zai::GLM_4_5_AIRX,
516 ModelId::ZaiGlm45Flash => models::zai::GLM_4_5_FLASH,
517 ModelId::ZaiGlm432b0414128k => models::zai::GLM_4_32B_0414_128K,
518 ModelId::MoonshotKimiK2TurboPreview => models::MOONSHOT_KIMI_K2_TURBO_PREVIEW,
520 ModelId::MoonshotKimiK2Thinking => models::MOONSHOT_KIMI_K2_THINKING,
521 ModelId::MoonshotKimiK20905Preview => models::MOONSHOT_KIMI_K2_0905_PREVIEW,
522 ModelId::MoonshotKimiK20711Preview => models::MOONSHOT_KIMI_K2_0711_PREVIEW,
523 ModelId::MoonshotKimiLatest => models::MOONSHOT_KIMI_LATEST,
524 ModelId::MoonshotKimiLatest8k => models::MOONSHOT_KIMI_LATEST_8K,
525 ModelId::MoonshotKimiLatest32k => models::MOONSHOT_KIMI_LATEST_32K,
526 ModelId::MoonshotKimiLatest128k => models::MOONSHOT_KIMI_LATEST_128K,
527 ModelId::OllamaGptOss20b => models::ollama::GPT_OSS_20B,
529 ModelId::OllamaGptOss20bCloud => models::ollama::GPT_OSS_20B_CLOUD,
530 ModelId::OllamaGptOss120bCloud => models::ollama::GPT_OSS_120B_CLOUD,
531 ModelId::OllamaQwen317b => models::ollama::QWEN3_1_7B,
532 ModelId::OllamaDeepseekV31671bCloud => models::ollama::DEEPSEEK_V31_671B_CLOUD,
533
534 ModelId::OllamaKimiK21tCloud => models::ollama::KIMI_K2_1T_CLOUD,
535 ModelId::OllamaQwen3Coder480bCloud => models::ollama::QWEN3_CODER_480B_CLOUD,
536 ModelId::OllamaGlm46Cloud => models::ollama::GLM_46_CLOUD,
537 ModelId::OllamaMinimaxM2Cloud => models::ollama::MINIMAX_M2_CLOUD,
538 ModelId::LmStudioMetaLlama38BInstruct => models::lmstudio::META_LLAMA_3_8B_INSTRUCT,
540 ModelId::LmStudioMetaLlama318BInstruct => models::lmstudio::META_LLAMA_31_8B_INSTRUCT,
541 ModelId::LmStudioQwen257BInstruct => models::lmstudio::QWEN25_7B_INSTRUCT,
542 ModelId::LmStudioGemma22BIt => models::lmstudio::GEMMA_2_2B_IT,
543 ModelId::LmStudioGemma29BIt => models::lmstudio::GEMMA_2_9B_IT,
544 ModelId::LmStudioPhi31Mini4kInstruct => models::lmstudio::PHI_31_MINI_4K_INSTRUCT,
545 _ => unreachable!(),
547 }
548 }
549
550 pub fn provider(&self) -> Provider {
552 if self.openrouter_metadata().is_some() {
553 return Provider::OpenRouter;
554 }
555 match self {
556 ModelId::Gemini25FlashPreview
557 | ModelId::Gemini25Flash
558 | ModelId::Gemini25FlashLite
559 | ModelId::Gemini25Pro
560 | ModelId::Gemini3ProPreview => Provider::Gemini,
561 ModelId::GPT5
562 | ModelId::GPT5Codex
563 | ModelId::GPT5Mini
564 | ModelId::GPT5Nano
565 | ModelId::CodexMiniLatest
566 | ModelId::OpenAIGptOss20b
567 | ModelId::OpenAIGptOss120b => Provider::OpenAI,
568 ModelId::ClaudeOpus41
569 | ModelId::ClaudeSonnet45
570 | ModelId::ClaudeHaiku45
571 | ModelId::ClaudeSonnet4 => Provider::Anthropic,
572 ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => Provider::DeepSeek,
573 ModelId::XaiGrok4
574 | ModelId::XaiGrok4Mini
575 | ModelId::XaiGrok4Code
576 | ModelId::XaiGrok4CodeLatest
577 | ModelId::XaiGrok4Vision => Provider::XAI,
578 ModelId::ZaiGlm46
579 | ModelId::ZaiGlm45
580 | ModelId::ZaiGlm45Air
581 | ModelId::ZaiGlm45X
582 | ModelId::ZaiGlm45Airx
583 | ModelId::ZaiGlm45Flash
584 | ModelId::ZaiGlm432b0414128k => Provider::ZAI,
585 ModelId::MoonshotKimiK2TurboPreview
586 | ModelId::MoonshotKimiK2Thinking
587 | ModelId::MoonshotKimiK20905Preview
588 | ModelId::MoonshotKimiK20711Preview
589 | ModelId::MoonshotKimiLatest
590 | ModelId::MoonshotKimiLatest8k
591 | ModelId::MoonshotKimiLatest32k
592 | ModelId::MoonshotKimiLatest128k => Provider::Moonshot,
593 ModelId::OllamaGptOss20b
594 | ModelId::OllamaGptOss20bCloud
595 | ModelId::OllamaGptOss120bCloud
596 | ModelId::OllamaQwen317b
597 | ModelId::OllamaDeepseekV31671bCloud
598 | ModelId::OllamaKimiK21tCloud
599 | ModelId::OllamaQwen3Coder480bCloud
600 | ModelId::OllamaGlm46Cloud
601 | ModelId::OllamaMinimaxM2Cloud => Provider::Ollama,
602 ModelId::LmStudioMetaLlama38BInstruct
603 | ModelId::LmStudioMetaLlama318BInstruct
604 | ModelId::LmStudioQwen257BInstruct
605 | ModelId::LmStudioGemma22BIt
606 | ModelId::LmStudioGemma29BIt
607 | ModelId::LmStudioPhi31Mini4kInstruct => Provider::LmStudio,
608 _ => unreachable!(),
609 }
610 }
611
612 pub fn supports_reasoning_effort(&self) -> bool {
614 self.provider().supports_reasoning_effort(self.as_str())
615 }
616
617 pub fn display_name(&self) -> &'static str {
619 if let Some(meta) = self.openrouter_metadata() {
620 return meta.display;
621 }
622 match self {
623 ModelId::Gemini25FlashPreview => "Gemini 2.5 Flash Preview",
625 ModelId::Gemini25Flash => "Gemini 2.5 Flash",
626 ModelId::Gemini25FlashLite => "Gemini 2.5 Flash Lite",
627 ModelId::Gemini25Pro => "Gemini 2.5 Pro",
628 ModelId::Gemini3ProPreview => "Gemini 3 Pro Preview",
629 ModelId::GPT5 => "GPT-5",
631 ModelId::GPT5Codex => "GPT-5 Codex",
632 ModelId::GPT5Mini => "GPT-5 Mini",
633 ModelId::GPT5Nano => "GPT-5 Nano",
634 ModelId::CodexMiniLatest => "Codex Mini Latest",
635 ModelId::ClaudeOpus41 => "Claude Opus 4.1",
637 ModelId::ClaudeSonnet45 => "Claude Sonnet 4.5",
638 ModelId::ClaudeHaiku45 => "Claude Haiku 4.5",
639 ModelId::ClaudeSonnet4 => "Claude Sonnet 4",
640 ModelId::DeepSeekChat => "DeepSeek V3.2-Exp (Chat)",
642 ModelId::DeepSeekReasoner => "DeepSeek V3.2-Exp (Reasoner)",
643 ModelId::XaiGrok4 => "Grok-4",
645 ModelId::XaiGrok4Mini => "Grok-4 Mini",
646 ModelId::XaiGrok4Code => "Grok-4 Code",
647 ModelId::XaiGrok4CodeLatest => "Grok-4 Code Latest",
648 ModelId::XaiGrok4Vision => "Grok-4 Vision",
649 ModelId::ZaiGlm46 => "GLM 4.6",
651 ModelId::ZaiGlm45 => "GLM 4.5",
652 ModelId::ZaiGlm45Air => "GLM 4.5 Air",
653 ModelId::ZaiGlm45X => "GLM 4.5 X",
654 ModelId::ZaiGlm45Airx => "GLM 4.5 AirX",
655 ModelId::ZaiGlm45Flash => "GLM 4.5 Flash",
656 ModelId::ZaiGlm432b0414128k => "GLM 4 32B 0414 128K",
657 ModelId::MoonshotKimiK2TurboPreview => "Kimi K2 Turbo Preview",
659 ModelId::MoonshotKimiK2Thinking => "Kimi K2 Thinking",
660 ModelId::MoonshotKimiK20905Preview => "Kimi K2 0905 Preview",
661 ModelId::MoonshotKimiK20711Preview => "Kimi K2 0711 Preview",
662 ModelId::MoonshotKimiLatest => "Kimi Latest (auto-tier)",
663 ModelId::MoonshotKimiLatest8k => "Kimi Latest 8K",
664 ModelId::MoonshotKimiLatest32k => "Kimi Latest 32K",
665 ModelId::MoonshotKimiLatest128k => "Kimi Latest 128K",
666 ModelId::OllamaGptOss20b => "GPT-OSS 20B (local)",
668 ModelId::OllamaGptOss20bCloud => "GPT-OSS 20B (cloud)",
669 ModelId::OllamaGptOss120bCloud => "GPT-OSS 120B (cloud)",
670 ModelId::OllamaQwen317b => "Qwen3 1.7B (local)",
671 ModelId::OllamaDeepseekV31671bCloud => "DeepSeek V3.1 671B (cloud)",
672
673 ModelId::OllamaKimiK21tCloud => "Kimi K2 1T (cloud)",
674 ModelId::OllamaQwen3Coder480bCloud => "Qwen3 Coder 480B (cloud)",
675 ModelId::OllamaGlm46Cloud => "GLM-4.6 (cloud)",
676 ModelId::OllamaMinimaxM2Cloud => "MiniMax-M2 (cloud)",
677 ModelId::LmStudioMetaLlama38BInstruct => "Meta Llama 3 8B (LM Studio)",
678 ModelId::LmStudioMetaLlama318BInstruct => "Meta Llama 3.1 8B (LM Studio)",
679 ModelId::LmStudioQwen257BInstruct => "Qwen2.5 7B (LM Studio)",
680 ModelId::LmStudioGemma22BIt => "Gemma 2 2B (LM Studio)",
681 ModelId::LmStudioGemma29BIt => "Gemma 2 9B (LM Studio)",
682 ModelId::LmStudioPhi31Mini4kInstruct => "Phi-3.1 Mini 4K (LM Studio)",
683 _ => unreachable!(),
685 }
686 }
687
688 pub fn description(&self) -> &'static str {
690 if let Some(meta) = self.openrouter_metadata() {
691 return meta.description;
692 }
693 match self {
694 ModelId::Gemini25FlashPreview => {
696 "Latest fast Gemini model with advanced multimodal capabilities"
697 }
698 ModelId::Gemini25Flash => {
699 "Legacy alias for Gemini 2.5 Flash Preview (same capabilities)"
700 }
701 ModelId::Gemini25FlashLite => {
702 "Legacy alias for Gemini 2.5 Flash Preview optimized for efficiency"
703 }
704 ModelId::Gemini25Pro => "Latest most capable Gemini model with reasoning",
705 ModelId::Gemini3ProPreview => {
706 "Preview of next-generation Gemini 3 Pro model with advanced reasoning and capabilities"
707 }
708 ModelId::GPT5 => "Latest most capable OpenAI model with advanced reasoning",
710 ModelId::GPT5Codex => {
711 "Code-focused GPT-5 variant optimized for tool calling and structured outputs"
712 }
713 ModelId::GPT5Mini => "Latest efficient OpenAI model, great for most tasks",
714 ModelId::GPT5Nano => "Latest most cost-effective OpenAI model",
715 ModelId::CodexMiniLatest => "Latest Codex model optimized for code generation",
716 ModelId::OpenAIGptOss20b => {
717 "OpenAI's open-source 20B parameter GPT-OSS model using harmony tokenization"
718 }
719 ModelId::OpenAIGptOss120b => {
720 "OpenAI's open-source 120B parameter GPT-OSS model using harmony tokenization"
721 }
722 ModelId::ClaudeOpus41 => "Latest most capable Anthropic model with advanced reasoning",
724 ModelId::ClaudeSonnet45 => "Latest balanced Anthropic model for general tasks",
725 ModelId::ClaudeHaiku45 => {
726 "Latest efficient Anthropic model optimized for low-latency agent workflows"
727 }
728 ModelId::ClaudeSonnet4 => {
729 "Previous balanced Anthropic model maintained for compatibility"
730 }
731 ModelId::DeepSeekChat => {
733 "DeepSeek V3.2-Exp non-thinking mode optimized for fast coding responses"
734 }
735 ModelId::DeepSeekReasoner => {
736 "DeepSeek V3.2-Exp thinking mode with structured reasoning output"
737 }
738 ModelId::XaiGrok4 => "Flagship Grok 4 model with long context and tool use",
740 ModelId::XaiGrok4Mini => "Efficient Grok 4 Mini tuned for low latency",
741 ModelId::XaiGrok4Code => "Code-specialized Grok 4 deployment with tool support",
742 ModelId::XaiGrok4CodeLatest => {
743 "Latest Grok 4 code model offering enhanced reasoning traces"
744 }
745 ModelId::XaiGrok4Vision => "Multimodal Grok 4 model with image understanding",
746 ModelId::ZaiGlm46 => {
748 "Latest Z.AI GLM flagship with long-context reasoning and coding strengths"
749 }
750 ModelId::ZaiGlm45 => "Balanced GLM 4.5 release for general assistant tasks",
751 ModelId::ZaiGlm45Air => "Efficient GLM 4.5 Air variant tuned for lower latency",
752 ModelId::ZaiGlm45X => "Enhanced GLM 4.5 X variant with improved reasoning",
753 ModelId::ZaiGlm45Airx => "Hybrid GLM 4.5 AirX variant blending efficiency with quality",
754 ModelId::ZaiGlm45Flash => "Low-latency GLM 4.5 Flash optimized for responsiveness",
755 ModelId::ZaiGlm432b0414128k => {
756 "Legacy GLM 4 32B deployment offering extended 128K context window"
757 }
758 ModelId::MoonshotKimiK2TurboPreview => {
760 "Recommended high-speed Kimi K2 turbo variant with 256K context and 60+ tok/s output"
761 }
762 ModelId::MoonshotKimiK2Thinking => {
763 "Moonshot reasoning-tier Kimi K2 release optimized for deliberate, multi-step agentic reasoning"
764 }
765 ModelId::MoonshotKimiK20905Preview => {
766 "Latest Kimi K2 0905 flagship with enhanced agentic coding, 256K context, and richer tool support"
767 }
768 ModelId::MoonshotKimiK20711Preview => {
769 "Kimi K2 0711 preview tuned for balanced cost and capability with 131K context"
770 }
771 ModelId::MoonshotKimiLatest => {
772 "Auto-tier alias that selects the right Kimi Latest vision tier (8K/32K/128K) with context caching"
773 }
774 ModelId::MoonshotKimiLatest8k => {
775 "Kimi Latest 8K vision tier for short tasks with automatic context caching"
776 }
777 ModelId::MoonshotKimiLatest32k => {
778 "Kimi Latest 32K vision tier blending longer context with latest assistant features"
779 }
780 ModelId::MoonshotKimiLatest128k => {
781 "Kimi Latest 128K flagship vision tier delivering maximum context and newest capabilities"
782 }
783 ModelId::OllamaGptOss20b => {
784 "Local GPT-OSS 20B deployment served via Ollama with no external API dependency"
785 }
786 ModelId::OllamaGptOss20bCloud => {
787 "Cloud-hosted GPT-OSS 20B accessed through Ollama Cloud for efficient reasoning tasks"
788 }
789 ModelId::OllamaGptOss120bCloud => {
790 "Cloud-hosted GPT-OSS 120B accessed through Ollama Cloud for larger reasoning tasks"
791 }
792 ModelId::OllamaQwen317b => {
793 "Qwen3 1.7B served locally through Ollama without external API requirements"
794 }
795 ModelId::OllamaDeepseekV31671bCloud => {
796 "Cloud-hosted DeepSeek V3.1 671B model accessed through Ollama Cloud for advanced reasoning"
797 }
798
799 ModelId::OllamaKimiK21tCloud => {
800 "Cloud-hosted Kimi K2 1T model accessed through Ollama Cloud for high-capacity reasoning tasks"
801 }
802 ModelId::OllamaQwen3Coder480bCloud => {
803 "Cloud-hosted Qwen3 Coder 480B model accessed through Ollama Cloud for coding tasks"
804 }
805 ModelId::OllamaGlm46Cloud => {
806 "Cloud-hosted GLM-4.6 model accessed through Ollama Cloud for reasoning and coding"
807 }
808 ModelId::OllamaMinimaxM2Cloud => {
809 "Cloud-hosted MiniMax-M2 model accessed through Ollama Cloud for reasoning tasks"
810 }
811 ModelId::LmStudioMetaLlama38BInstruct => {
812 "Meta Llama 3 8B running through LM Studio's local OpenAI-compatible server"
813 }
814 ModelId::LmStudioMetaLlama318BInstruct => {
815 "Meta Llama 3.1 8B running through LM Studio's local OpenAI-compatible server"
816 }
817 ModelId::LmStudioQwen257BInstruct => {
818 "Qwen2.5 7B hosted in LM Studio for local experimentation and coding tasks"
819 }
820 ModelId::LmStudioGemma22BIt => {
821 "Gemma 2 2B IT deployed via LM Studio for lightweight on-device assistance"
822 }
823 ModelId::LmStudioGemma29BIt => {
824 "Gemma 2 9B IT served locally via LM Studio when you need additional capacity"
825 }
826 ModelId::LmStudioPhi31Mini4kInstruct => {
827 "Phi-3.1 Mini 4K hosted in LM Studio for compact reasoning and experimentation"
828 }
829 _ => unreachable!(),
830 }
831 }
832
833 pub fn openrouter_vendor(&self) -> Option<&'static str> {
835 self.openrouter_metadata().map(|meta| meta.vendor)
836 }
837
838 pub fn all_models() -> Vec<ModelId> {
840 let mut models = vec![
841 ModelId::Gemini25FlashPreview,
843 ModelId::Gemini25Flash,
844 ModelId::Gemini25FlashLite,
845 ModelId::Gemini25Pro,
846 ModelId::GPT5,
848 ModelId::GPT5Codex,
849 ModelId::GPT5Mini,
850 ModelId::GPT5Nano,
851 ModelId::CodexMiniLatest,
852 ModelId::ClaudeOpus41,
854 ModelId::ClaudeSonnet45,
855 ModelId::ClaudeHaiku45,
856 ModelId::ClaudeSonnet4,
857 ModelId::DeepSeekChat,
859 ModelId::DeepSeekReasoner,
860 ModelId::XaiGrok4,
862 ModelId::XaiGrok4Mini,
863 ModelId::XaiGrok4Code,
864 ModelId::XaiGrok4CodeLatest,
865 ModelId::XaiGrok4Vision,
866 ModelId::ZaiGlm46,
868 ModelId::ZaiGlm45,
869 ModelId::ZaiGlm45Air,
870 ModelId::ZaiGlm45X,
871 ModelId::ZaiGlm45Airx,
872 ModelId::ZaiGlm45Flash,
873 ModelId::ZaiGlm432b0414128k,
874 ModelId::MoonshotKimiK2TurboPreview,
876 ModelId::MoonshotKimiK2Thinking,
877 ModelId::MoonshotKimiK20905Preview,
878 ModelId::MoonshotKimiK20711Preview,
879 ModelId::MoonshotKimiLatest,
880 ModelId::MoonshotKimiLatest8k,
881 ModelId::MoonshotKimiLatest32k,
882 ModelId::MoonshotKimiLatest128k,
883 ModelId::OllamaGptOss20b,
885 ModelId::OllamaGptOss20bCloud,
886 ModelId::OllamaGptOss120bCloud,
887 ModelId::OllamaQwen317b,
888 ModelId::OllamaDeepseekV31671bCloud,
889 ModelId::OllamaKimiK21tCloud,
890 ModelId::OllamaQwen3Coder480bCloud,
891 ModelId::OllamaGlm46Cloud,
892 ModelId::OllamaMinimaxM2Cloud,
893 ModelId::LmStudioMetaLlama38BInstruct,
895 ModelId::LmStudioMetaLlama318BInstruct,
896 ModelId::LmStudioQwen257BInstruct,
897 ModelId::LmStudioGemma22BIt,
898 ModelId::LmStudioGemma29BIt,
899 ModelId::LmStudioPhi31Mini4kInstruct,
900 ];
901 models.extend(Self::openrouter_models());
902 models
903 }
904
905 pub fn models_for_provider(provider: Provider) -> Vec<ModelId> {
907 Self::all_models()
908 .into_iter()
909 .filter(|model| model.provider() == provider)
910 .collect()
911 }
912
913 pub fn fallback_models() -> Vec<ModelId> {
915 vec![
916 ModelId::Gemini25FlashPreview,
917 ModelId::Gemini25Pro,
918 ModelId::GPT5,
919 ModelId::OpenAIGptOss20b,
920 ModelId::ClaudeOpus41,
921 ModelId::ClaudeSonnet45,
922 ModelId::DeepSeekReasoner,
923 ModelId::MoonshotKimiK20905Preview,
924 ModelId::XaiGrok4,
925 ModelId::ZaiGlm46,
926 ModelId::OpenRouterGrokCodeFast1,
927 ]
928 }
929
930 pub fn default_orchestrator() -> Self {
932 ModelId::Gemini25Pro
933 }
934
935 pub fn default_subagent() -> Self {
937 ModelId::Gemini25FlashPreview
938 }
939
940 pub fn default_orchestrator_for_provider(provider: Provider) -> Self {
942 match provider {
943 Provider::Gemini => ModelId::Gemini25Pro,
944 Provider::OpenAI => ModelId::GPT5,
945 Provider::Anthropic => ModelId::ClaudeOpus41,
946 Provider::DeepSeek => ModelId::DeepSeekReasoner,
947 Provider::Moonshot => ModelId::MoonshotKimiK20905Preview,
948 Provider::XAI => ModelId::XaiGrok4,
949 Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
950 Provider::Ollama => ModelId::OllamaGptOss20b,
951 Provider::LmStudio => ModelId::LmStudioMetaLlama318BInstruct,
952 Provider::ZAI => ModelId::ZaiGlm46,
953 }
954 }
955
956 pub fn default_subagent_for_provider(provider: Provider) -> Self {
958 match provider {
959 Provider::Gemini => ModelId::Gemini25FlashPreview,
960 Provider::OpenAI => ModelId::GPT5Mini,
961 Provider::Anthropic => ModelId::ClaudeSonnet45,
962 Provider::DeepSeek => ModelId::DeepSeekChat,
963 Provider::Moonshot => ModelId::MoonshotKimiK2TurboPreview,
964 Provider::XAI => ModelId::XaiGrok4Code,
965 Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
966 Provider::Ollama => ModelId::OllamaQwen317b,
967 Provider::LmStudio => ModelId::LmStudioQwen257BInstruct,
968 Provider::ZAI => ModelId::ZaiGlm45Flash,
969 }
970 }
971
972 pub fn default_single_for_provider(provider: Provider) -> Self {
974 match provider {
975 Provider::Gemini => ModelId::Gemini25FlashPreview,
976 Provider::OpenAI => ModelId::GPT5,
977 Provider::Anthropic => ModelId::ClaudeOpus41,
978 Provider::DeepSeek => ModelId::DeepSeekReasoner,
979 Provider::Moonshot => ModelId::MoonshotKimiK2TurboPreview,
980 Provider::XAI => ModelId::XaiGrok4,
981 Provider::OpenRouter => ModelId::OpenRouterGrokCodeFast1,
982 Provider::Ollama => ModelId::OllamaGptOss20b,
983 Provider::LmStudio => ModelId::LmStudioMetaLlama318BInstruct,
984 Provider::ZAI => ModelId::ZaiGlm46,
985 }
986 }
987
988 pub fn is_flash_variant(&self) -> bool {
990 matches!(
991 self,
992 ModelId::Gemini25FlashPreview
993 | ModelId::Gemini25Flash
994 | ModelId::Gemini25FlashLite
995 | ModelId::ZaiGlm45Flash
996 | ModelId::MoonshotKimiK2TurboPreview
997 | ModelId::MoonshotKimiLatest8k
998 )
999 }
1000
1001 pub fn is_pro_variant(&self) -> bool {
1003 matches!(
1004 self,
1005 ModelId::Gemini25Pro
1006 | ModelId::GPT5
1007 | ModelId::GPT5Codex
1008 | ModelId::ClaudeOpus41
1009 | ModelId::DeepSeekReasoner
1010 | ModelId::XaiGrok4
1011 | ModelId::ZaiGlm46
1012 | ModelId::MoonshotKimiK2Thinking
1013 | ModelId::MoonshotKimiK20905Preview
1014 | ModelId::MoonshotKimiLatest128k
1015 )
1016 }
1017
1018 pub fn is_efficient_variant(&self) -> bool {
1020 if let Some(meta) = self.openrouter_metadata() {
1021 return meta.efficient;
1022 }
1023 matches!(
1024 self,
1025 ModelId::Gemini25FlashPreview
1026 | ModelId::Gemini25Flash
1027 | ModelId::Gemini25FlashLite
1028 | ModelId::GPT5Mini
1029 | ModelId::GPT5Nano
1030 | ModelId::ClaudeHaiku45
1031 | ModelId::DeepSeekChat
1032 | ModelId::XaiGrok4Code
1033 | ModelId::ZaiGlm45Air
1034 | ModelId::ZaiGlm45Airx
1035 | ModelId::ZaiGlm45Flash
1036 | ModelId::MoonshotKimiK2TurboPreview
1037 | ModelId::MoonshotKimiLatest8k
1038 )
1039 }
1040
1041 pub fn is_top_tier(&self) -> bool {
1043 if let Some(meta) = self.openrouter_metadata() {
1044 return meta.top_tier;
1045 }
1046 matches!(
1047 self,
1048 ModelId::Gemini25Pro
1049 | ModelId::GPT5
1050 | ModelId::GPT5Codex
1051 | ModelId::ClaudeOpus41
1052 | ModelId::ClaudeSonnet45
1053 | ModelId::ClaudeSonnet4
1054 | ModelId::DeepSeekReasoner
1055 | ModelId::XaiGrok4
1056 | ModelId::XaiGrok4CodeLatest
1057 | ModelId::ZaiGlm46
1058 | ModelId::MoonshotKimiK2Thinking
1059 | ModelId::MoonshotKimiK20905Preview
1060 | ModelId::MoonshotKimiLatest128k
1061 )
1062 }
1063
1064 pub fn is_reasoning_variant(&self) -> bool {
1066 if matches!(self, ModelId::MoonshotKimiK2Thinking) {
1067 return true;
1068 }
1069 if let Some(meta) = self.openrouter_metadata() {
1070 return meta.reasoning;
1071 }
1072 self.provider().supports_reasoning_effort(self.as_str())
1073 }
1074
1075 pub fn supports_tool_calls(&self) -> bool {
1077 if let Some(meta) = self.openrouter_metadata() {
1078 return meta.tool_call;
1079 }
1080 true
1081 }
1082
1083 pub fn generation(&self) -> &'static str {
1085 if let Some(meta) = self.openrouter_metadata() {
1086 return meta.generation;
1087 }
1088 match self {
1089 ModelId::Gemini25FlashPreview
1091 | ModelId::Gemini25Flash
1092 | ModelId::Gemini25FlashLite
1093 | ModelId::Gemini25Pro => "2.5",
1094 ModelId::Gemini3ProPreview => "3",
1095 ModelId::GPT5
1097 | ModelId::GPT5Codex
1098 | ModelId::GPT5Mini
1099 | ModelId::GPT5Nano
1100 | ModelId::CodexMiniLatest => "5",
1101 ModelId::ClaudeSonnet45 | ModelId::ClaudeHaiku45 => "4.5",
1103 ModelId::ClaudeSonnet4 => "4",
1104 ModelId::ClaudeOpus41 => "4.1",
1105 ModelId::DeepSeekChat | ModelId::DeepSeekReasoner => "V3.2-Exp",
1107 ModelId::XaiGrok4
1109 | ModelId::XaiGrok4Mini
1110 | ModelId::XaiGrok4Code
1111 | ModelId::XaiGrok4CodeLatest
1112 | ModelId::XaiGrok4Vision => "4",
1113 ModelId::ZaiGlm46 => "4.6",
1115 ModelId::ZaiGlm45
1116 | ModelId::ZaiGlm45Air
1117 | ModelId::ZaiGlm45X
1118 | ModelId::ZaiGlm45Airx
1119 | ModelId::ZaiGlm45Flash => "4.5",
1120 ModelId::ZaiGlm432b0414128k => "4-32B",
1121 ModelId::MoonshotKimiK2TurboPreview
1123 | ModelId::MoonshotKimiK20905Preview
1124 | ModelId::MoonshotKimiK20711Preview => "k2",
1125 ModelId::MoonshotKimiK2Thinking => "k2-thinking",
1126 ModelId::MoonshotKimiLatest
1127 | ModelId::MoonshotKimiLatest8k
1128 | ModelId::MoonshotKimiLatest32k
1129 | ModelId::MoonshotKimiLatest128k => "latest",
1130 ModelId::OllamaGptOss20b => "oss",
1131 ModelId::OllamaGptOss20bCloud => "oss-cloud",
1132 ModelId::OllamaGptOss120bCloud => "oss-cloud",
1133 ModelId::OllamaQwen317b => "oss",
1134 ModelId::OllamaDeepseekV31671bCloud => "deepseek-cloud",
1135 ModelId::OllamaKimiK21tCloud => "kimi-k2-cloud",
1136 ModelId::OllamaQwen3Coder480bCloud => "qwen3-coder-cloud",
1137 ModelId::OllamaGlm46Cloud => "glm-cloud",
1138 ModelId::OllamaMinimaxM2Cloud => "minimax-cloud",
1139 ModelId::LmStudioMetaLlama38BInstruct => "meta-llama-3",
1140 ModelId::LmStudioMetaLlama318BInstruct => "meta-llama-3.1",
1141 ModelId::LmStudioQwen257BInstruct => "qwen2.5",
1142 ModelId::LmStudioGemma22BIt => "gemma-2",
1143 ModelId::LmStudioGemma29BIt => "gemma-2",
1144 ModelId::LmStudioPhi31Mini4kInstruct => "phi-3.1",
1145 _ => unreachable!(),
1146 }
1147 }
1148}
1149
1150impl fmt::Display for ModelId {
1151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1152 write!(f, "{}", self.as_str())
1153 }
1154}
1155
1156impl FromStr for ModelId {
1157 type Err = ModelParseError;
1158
1159 fn from_str(s: &str) -> Result<Self, Self::Err> {
1160 use crate::constants::models;
1161 match s {
1162 s if s == models::GEMINI_2_5_FLASH_PREVIEW => Ok(ModelId::Gemini25FlashPreview),
1164 s if s == models::GEMINI_2_5_FLASH => Ok(ModelId::Gemini25Flash),
1165 s if s == models::GEMINI_2_5_FLASH_LITE => Ok(ModelId::Gemini25FlashLite),
1166 s if s == models::GEMINI_2_5_PRO => Ok(ModelId::Gemini25Pro),
1167 s if s == models::GEMINI_3_PRO_PREVIEW => Ok(ModelId::Gemini3ProPreview),
1168 s if s == models::GPT_5 => Ok(ModelId::GPT5),
1170 s if s == models::GPT_5_CODEX => Ok(ModelId::GPT5Codex),
1171 s if s == models::GPT_5_MINI => Ok(ModelId::GPT5Mini),
1172 s if s == models::GPT_5_NANO => Ok(ModelId::GPT5Nano),
1173 s if s == models::CODEX_MINI_LATEST => Ok(ModelId::CodexMiniLatest),
1174 s if s == models::openai::GPT_OSS_20B => Ok(ModelId::OpenAIGptOss20b),
1175 s if s == models::openai::GPT_OSS_120B => Ok(ModelId::OpenAIGptOss120b),
1176 s if s == models::CLAUDE_OPUS_4_1 => Ok(ModelId::ClaudeOpus41),
1178 s if s == models::CLAUDE_SONNET_4_5 => Ok(ModelId::ClaudeSonnet45),
1179 s if s == models::CLAUDE_HAIKU_4_5 => Ok(ModelId::ClaudeHaiku45),
1180 s if s == models::CLAUDE_SONNET_4_5_20250929 => Ok(ModelId::ClaudeSonnet4),
1181 s if s == models::DEEPSEEK_CHAT => Ok(ModelId::DeepSeekChat),
1183 s if s == models::DEEPSEEK_REASONER => Ok(ModelId::DeepSeekReasoner),
1184 s if s == models::xai::GROK_4 => Ok(ModelId::XaiGrok4),
1186 s if s == models::xai::GROK_4_MINI => Ok(ModelId::XaiGrok4Mini),
1187 s if s == models::xai::GROK_4_CODE => Ok(ModelId::XaiGrok4Code),
1188 s if s == models::xai::GROK_4_CODE_LATEST => Ok(ModelId::XaiGrok4CodeLatest),
1189 s if s == models::xai::GROK_4_VISION => Ok(ModelId::XaiGrok4Vision),
1190 s if s == models::zai::GLM_4_6 => Ok(ModelId::ZaiGlm46),
1192 s if s == models::zai::GLM_4_5 => Ok(ModelId::ZaiGlm45),
1193 s if s == models::zai::GLM_4_5_AIR => Ok(ModelId::ZaiGlm45Air),
1194 s if s == models::zai::GLM_4_5_X => Ok(ModelId::ZaiGlm45X),
1195 s if s == models::zai::GLM_4_5_AIRX => Ok(ModelId::ZaiGlm45Airx),
1196 s if s == models::zai::GLM_4_5_FLASH => Ok(ModelId::ZaiGlm45Flash),
1197 s if s == models::zai::GLM_4_32B_0414_128K => Ok(ModelId::ZaiGlm432b0414128k),
1198 s if s == models::MOONSHOT_KIMI_K2_TURBO_PREVIEW => {
1200 Ok(ModelId::MoonshotKimiK2TurboPreview)
1201 }
1202 s if s == models::MOONSHOT_KIMI_K2_THINKING => Ok(ModelId::MoonshotKimiK2Thinking),
1203 s if s == models::MOONSHOT_KIMI_K2_0905_PREVIEW => {
1204 Ok(ModelId::MoonshotKimiK20905Preview)
1205 }
1206 s if s == models::MOONSHOT_KIMI_K2_0711_PREVIEW => {
1207 Ok(ModelId::MoonshotKimiK20711Preview)
1208 }
1209 s if s == models::MOONSHOT_KIMI_LATEST => Ok(ModelId::MoonshotKimiLatest),
1210 s if s == models::MOONSHOT_KIMI_LATEST_8K => Ok(ModelId::MoonshotKimiLatest8k),
1211 s if s == models::MOONSHOT_KIMI_LATEST_32K => Ok(ModelId::MoonshotKimiLatest32k),
1212 s if s == models::MOONSHOT_KIMI_LATEST_128K => Ok(ModelId::MoonshotKimiLatest128k),
1213 s if s == models::ollama::GPT_OSS_20B => Ok(ModelId::OllamaGptOss20b),
1214 s if s == models::ollama::GPT_OSS_20B_CLOUD => Ok(ModelId::OllamaGptOss20bCloud),
1215 s if s == models::ollama::GPT_OSS_120B_CLOUD => Ok(ModelId::OllamaGptOss120bCloud),
1216 s if s == models::ollama::QWEN3_1_7B => Ok(ModelId::OllamaQwen317b),
1217 s if s == models::ollama::DEEPSEEK_V31_671B_CLOUD => {
1218 Ok(ModelId::OllamaDeepseekV31671bCloud)
1219 }
1220
1221 s if s == models::ollama::KIMI_K2_1T_CLOUD => Ok(ModelId::OllamaKimiK21tCloud),
1222 s if s == models::ollama::QWEN3_CODER_480B_CLOUD => {
1223 Ok(ModelId::OllamaQwen3Coder480bCloud)
1224 }
1225 s if s == models::ollama::GLM_46_CLOUD => Ok(ModelId::OllamaGlm46Cloud),
1226 s if s == models::ollama::MINIMAX_M2_CLOUD => Ok(ModelId::OllamaMinimaxM2Cloud),
1227 s if s == models::lmstudio::META_LLAMA_3_8B_INSTRUCT => {
1228 Ok(ModelId::LmStudioMetaLlama38BInstruct)
1229 }
1230 s if s == models::lmstudio::META_LLAMA_31_8B_INSTRUCT => {
1231 Ok(ModelId::LmStudioMetaLlama318BInstruct)
1232 }
1233 s if s == models::lmstudio::QWEN25_7B_INSTRUCT => Ok(ModelId::LmStudioQwen257BInstruct),
1234 s if s == models::lmstudio::GEMMA_2_2B_IT => Ok(ModelId::LmStudioGemma22BIt),
1235 s if s == models::lmstudio::GEMMA_2_9B_IT => Ok(ModelId::LmStudioGemma29BIt),
1236 s if s == models::lmstudio::PHI_31_MINI_4K_INSTRUCT => {
1237 Ok(ModelId::LmStudioPhi31Mini4kInstruct)
1238 }
1239 _ => {
1240 if let Some(model) = Self::parse_openrouter_model(s) {
1241 Ok(model)
1242 } else {
1243 Err(ModelParseError::InvalidModel(s.to_string()))
1244 }
1245 }
1246 }
1247 }
1248}
1249
1250#[derive(Debug, Clone, PartialEq)]
1252pub enum ModelParseError {
1253 InvalidModel(String),
1254 InvalidProvider(String),
1255}
1256
1257impl fmt::Display for ModelParseError {
1258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1259 match self {
1260 ModelParseError::InvalidModel(model) => {
1261 write!(
1262 f,
1263 "Invalid model identifier: '{}'. Supported models: {}",
1264 model,
1265 ModelId::all_models()
1266 .iter()
1267 .map(|m| m.as_str())
1268 .collect::<Vec<_>>()
1269 .join(", ")
1270 )
1271 }
1272 ModelParseError::InvalidProvider(provider) => {
1273 write!(
1274 f,
1275 "Invalid provider: '{}'. Supported providers: {}",
1276 provider,
1277 Provider::all_providers()
1278 .iter()
1279 .map(|p| p.to_string())
1280 .collect::<Vec<_>>()
1281 .join(", ")
1282 )
1283 }
1284 }
1285 }
1286}
1287
1288impl std::error::Error for ModelParseError {}
1289
1290#[cfg(test)]
1291mod tests {
1292 use super::*;
1293 use crate::constants::models;
1294
1295 #[test]
1296 fn test_model_string_conversion() {
1297 assert_eq!(
1299 ModelId::Gemini25FlashPreview.as_str(),
1300 models::GEMINI_2_5_FLASH_PREVIEW
1301 );
1302 assert_eq!(ModelId::Gemini25Flash.as_str(), models::GEMINI_2_5_FLASH);
1303 assert_eq!(
1304 ModelId::Gemini25FlashLite.as_str(),
1305 models::GEMINI_2_5_FLASH_LITE
1306 );
1307 assert_eq!(ModelId::Gemini25Pro.as_str(), models::GEMINI_2_5_PRO);
1308 assert_eq!(ModelId::GPT5.as_str(), models::GPT_5);
1310 assert_eq!(ModelId::GPT5Codex.as_str(), models::GPT_5_CODEX);
1311 assert_eq!(ModelId::GPT5Mini.as_str(), models::GPT_5_MINI);
1312 assert_eq!(ModelId::GPT5Nano.as_str(), models::GPT_5_NANO);
1313 assert_eq!(ModelId::CodexMiniLatest.as_str(), models::CODEX_MINI_LATEST);
1314 assert_eq!(ModelId::ClaudeSonnet45.as_str(), models::CLAUDE_SONNET_4_5);
1316 assert_eq!(ModelId::ClaudeHaiku45.as_str(), models::CLAUDE_HAIKU_4_5);
1317 assert_eq!(
1318 ModelId::ClaudeSonnet4.as_str(),
1319 models::CLAUDE_SONNET_4_5_20250929
1320 );
1321 assert_eq!(ModelId::ClaudeOpus41.as_str(), models::CLAUDE_OPUS_4_1);
1322 assert_eq!(ModelId::DeepSeekChat.as_str(), models::DEEPSEEK_CHAT);
1324 assert_eq!(
1325 ModelId::DeepSeekReasoner.as_str(),
1326 models::DEEPSEEK_REASONER
1327 );
1328 assert_eq!(ModelId::XaiGrok4.as_str(), models::xai::GROK_4);
1330 assert_eq!(ModelId::XaiGrok4Mini.as_str(), models::xai::GROK_4_MINI);
1331 assert_eq!(ModelId::XaiGrok4Code.as_str(), models::xai::GROK_4_CODE);
1332 assert_eq!(
1333 ModelId::XaiGrok4CodeLatest.as_str(),
1334 models::xai::GROK_4_CODE_LATEST
1335 );
1336 assert_eq!(ModelId::XaiGrok4Vision.as_str(), models::xai::GROK_4_VISION);
1337 assert_eq!(ModelId::ZaiGlm46.as_str(), models::zai::GLM_4_6);
1339 assert_eq!(ModelId::ZaiGlm45.as_str(), models::zai::GLM_4_5);
1340 assert_eq!(ModelId::ZaiGlm45Air.as_str(), models::zai::GLM_4_5_AIR);
1341 assert_eq!(ModelId::ZaiGlm45X.as_str(), models::zai::GLM_4_5_X);
1342 assert_eq!(ModelId::ZaiGlm45Airx.as_str(), models::zai::GLM_4_5_AIRX);
1343 assert_eq!(ModelId::ZaiGlm45Flash.as_str(), models::zai::GLM_4_5_FLASH);
1344 assert_eq!(
1345 ModelId::ZaiGlm432b0414128k.as_str(),
1346 models::zai::GLM_4_32B_0414_128K
1347 );
1348 for entry in openrouter_generated::ENTRIES {
1349 assert_eq!(entry.variant.as_str(), entry.id);
1350 }
1351 }
1352
1353 #[test]
1354 fn test_model_from_string() {
1355 assert_eq!(
1357 models::GEMINI_2_5_FLASH_PREVIEW.parse::<ModelId>().unwrap(),
1358 ModelId::Gemini25FlashPreview
1359 );
1360 assert_eq!(
1361 models::GEMINI_2_5_FLASH.parse::<ModelId>().unwrap(),
1362 ModelId::Gemini25Flash
1363 );
1364 assert_eq!(
1365 models::GEMINI_2_5_FLASH_LITE.parse::<ModelId>().unwrap(),
1366 ModelId::Gemini25FlashLite
1367 );
1368 assert_eq!(
1369 models::GEMINI_2_5_PRO.parse::<ModelId>().unwrap(),
1370 ModelId::Gemini25Pro
1371 );
1372 assert_eq!(models::GPT_5.parse::<ModelId>().unwrap(), ModelId::GPT5);
1374 assert_eq!(
1375 models::GPT_5_CODEX.parse::<ModelId>().unwrap(),
1376 ModelId::GPT5Codex
1377 );
1378 assert_eq!(
1379 models::GPT_5_MINI.parse::<ModelId>().unwrap(),
1380 ModelId::GPT5Mini
1381 );
1382 assert_eq!(
1383 models::GPT_5_NANO.parse::<ModelId>().unwrap(),
1384 ModelId::GPT5Nano
1385 );
1386 assert_eq!(
1387 models::CODEX_MINI_LATEST.parse::<ModelId>().unwrap(),
1388 ModelId::CodexMiniLatest
1389 );
1390 assert_eq!(
1391 models::openai::GPT_OSS_20B.parse::<ModelId>().unwrap(),
1392 ModelId::OpenAIGptOss20b
1393 );
1394 assert_eq!(
1395 models::openai::GPT_OSS_120B.parse::<ModelId>().unwrap(),
1396 ModelId::OpenAIGptOss120b
1397 );
1398 assert_eq!(
1400 models::CLAUDE_SONNET_4_5.parse::<ModelId>().unwrap(),
1401 ModelId::ClaudeSonnet45
1402 );
1403 assert_eq!(
1404 models::CLAUDE_HAIKU_4_5.parse::<ModelId>().unwrap(),
1405 ModelId::ClaudeHaiku45
1406 );
1407 assert_eq!(
1408 models::CLAUDE_SONNET_4_5_20250929
1409 .parse::<ModelId>()
1410 .unwrap(),
1411 ModelId::ClaudeSonnet4
1412 );
1413 assert_eq!(
1414 models::CLAUDE_OPUS_4_1.parse::<ModelId>().unwrap(),
1415 ModelId::ClaudeOpus41
1416 );
1417 assert_eq!(
1419 models::DEEPSEEK_CHAT.parse::<ModelId>().unwrap(),
1420 ModelId::DeepSeekChat
1421 );
1422 assert_eq!(
1423 models::DEEPSEEK_REASONER.parse::<ModelId>().unwrap(),
1424 ModelId::DeepSeekReasoner
1425 );
1426 assert_eq!(
1428 models::xai::GROK_4.parse::<ModelId>().unwrap(),
1429 ModelId::XaiGrok4
1430 );
1431 assert_eq!(
1432 models::xai::GROK_4_MINI.parse::<ModelId>().unwrap(),
1433 ModelId::XaiGrok4Mini
1434 );
1435 assert_eq!(
1436 models::xai::GROK_4_CODE.parse::<ModelId>().unwrap(),
1437 ModelId::XaiGrok4Code
1438 );
1439 assert_eq!(
1440 models::xai::GROK_4_CODE_LATEST.parse::<ModelId>().unwrap(),
1441 ModelId::XaiGrok4CodeLatest
1442 );
1443 assert_eq!(
1444 models::xai::GROK_4_VISION.parse::<ModelId>().unwrap(),
1445 ModelId::XaiGrok4Vision
1446 );
1447 assert_eq!(
1449 models::zai::GLM_4_6.parse::<ModelId>().unwrap(),
1450 ModelId::ZaiGlm46
1451 );
1452 assert_eq!(
1453 models::zai::GLM_4_5.parse::<ModelId>().unwrap(),
1454 ModelId::ZaiGlm45
1455 );
1456 assert_eq!(
1457 models::zai::GLM_4_5_AIR.parse::<ModelId>().unwrap(),
1458 ModelId::ZaiGlm45Air
1459 );
1460 assert_eq!(
1461 models::zai::GLM_4_5_X.parse::<ModelId>().unwrap(),
1462 ModelId::ZaiGlm45X
1463 );
1464 assert_eq!(
1465 models::zai::GLM_4_5_AIRX.parse::<ModelId>().unwrap(),
1466 ModelId::ZaiGlm45Airx
1467 );
1468 assert_eq!(
1469 models::zai::GLM_4_5_FLASH.parse::<ModelId>().unwrap(),
1470 ModelId::ZaiGlm45Flash
1471 );
1472 assert_eq!(
1473 models::zai::GLM_4_32B_0414_128K.parse::<ModelId>().unwrap(),
1474 ModelId::ZaiGlm432b0414128k
1475 );
1476 assert_eq!(
1477 models::MOONSHOT_KIMI_K2_TURBO_PREVIEW
1478 .parse::<ModelId>()
1479 .unwrap(),
1480 ModelId::MoonshotKimiK2TurboPreview
1481 );
1482 assert_eq!(
1483 models::MOONSHOT_KIMI_K2_THINKING
1484 .parse::<ModelId>()
1485 .unwrap(),
1486 ModelId::MoonshotKimiK2Thinking
1487 );
1488 assert_eq!(
1489 models::MOONSHOT_KIMI_K2_0905_PREVIEW
1490 .parse::<ModelId>()
1491 .unwrap(),
1492 ModelId::MoonshotKimiK20905Preview
1493 );
1494 assert_eq!(
1495 models::MOONSHOT_KIMI_K2_0711_PREVIEW
1496 .parse::<ModelId>()
1497 .unwrap(),
1498 ModelId::MoonshotKimiK20711Preview
1499 );
1500 assert_eq!(
1501 models::MOONSHOT_KIMI_LATEST.parse::<ModelId>().unwrap(),
1502 ModelId::MoonshotKimiLatest
1503 );
1504 assert_eq!(
1505 models::MOONSHOT_KIMI_LATEST_8K.parse::<ModelId>().unwrap(),
1506 ModelId::MoonshotKimiLatest8k
1507 );
1508 assert_eq!(
1509 models::MOONSHOT_KIMI_LATEST_32K.parse::<ModelId>().unwrap(),
1510 ModelId::MoonshotKimiLatest32k
1511 );
1512 assert_eq!(
1513 models::MOONSHOT_KIMI_LATEST_128K
1514 .parse::<ModelId>()
1515 .unwrap(),
1516 ModelId::MoonshotKimiLatest128k
1517 );
1518 for entry in openrouter_generated::ENTRIES {
1519 assert_eq!(entry.id.parse::<ModelId>().unwrap(), entry.variant);
1520 }
1521 assert!("invalid-model".parse::<ModelId>().is_err());
1523 }
1524
1525 #[test]
1526 fn test_provider_parsing() {
1527 assert_eq!("gemini".parse::<Provider>().unwrap(), Provider::Gemini);
1528 assert_eq!("openai".parse::<Provider>().unwrap(), Provider::OpenAI);
1529 assert_eq!(
1530 "anthropic".parse::<Provider>().unwrap(),
1531 Provider::Anthropic
1532 );
1533 assert_eq!("deepseek".parse::<Provider>().unwrap(), Provider::DeepSeek);
1534 assert_eq!(
1535 "openrouter".parse::<Provider>().unwrap(),
1536 Provider::OpenRouter
1537 );
1538 assert_eq!("xai".parse::<Provider>().unwrap(), Provider::XAI);
1539 assert_eq!("zai".parse::<Provider>().unwrap(), Provider::ZAI);
1540 assert_eq!("moonshot".parse::<Provider>().unwrap(), Provider::Moonshot);
1541 assert_eq!("lmstudio".parse::<Provider>().unwrap(), Provider::LmStudio);
1542 assert!("invalid-provider".parse::<Provider>().is_err());
1543 }
1544
1545 #[test]
1546 fn test_model_providers() {
1547 assert_eq!(ModelId::Gemini25FlashPreview.provider(), Provider::Gemini);
1548 assert_eq!(ModelId::GPT5.provider(), Provider::OpenAI);
1549 assert_eq!(ModelId::GPT5Codex.provider(), Provider::OpenAI);
1550 assert_eq!(ModelId::ClaudeSonnet45.provider(), Provider::Anthropic);
1551 assert_eq!(ModelId::ClaudeHaiku45.provider(), Provider::Anthropic);
1552 assert_eq!(ModelId::ClaudeSonnet4.provider(), Provider::Anthropic);
1553 assert_eq!(ModelId::DeepSeekChat.provider(), Provider::DeepSeek);
1554 assert_eq!(ModelId::XaiGrok4.provider(), Provider::XAI);
1555 assert_eq!(ModelId::ZaiGlm46.provider(), Provider::ZAI);
1556 assert_eq!(
1557 ModelId::MoonshotKimiK20905Preview.provider(),
1558 Provider::Moonshot
1559 );
1560 assert_eq!(ModelId::OllamaGptOss20b.provider(), Provider::Ollama);
1561 assert_eq!(ModelId::OllamaGptOss120bCloud.provider(), Provider::Ollama);
1562 assert_eq!(ModelId::OllamaQwen317b.provider(), Provider::Ollama);
1563 assert_eq!(
1564 ModelId::LmStudioMetaLlama38BInstruct.provider(),
1565 Provider::LmStudio
1566 );
1567 assert_eq!(
1568 ModelId::LmStudioMetaLlama318BInstruct.provider(),
1569 Provider::LmStudio
1570 );
1571 assert_eq!(
1572 ModelId::LmStudioQwen257BInstruct.provider(),
1573 Provider::LmStudio
1574 );
1575 assert_eq!(ModelId::LmStudioGemma22BIt.provider(), Provider::LmStudio);
1576 assert_eq!(ModelId::LmStudioGemma29BIt.provider(), Provider::LmStudio);
1577 assert_eq!(
1578 ModelId::LmStudioPhi31Mini4kInstruct.provider(),
1579 Provider::LmStudio
1580 );
1581 assert_eq!(
1582 ModelId::OpenRouterGrokCodeFast1.provider(),
1583 Provider::OpenRouter
1584 );
1585 assert_eq!(
1586 ModelId::OpenRouterAnthropicClaudeSonnet45.provider(),
1587 Provider::OpenRouter
1588 );
1589
1590 for entry in openrouter_generated::ENTRIES {
1591 assert_eq!(entry.variant.provider(), Provider::OpenRouter);
1592 }
1593 }
1594
1595 #[test]
1596 fn test_provider_defaults() {
1597 assert_eq!(
1598 ModelId::default_orchestrator_for_provider(Provider::Gemini),
1599 ModelId::Gemini25Pro
1600 );
1601 assert_eq!(
1602 ModelId::default_orchestrator_for_provider(Provider::OpenAI),
1603 ModelId::GPT5
1604 );
1605 assert_eq!(
1606 ModelId::default_orchestrator_for_provider(Provider::Anthropic),
1607 ModelId::ClaudeOpus41
1608 );
1609 assert_eq!(
1610 ModelId::default_orchestrator_for_provider(Provider::DeepSeek),
1611 ModelId::DeepSeekReasoner
1612 );
1613 assert_eq!(
1614 ModelId::default_orchestrator_for_provider(Provider::OpenRouter),
1615 ModelId::OpenRouterGrokCodeFast1
1616 );
1617 assert_eq!(
1618 ModelId::default_orchestrator_for_provider(Provider::XAI),
1619 ModelId::XaiGrok4
1620 );
1621 assert_eq!(
1622 ModelId::default_orchestrator_for_provider(Provider::Ollama),
1623 ModelId::OllamaGptOss20b
1624 );
1625 assert_eq!(
1626 ModelId::default_orchestrator_for_provider(Provider::LmStudio),
1627 ModelId::LmStudioMetaLlama318BInstruct
1628 );
1629 assert_eq!(
1630 ModelId::default_orchestrator_for_provider(Provider::ZAI),
1631 ModelId::ZaiGlm46
1632 );
1633 assert_eq!(
1634 ModelId::default_orchestrator_for_provider(Provider::Moonshot),
1635 ModelId::MoonshotKimiK20905Preview
1636 );
1637
1638 assert_eq!(
1639 ModelId::default_subagent_for_provider(Provider::Gemini),
1640 ModelId::Gemini25FlashPreview
1641 );
1642 assert_eq!(
1643 ModelId::default_subagent_for_provider(Provider::OpenAI),
1644 ModelId::GPT5Mini
1645 );
1646 assert_eq!(
1647 ModelId::default_subagent_for_provider(Provider::Anthropic),
1648 ModelId::ClaudeSonnet45
1649 );
1650 assert_eq!(
1651 ModelId::default_subagent_for_provider(Provider::DeepSeek),
1652 ModelId::DeepSeekChat
1653 );
1654 assert_eq!(
1655 ModelId::default_subagent_for_provider(Provider::OpenRouter),
1656 ModelId::OpenRouterGrokCodeFast1
1657 );
1658 assert_eq!(
1659 ModelId::default_subagent_for_provider(Provider::XAI),
1660 ModelId::XaiGrok4Code
1661 );
1662 assert_eq!(
1663 ModelId::default_subagent_for_provider(Provider::Ollama),
1664 ModelId::OllamaQwen317b
1665 );
1666 assert_eq!(
1667 ModelId::default_subagent_for_provider(Provider::LmStudio),
1668 ModelId::LmStudioQwen257BInstruct
1669 );
1670 assert_eq!(
1671 ModelId::default_subagent_for_provider(Provider::ZAI),
1672 ModelId::ZaiGlm45Flash
1673 );
1674 assert_eq!(
1675 ModelId::default_subagent_for_provider(Provider::Moonshot),
1676 ModelId::MoonshotKimiK2TurboPreview
1677 );
1678
1679 assert_eq!(
1680 ModelId::default_single_for_provider(Provider::DeepSeek),
1681 ModelId::DeepSeekReasoner
1682 );
1683 assert_eq!(
1684 ModelId::default_single_for_provider(Provider::Moonshot),
1685 ModelId::MoonshotKimiK2TurboPreview
1686 );
1687 assert_eq!(
1688 ModelId::default_single_for_provider(Provider::Ollama),
1689 ModelId::OllamaGptOss20b
1690 );
1691 assert_eq!(
1692 ModelId::default_single_for_provider(Provider::LmStudio),
1693 ModelId::LmStudioMetaLlama318BInstruct
1694 );
1695 }
1696
1697 #[test]
1698 fn test_model_defaults() {
1699 assert_eq!(ModelId::default(), ModelId::Gemini25FlashPreview);
1700 assert_eq!(ModelId::default_orchestrator(), ModelId::Gemini25Pro);
1701 assert_eq!(ModelId::default_subagent(), ModelId::Gemini25FlashPreview);
1702 }
1703
1704 #[test]
1705 fn test_model_variants() {
1706 assert!(ModelId::Gemini25FlashPreview.is_flash_variant());
1708 assert!(ModelId::Gemini25Flash.is_flash_variant());
1709 assert!(ModelId::Gemini25FlashLite.is_flash_variant());
1710 assert!(!ModelId::GPT5.is_flash_variant());
1711 assert!(ModelId::ZaiGlm45Flash.is_flash_variant());
1712 assert!(ModelId::MoonshotKimiK2TurboPreview.is_flash_variant());
1713 assert!(ModelId::MoonshotKimiLatest8k.is_flash_variant());
1714
1715 assert!(ModelId::Gemini25Pro.is_pro_variant());
1717 assert!(ModelId::GPT5.is_pro_variant());
1718 assert!(ModelId::GPT5Codex.is_pro_variant());
1719 assert!(ModelId::DeepSeekReasoner.is_pro_variant());
1720 assert!(ModelId::ZaiGlm46.is_pro_variant());
1721 assert!(ModelId::MoonshotKimiK20905Preview.is_pro_variant());
1722 assert!(ModelId::MoonshotKimiLatest128k.is_pro_variant());
1723 assert!(!ModelId::Gemini25FlashPreview.is_pro_variant());
1724
1725 assert!(ModelId::Gemini25FlashPreview.is_efficient_variant());
1727 assert!(ModelId::Gemini25Flash.is_efficient_variant());
1728 assert!(ModelId::Gemini25FlashLite.is_efficient_variant());
1729 assert!(ModelId::GPT5Mini.is_efficient_variant());
1730 assert!(ModelId::ClaudeHaiku45.is_efficient_variant());
1731 assert!(ModelId::XaiGrok4Code.is_efficient_variant());
1732 assert!(ModelId::DeepSeekChat.is_efficient_variant());
1733 assert!(ModelId::ZaiGlm45Air.is_efficient_variant());
1734 assert!(ModelId::ZaiGlm45Airx.is_efficient_variant());
1735 assert!(ModelId::ZaiGlm45Flash.is_efficient_variant());
1736 assert!(ModelId::MoonshotKimiK2TurboPreview.is_efficient_variant());
1737 assert!(ModelId::MoonshotKimiLatest8k.is_efficient_variant());
1738 assert!(!ModelId::GPT5.is_efficient_variant());
1739
1740 for entry in openrouter_generated::ENTRIES {
1741 assert_eq!(entry.variant.is_efficient_variant(), entry.efficient);
1742 }
1743
1744 assert!(ModelId::Gemini25Pro.is_top_tier());
1746 assert!(ModelId::GPT5.is_top_tier());
1747 assert!(ModelId::GPT5Codex.is_top_tier());
1748 assert!(ModelId::ClaudeSonnet45.is_top_tier());
1749 assert!(ModelId::ClaudeSonnet4.is_top_tier());
1750 assert!(ModelId::XaiGrok4.is_top_tier());
1751 assert!(ModelId::XaiGrok4CodeLatest.is_top_tier());
1752 assert!(ModelId::DeepSeekReasoner.is_top_tier());
1753 assert!(ModelId::ZaiGlm46.is_top_tier());
1754 assert!(ModelId::MoonshotKimiK20905Preview.is_top_tier());
1755 assert!(ModelId::MoonshotKimiLatest128k.is_top_tier());
1756 assert!(!ModelId::Gemini25FlashPreview.is_top_tier());
1757 assert!(!ModelId::ClaudeHaiku45.is_top_tier());
1758
1759 for entry in openrouter_generated::ENTRIES {
1760 assert_eq!(entry.variant.is_top_tier(), entry.top_tier);
1761 }
1762 }
1763
1764 #[test]
1765 fn test_model_generation() {
1766 assert_eq!(ModelId::Gemini25FlashPreview.generation(), "2.5");
1768 assert_eq!(ModelId::Gemini25Flash.generation(), "2.5");
1769 assert_eq!(ModelId::Gemini25FlashLite.generation(), "2.5");
1770 assert_eq!(ModelId::Gemini25Pro.generation(), "2.5");
1771
1772 assert_eq!(ModelId::GPT5.generation(), "5");
1774 assert_eq!(ModelId::GPT5Codex.generation(), "5");
1775 assert_eq!(ModelId::GPT5Mini.generation(), "5");
1776 assert_eq!(ModelId::GPT5Nano.generation(), "5");
1777 assert_eq!(ModelId::CodexMiniLatest.generation(), "5");
1778
1779 assert_eq!(ModelId::ClaudeSonnet45.generation(), "4.5");
1781 assert_eq!(ModelId::ClaudeHaiku45.generation(), "4.5");
1782 assert_eq!(ModelId::ClaudeSonnet4.generation(), "4");
1783 assert_eq!(ModelId::ClaudeOpus41.generation(), "4.1");
1784
1785 assert_eq!(ModelId::DeepSeekChat.generation(), "V3.2-Exp");
1787 assert_eq!(ModelId::DeepSeekReasoner.generation(), "V3.2-Exp");
1788
1789 assert_eq!(ModelId::XaiGrok4.generation(), "4");
1791 assert_eq!(ModelId::XaiGrok4Mini.generation(), "4");
1792 assert_eq!(ModelId::XaiGrok4Code.generation(), "4");
1793 assert_eq!(ModelId::XaiGrok4CodeLatest.generation(), "4");
1794 assert_eq!(ModelId::XaiGrok4Vision.generation(), "4");
1795 assert_eq!(ModelId::ZaiGlm46.generation(), "4.6");
1797 assert_eq!(ModelId::ZaiGlm45.generation(), "4.5");
1798 assert_eq!(ModelId::ZaiGlm45Air.generation(), "4.5");
1799 assert_eq!(ModelId::ZaiGlm45X.generation(), "4.5");
1800 assert_eq!(ModelId::ZaiGlm45Airx.generation(), "4.5");
1801 assert_eq!(ModelId::ZaiGlm45Flash.generation(), "4.5");
1802 assert_eq!(ModelId::ZaiGlm432b0414128k.generation(), "4-32B");
1803 assert_eq!(ModelId::MoonshotKimiK2TurboPreview.generation(), "k2");
1804 assert_eq!(ModelId::MoonshotKimiK20905Preview.generation(), "k2");
1805 assert_eq!(ModelId::MoonshotKimiK20711Preview.generation(), "k2");
1806 assert_eq!(ModelId::MoonshotKimiLatest.generation(), "latest");
1807 assert_eq!(ModelId::MoonshotKimiLatest8k.generation(), "latest");
1808 assert_eq!(ModelId::MoonshotKimiLatest32k.generation(), "latest");
1809 assert_eq!(ModelId::MoonshotKimiLatest128k.generation(), "latest");
1810 assert_eq!(
1811 ModelId::LmStudioMetaLlama38BInstruct.generation(),
1812 "meta-llama-3"
1813 );
1814 assert_eq!(
1815 ModelId::LmStudioMetaLlama318BInstruct.generation(),
1816 "meta-llama-3.1"
1817 );
1818 assert_eq!(ModelId::LmStudioQwen257BInstruct.generation(), "qwen2.5");
1819 assert_eq!(ModelId::LmStudioGemma22BIt.generation(), "gemma-2");
1820 assert_eq!(ModelId::LmStudioGemma29BIt.generation(), "gemma-2");
1821 assert_eq!(ModelId::LmStudioPhi31Mini4kInstruct.generation(), "phi-3.1");
1822
1823 for entry in openrouter_generated::ENTRIES {
1824 assert_eq!(entry.variant.generation(), entry.generation);
1825 }
1826 }
1827
1828 #[test]
1829 fn test_models_for_provider() {
1830 let gemini_models = ModelId::models_for_provider(Provider::Gemini);
1831 assert!(gemini_models.contains(&ModelId::Gemini25Pro));
1832 assert!(!gemini_models.contains(&ModelId::GPT5));
1833
1834 let openai_models = ModelId::models_for_provider(Provider::OpenAI);
1835 assert!(openai_models.contains(&ModelId::GPT5));
1836 assert!(openai_models.contains(&ModelId::GPT5Codex));
1837 assert!(!openai_models.contains(&ModelId::Gemini25Pro));
1838
1839 let anthropic_models = ModelId::models_for_provider(Provider::Anthropic);
1840 assert!(anthropic_models.contains(&ModelId::ClaudeSonnet45));
1841 assert!(anthropic_models.contains(&ModelId::ClaudeHaiku45));
1842 assert!(anthropic_models.contains(&ModelId::ClaudeSonnet4));
1843 assert!(!anthropic_models.contains(&ModelId::GPT5));
1844
1845 let deepseek_models = ModelId::models_for_provider(Provider::DeepSeek);
1846 assert!(deepseek_models.contains(&ModelId::DeepSeekChat));
1847 assert!(deepseek_models.contains(&ModelId::DeepSeekReasoner));
1848
1849 let openrouter_models = ModelId::models_for_provider(Provider::OpenRouter);
1850 for entry in openrouter_generated::ENTRIES {
1851 assert!(openrouter_models.contains(&entry.variant));
1852 }
1853
1854 let xai_models = ModelId::models_for_provider(Provider::XAI);
1855 assert!(xai_models.contains(&ModelId::XaiGrok4));
1856 assert!(xai_models.contains(&ModelId::XaiGrok4Mini));
1857 assert!(xai_models.contains(&ModelId::XaiGrok4Code));
1858 assert!(xai_models.contains(&ModelId::XaiGrok4CodeLatest));
1859 assert!(xai_models.contains(&ModelId::XaiGrok4Vision));
1860
1861 let zai_models = ModelId::models_for_provider(Provider::ZAI);
1862 assert!(zai_models.contains(&ModelId::ZaiGlm46));
1863 assert!(zai_models.contains(&ModelId::ZaiGlm45));
1864 assert!(zai_models.contains(&ModelId::ZaiGlm45Air));
1865 assert!(zai_models.contains(&ModelId::ZaiGlm45X));
1866 assert!(zai_models.contains(&ModelId::ZaiGlm45Airx));
1867 assert!(zai_models.contains(&ModelId::ZaiGlm45Flash));
1868 assert!(zai_models.contains(&ModelId::ZaiGlm432b0414128k));
1869
1870 let moonshot_models = ModelId::models_for_provider(Provider::Moonshot);
1871 assert!(moonshot_models.contains(&ModelId::MoonshotKimiK2TurboPreview));
1872 assert!(moonshot_models.contains(&ModelId::MoonshotKimiK2Thinking));
1873 assert!(moonshot_models.contains(&ModelId::MoonshotKimiK20905Preview));
1874 assert!(moonshot_models.contains(&ModelId::MoonshotKimiK20711Preview));
1875 assert!(moonshot_models.contains(&ModelId::MoonshotKimiLatest));
1876 assert!(moonshot_models.contains(&ModelId::MoonshotKimiLatest8k));
1877 assert!(moonshot_models.contains(&ModelId::MoonshotKimiLatest32k));
1878 assert!(moonshot_models.contains(&ModelId::MoonshotKimiLatest128k));
1879 assert_eq!(moonshot_models.len(), 8);
1880
1881 let ollama_models = ModelId::models_for_provider(Provider::Ollama);
1882 assert!(ollama_models.contains(&ModelId::OllamaGptOss20b));
1883 assert!(ollama_models.contains(&ModelId::OllamaGptOss20bCloud));
1884 assert!(ollama_models.contains(&ModelId::OllamaGptOss120bCloud));
1885 assert!(ollama_models.contains(&ModelId::OllamaQwen317b));
1886 assert!(ollama_models.contains(&ModelId::OllamaDeepseekV31671bCloud));
1887
1888 assert!(ollama_models.contains(&ModelId::OllamaQwen3Coder480bCloud));
1889 assert!(ollama_models.contains(&ModelId::OllamaGlm46Cloud));
1890 assert!(ollama_models.contains(&ModelId::OllamaMinimaxM2Cloud));
1891 assert_eq!(ollama_models.len(), 9); let lmstudio_models = ModelId::models_for_provider(Provider::LmStudio);
1894 assert!(lmstudio_models.contains(&ModelId::LmStudioMetaLlama38BInstruct));
1895 assert!(lmstudio_models.contains(&ModelId::LmStudioMetaLlama318BInstruct));
1896 assert!(lmstudio_models.contains(&ModelId::LmStudioQwen257BInstruct));
1897 assert!(lmstudio_models.contains(&ModelId::LmStudioGemma22BIt));
1898 assert!(lmstudio_models.contains(&ModelId::LmStudioGemma29BIt));
1899 assert!(lmstudio_models.contains(&ModelId::LmStudioPhi31Mini4kInstruct));
1900 assert_eq!(lmstudio_models.len(), 6);
1901 }
1902
1903 #[test]
1904 fn test_ollama_cloud_models() {
1905 use crate::constants::models;
1906
1907 let model_pairs = vec![
1909 (
1910 ModelId::OllamaGptOss20bCloud,
1911 models::ollama::GPT_OSS_20B_CLOUD,
1912 ),
1913 (
1914 ModelId::OllamaGptOss120bCloud,
1915 models::ollama::GPT_OSS_120B_CLOUD,
1916 ),
1917 (
1918 ModelId::OllamaDeepseekV31671bCloud,
1919 models::ollama::DEEPSEEK_V31_671B_CLOUD,
1920 ),
1921 (
1922 ModelId::OllamaKimiK21tCloud,
1923 models::ollama::KIMI_K2_1T_CLOUD,
1924 ),
1925 (
1926 ModelId::OllamaQwen3Coder480bCloud,
1927 models::ollama::QWEN3_CODER_480B_CLOUD,
1928 ),
1929 (ModelId::OllamaGlm46Cloud, models::ollama::GLM_46_CLOUD),
1930 (
1931 ModelId::OllamaMinimaxM2Cloud,
1932 models::ollama::MINIMAX_M2_CLOUD,
1933 ),
1934 ];
1935
1936 for (model_id, expected_str) in model_pairs {
1937 assert_eq!(model_id.as_str(), expected_str);
1938 assert_eq!(ModelId::from_str(expected_str).unwrap(), model_id);
1939 assert_eq!(model_id.provider(), Provider::Ollama);
1940
1941 assert!(!model_id.display_name().is_empty());
1943
1944 assert!(!model_id.description().is_empty());
1946
1947 assert!(!model_id.generation().is_empty());
1949 }
1950 }
1951
1952 #[test]
1953 fn test_fallback_models() {
1954 let fallbacks = ModelId::fallback_models();
1955 assert!(!fallbacks.is_empty());
1956 assert!(fallbacks.contains(&ModelId::Gemini25Pro));
1957 assert!(fallbacks.contains(&ModelId::GPT5));
1958 assert!(fallbacks.contains(&ModelId::ClaudeOpus41));
1959 assert!(fallbacks.contains(&ModelId::ClaudeSonnet45));
1960 assert!(fallbacks.contains(&ModelId::DeepSeekReasoner));
1961 assert!(fallbacks.contains(&ModelId::MoonshotKimiK20905Preview));
1962 assert!(fallbacks.contains(&ModelId::XaiGrok4));
1963 assert!(fallbacks.contains(&ModelId::ZaiGlm46));
1964 assert!(fallbacks.contains(&ModelId::OpenRouterGrokCodeFast1));
1965 }
1966}