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