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