Skip to main content

vtcode_commons/
provider.rs

1//! Provider enum and core definitions shared across VT Code crates.
2//!
3//! This module provides the [`Provider`] enum and its pure methods. Methods
4//! that depend on model catalogs or vtcode-config internals
5//! (`supports_reasoning_effort`, `supports_service_tier`) remain in
6//! `vtcode-config` as extension methods.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12/// Error returned when parsing a provider string fails.
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum ProviderParseError {
15    #[error("Invalid provider: '{}'. Supported providers: {}", .0, crate::provider::Provider::all_providers().iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "))]
16    InvalidProvider(String),
17}
18
19/// Supported AI model providers
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
22pub enum Provider {
23    /// Google Gemini models
24    Gemini,
25    /// OpenAI GPT models
26    #[default]
27    OpenAI,
28    /// Anthropic Claude models
29    Anthropic,
30    /// GitHub Copilot preview integration
31    Copilot,
32    /// DeepSeek native models
33    DeepSeek,
34    /// Official Meta AI Muse models
35    Meta,
36    /// OpenRouter marketplace models
37    OpenRouter,
38    /// Local Ollama models
39    Ollama,
40    /// Ollama Cloud hosted models (ollama.com)
41    OllamaCloud,
42    /// LM Studio local models
43    LmStudio,
44    /// llama.cpp local models
45    LlamaCpp,
46    /// Moonshot.ai models
47    Moonshot,
48    /// Z.AI GLM models
49    ZAI,
50    /// MiniMax models
51    Minimax,
52    /// Xiaomi MiMo models
53    MiMo,
54    /// Mistral AI models
55    Mistral,
56    /// Hugging Face Inference Providers
57    HuggingFace,
58    /// OpenCode Zen gateway (pay-as-you-go)
59    OpenCodeZen,
60    /// OpenCode Go subscription
61    OpenCodeGo,
62    /// Alibaba Cloud Qwen models
63    Qwen,
64    /// StepFun models
65    StepFun,
66    /// Evolink multi-model gateway
67    Evolink,
68    /// Poolside AI models
69    Poolside,
70    /// xAI Grok models
71    XAI,
72    /// NVIDIA NIM OpenAI-compatible models
73    NVIDIA,
74    /// Merge Gateway multi-provider routing gateway
75    MergeGateway,
76    /// Vercel AI Gateway multi-provider routing gateway
77    Vercel,
78}
79
80impl Provider {
81    /// Case-insensitive canonical name match against this provider's
82    /// accepted spellings. Single source of truth for string-matching call
83    /// sites; exhaustively matched so a new variant forces the alias list
84    /// to be written (a silent `false` would be drift).
85    pub fn name_matches(&self, candidate: &str) -> bool {
86        let normalized = candidate.to_lowercase();
87        self.aliases().contains(&normalized.as_str())
88    }
89
90    /// Accepted lowercase spellings of this provider. In lockstep with
91    /// `FromStr::from_str` and `AsRef::as_ref`; both must stay inside this
92    /// list so a parse alias is always also a match alias.
93    fn aliases(self) -> &'static [&'static str] {
94        match self {
95            Provider::Gemini => &["gemini", "google"],
96            Provider::OpenAI => &["openai"],
97            Provider::Anthropic => &["anthropic"],
98            Provider::Copilot => &["copilot"],
99            Provider::DeepSeek => &["deepseek"],
100            Provider::Meta => &["meta", "meta-ai"],
101            Provider::OpenRouter => &["openrouter"],
102            Provider::Ollama => &["ollama"],
103            Provider::OllamaCloud => &["ollama-cloud", "ollamacloud", "ollama_cloud"],
104            Provider::LmStudio => &["lmstudio"],
105            Provider::LlamaCpp => &["llamacpp", "llama.cpp", "llama-cpp"],
106            Provider::Moonshot => &["moonshot"],
107            Provider::ZAI => &["zai"],
108            Provider::Minimax => &["minimax"],
109            Provider::MiMo => &["mimo"],
110            Provider::Mistral => &["mistral"],
111            Provider::HuggingFace => &["huggingface"],
112            Provider::OpenCodeZen => &["opencode-zen", "opencodezen"],
113            Provider::OpenCodeGo => &["opencode-go", "opencodego"],
114            Provider::Qwen => &["qwen"],
115            Provider::StepFun => &["stepfun"],
116            Provider::Evolink => &["evolink"],
117            Provider::Poolside => &["poolside"],
118            Provider::XAI => &["xai"],
119            Provider::NVIDIA => &["nvidia", "nvidia-nim"],
120            Provider::MergeGateway => &["merge-gateway", "merge_gateway", "mergegateway"],
121            Provider::Vercel => &["vercel", "vercel-ai-gateway", "ai-gateway"],
122        }
123    }
124
125    /// Get the default API key environment variable for this provider
126    pub fn default_api_key_env(&self) -> &'static str {
127        match self {
128            Provider::Gemini => "GEMINI_API_KEY",
129            Provider::OpenAI => "OPENAI_API_KEY",
130            Provider::Anthropic => "ANTHROPIC_API_KEY",
131            Provider::Copilot => "",
132            Provider::DeepSeek => "DEEPSEEK_API_KEY",
133            Provider::Meta => "META_API_KEY",
134            Provider::OpenRouter => "OPENROUTER_API_KEY",
135            Provider::Ollama => "OLLAMA_API_KEY",
136            Provider::OllamaCloud => "OLLAMA_API_KEY",
137            Provider::LmStudio => "LMSTUDIO_API_KEY",
138            Provider::LlamaCpp => "LLAMACPP_API_KEY",
139            Provider::Moonshot => "MOONSHOT_API_KEY",
140            Provider::ZAI => "ZAI_API_KEY",
141            Provider::Minimax => "MINIMAX_API_KEY",
142            Provider::MiMo => "MIMO_API_KEY",
143            Provider::Mistral => "MISTRAL_API_KEY",
144            Provider::HuggingFace => "HF_TOKEN",
145            Provider::OpenCodeZen => "OPENCODE_ZEN_API_KEY",
146            Provider::OpenCodeGo => "OPENCODE_GO_API_KEY",
147            Provider::Qwen => "QWEN_API_KEY",
148            Provider::StepFun => "STEPFUN_API_KEY",
149            Provider::Evolink => "EVOLINK_API_KEY",
150            Provider::Poolside => "POOLSIDE_API_KEY",
151            Provider::XAI => "XAI_API_KEY",
152            Provider::NVIDIA => "NVIDIA_API_KEY",
153            Provider::MergeGateway => "MERGE_GATEWAY_API_KEY",
154            Provider::Vercel => "AI_GATEWAY_API_KEY",
155        }
156    }
157
158    /// Get all supported providers
159    pub fn all_providers() -> Vec<Provider> {
160        vec![
161            Provider::OpenAI,
162            Provider::Anthropic,
163            Provider::Copilot,
164            Provider::Minimax,
165            Provider::MiMo,
166            Provider::Mistral,
167            Provider::Gemini,
168            Provider::DeepSeek,
169            Provider::Meta,
170            Provider::HuggingFace,
171            Provider::OpenRouter,
172            Provider::Ollama,
173            Provider::OllamaCloud,
174            Provider::LmStudio,
175            Provider::LlamaCpp,
176            Provider::Moonshot,
177            Provider::ZAI,
178            Provider::OpenCodeZen,
179            Provider::OpenCodeGo,
180            Provider::Qwen,
181            Provider::StepFun,
182            Provider::Evolink,
183            Provider::Poolside,
184            Provider::XAI,
185            Provider::NVIDIA,
186            Provider::MergeGateway,
187            Provider::Vercel,
188        ]
189    }
190
191    /// Human-friendly label for display purposes
192    pub fn label(&self) -> &'static str {
193        match self {
194            Provider::Gemini => "Gemini",
195            Provider::OpenAI => "OpenAI",
196            Provider::Anthropic => "Anthropic",
197            Provider::Copilot => "GitHub Copilot",
198            Provider::DeepSeek => "DeepSeek",
199            Provider::Meta => "Meta AI",
200            Provider::OpenRouter => "OpenRouter",
201            Provider::Ollama => "Ollama",
202            Provider::OllamaCloud => "Ollama Cloud",
203            Provider::LmStudio => "LM Studio",
204            Provider::LlamaCpp => "llama.cpp",
205            Provider::Moonshot => "Moonshot",
206            Provider::ZAI => "Z.AI",
207            Provider::Minimax => "MiniMax",
208            Provider::MiMo => "Xiaomi MiMo",
209            Provider::Mistral => "Mistral",
210            Provider::HuggingFace => "Hugging Face",
211            Provider::OpenCodeZen => "OpenCode Zen",
212            Provider::OpenCodeGo => "OpenCode Go",
213            Provider::Qwen => "Qwen",
214            Provider::StepFun => "StepFun",
215            Provider::Evolink => "Evolink",
216            Provider::Poolside => "Poolside",
217            Provider::XAI => "xAI",
218            Provider::NVIDIA => "NVIDIA",
219            Provider::MergeGateway => "Merge Gateway",
220            Provider::Vercel => "Vercel AI Gateway",
221        }
222    }
223
224    pub fn is_dynamic(&self) -> bool {
225        matches!(self, Provider::Copilot) || self.is_local()
226    }
227
228    pub fn is_local(&self) -> bool {
229        matches!(self, Provider::Ollama | Provider::LmStudio | Provider::LlamaCpp)
230    }
231
232    pub fn local_install_instructions(&self) -> Option<&'static str> {
233        match self {
234            Provider::Ollama => Some(
235                "Ollama server is not running. To start:\n  1. Install Ollama from https://ollama.com\n  2. Run 'ollama serve' in a terminal\n  3. Pull models using 'ollama pull <model-name>' (e.g., 'ollama pull gpt-oss:20b')",
236            ),
237            Provider::LmStudio => Some(
238                "LM Studio server is not running. To start:\n  1. Install LM Studio from https://lmstudio.ai\n  2. Open LM Studio and start the Local Server on port 1234\n  3. Load the model you want to use",
239            ),
240            Provider::LlamaCpp => Some(
241                "llama.cpp server is not running. To start:\n  1. Install llama.cpp from https://llama.app or your package manager\n  2. Run 'llama-server -m /path/to/model.gguf --port 8080'\n  3. Keep the server running while VT Code connects",
242            ),
243            _ => None,
244        }
245    }
246
247    pub fn uses_managed_auth(&self) -> bool {
248        matches!(self, Provider::Copilot)
249    }
250
251    pub fn platform_url(&self) -> Option<&'static str> {
252        match self {
253            Provider::Gemini => Some("https://aistudio.google.com/app/apikey"),
254            Provider::OpenAI => Some("https://platform.openai.com/api-keys"),
255            Provider::Anthropic => Some("https://console.anthropic.com"),
256            Provider::Copilot => None,
257            Provider::DeepSeek => Some("https://platform.deepseek.com"),
258            Provider::Meta => Some("https://developer.meta.com/ai/models"),
259            Provider::OpenRouter => Some("https://openrouter.ai/api/v1/auth/keys"),
260            Provider::Ollama => None,
261            Provider::OllamaCloud => Some("https://ollama.com"),
262            Provider::LmStudio => None,
263            Provider::LlamaCpp => None,
264            Provider::Moonshot => Some("https://platform.kimi.ai/console/api-keys"),
265            Provider::ZAI => Some("https://z.ai/docs"),
266            Provider::Minimax => Some("https://platform.minimax.io/docs"),
267            Provider::MiMo => Some("https://platform.xiaomimimo.com/docs/en-US/welcome"),
268            Provider::Mistral => Some("https://console.mistral.ai"),
269            Provider::HuggingFace => Some("https://huggingface.co/settings/tokens"),
270            Provider::OpenCodeZen => Some("https://opencode.ai/docs/zen/"),
271            Provider::OpenCodeGo => Some("https://opencode.ai/docs/go/"),
272            Provider::Qwen => Some("https://dashscope.console.aliyun.com"),
273            Provider::StepFun => Some("https://platform.stepfun.com"),
274            Provider::Evolink => Some("https://evolink.ai/dashboard/keys"),
275            Provider::Poolside => Some("https://poolside.ai"),
276            Provider::XAI => Some("https://docs.x.ai"),
277            Provider::NVIDIA => Some("https://build.nvidia.com"),
278            Provider::MergeGateway => Some("https://dashboard.merge.dev"),
279            Provider::Vercel => Some("https://vercel.com/docs/ai-gateway/getting-started"),
280        }
281    }
282}
283
284impl fmt::Display for Provider {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        match self {
287            Provider::Gemini => write!(f, "gemini"),
288            Provider::OpenAI => write!(f, "openai"),
289            Provider::Anthropic => write!(f, "anthropic"),
290            Provider::Copilot => write!(f, "copilot"),
291            Provider::DeepSeek => write!(f, "deepseek"),
292            Provider::Meta => write!(f, "meta"),
293            Provider::OpenRouter => write!(f, "openrouter"),
294            Provider::Ollama => write!(f, "ollama"),
295            Provider::OllamaCloud => write!(f, "ollama-cloud"),
296            Provider::LmStudio => write!(f, "lmstudio"),
297            Provider::LlamaCpp => write!(f, "llamacpp"),
298            Provider::Moonshot => write!(f, "moonshot"),
299            Provider::ZAI => write!(f, "zai"),
300            Provider::Minimax => write!(f, "minimax"),
301            Provider::MiMo => write!(f, "mimo"),
302            Provider::Mistral => write!(f, "mistral"),
303            Provider::HuggingFace => write!(f, "huggingface"),
304            Provider::OpenCodeZen => write!(f, "opencode-zen"),
305            Provider::OpenCodeGo => write!(f, "opencode-go"),
306            Provider::Qwen => write!(f, "qwen"),
307            Provider::StepFun => write!(f, "stepfun"),
308            Provider::Evolink => write!(f, "evolink"),
309            Provider::Poolside => write!(f, "poolside"),
310            Provider::XAI => write!(f, "xai"),
311            Provider::NVIDIA => write!(f, "nvidia"),
312            Provider::MergeGateway => write!(f, "merge-gateway"),
313            Provider::Vercel => write!(f, "vercel"),
314        }
315    }
316}
317
318impl AsRef<str> for Provider {
319    fn as_ref(&self) -> &str {
320        match self {
321            Provider::Gemini => "gemini",
322            Provider::OpenAI => "openai",
323            Provider::Anthropic => "anthropic",
324            Provider::Copilot => "copilot",
325            Provider::DeepSeek => "deepseek",
326            Provider::Meta => "meta",
327            Provider::OpenRouter => "openrouter",
328            Provider::Ollama => "ollama",
329            Provider::OllamaCloud => "ollama-cloud",
330            Provider::LmStudio => "lmstudio",
331            Provider::LlamaCpp => "llamacpp",
332            Provider::Moonshot => "moonshot",
333            Provider::ZAI => "zai",
334            Provider::Minimax => "minimax",
335            Provider::MiMo => "mimo",
336            Provider::Mistral => "mistral",
337            Provider::HuggingFace => "huggingface",
338            Provider::OpenCodeZen => "opencode-zen",
339            Provider::OpenCodeGo => "opencode-go",
340            Provider::Qwen => "qwen",
341            Provider::StepFun => "stepfun",
342            Provider::Evolink => "evolink",
343            Provider::Poolside => "poolside",
344            Provider::XAI => "xai",
345            Provider::NVIDIA => "nvidia",
346            Provider::MergeGateway => "merge-gateway",
347            Provider::Vercel => "vercel",
348        }
349    }
350}
351
352impl FromStr for Provider {
353    type Err = ProviderParseError;
354
355    fn from_str(s: &str) -> Result<Self, Self::Err> {
356        match s.to_lowercase().as_str() {
357            "gemini" | "google" => Ok(Provider::Gemini),
358            "openai" => Ok(Provider::OpenAI),
359            "anthropic" => Ok(Provider::Anthropic),
360            "copilot" => Ok(Provider::Copilot),
361            "deepseek" => Ok(Provider::DeepSeek),
362            "meta" | "meta-ai" => Ok(Provider::Meta),
363            "openrouter" => Ok(Provider::OpenRouter),
364            "ollama" => Ok(Provider::Ollama),
365            "ollama-cloud" | "ollamacloud" | "ollama_cloud" => Ok(Provider::OllamaCloud),
366            "lmstudio" => Ok(Provider::LmStudio),
367            "llamacpp" | "llama.cpp" | "llama-cpp" => Ok(Provider::LlamaCpp),
368            "moonshot" => Ok(Provider::Moonshot),
369            "zai" => Ok(Provider::ZAI),
370            "minimax" => Ok(Provider::Minimax),
371            "mimo" => Ok(Provider::MiMo),
372            "mistral" => Ok(Provider::Mistral),
373            "huggingface" => Ok(Provider::HuggingFace),
374            "opencode-zen" | "opencodezen" => Ok(Provider::OpenCodeZen),
375            "opencode-go" | "opencodego" => Ok(Provider::OpenCodeGo),
376            "qwen" => Ok(Provider::Qwen),
377            "stepfun" => Ok(Provider::StepFun),
378            "evolink" => Ok(Provider::Evolink),
379            "poolside" => Ok(Provider::Poolside),
380            "xai" => Ok(Provider::XAI),
381            "nvidia" | "nvidia-nim" => Ok(Provider::NVIDIA),
382            "merge-gateway" | "merge_gateway" | "mergegateway" => Ok(Provider::MergeGateway),
383            "vercel" | "vercel-ai-gateway" | "ai-gateway" => Ok(Provider::Vercel),
384            _ => Err(ProviderParseError::InvalidProvider(s.to_string())),
385        }
386    }
387}
388
389#[cfg(test)]
390mod name_match_tests {
391    use super::*;
392
393    /// Test-only alias table mirroring `FromStr::from_str`. Kept here (not in
394    /// production) so an edit on either side trips exactly one asymmetric
395    /// assertion instead of two dependent ones.
396    const PARSE_ALIASES: &[(&str, Provider)] = &[
397        ("gemini", Provider::Gemini),
398        ("google", Provider::Gemini),
399        ("openai", Provider::OpenAI),
400        ("anthropic", Provider::Anthropic),
401        ("copilot", Provider::Copilot),
402        ("deepseek", Provider::DeepSeek),
403        ("meta", Provider::Meta),
404        ("meta-ai", Provider::Meta),
405        ("openrouter", Provider::OpenRouter),
406        ("ollama", Provider::Ollama),
407        ("ollama-cloud", Provider::OllamaCloud),
408        ("ollamacloud", Provider::OllamaCloud),
409        ("ollama_cloud", Provider::OllamaCloud),
410        ("lmstudio", Provider::LmStudio),
411        ("llamacpp", Provider::LlamaCpp),
412        ("llama.cpp", Provider::LlamaCpp),
413        ("llama-cpp", Provider::LlamaCpp),
414        ("moonshot", Provider::Moonshot),
415        ("zai", Provider::ZAI),
416        ("minimax", Provider::Minimax),
417        ("mimo", Provider::MiMo),
418        ("mistral", Provider::Mistral),
419        ("huggingface", Provider::HuggingFace),
420        ("opencode-zen", Provider::OpenCodeZen),
421        ("opencodezen", Provider::OpenCodeZen),
422        ("opencode-go", Provider::OpenCodeGo),
423        ("opencodego", Provider::OpenCodeGo),
424        ("qwen", Provider::Qwen),
425        ("stepfun", Provider::StepFun),
426        ("evolink", Provider::Evolink),
427        ("poolside", Provider::Poolside),
428        ("xai", Provider::XAI),
429        ("nvidia", Provider::NVIDIA),
430        ("nvidia-nim", Provider::NVIDIA),
431        ("merge-gateway", Provider::MergeGateway),
432        ("merge_gateway", Provider::MergeGateway),
433        ("mergegateway", Provider::MergeGateway),
434        ("vercel", Provider::Vercel),
435        ("vercel-ai-gateway", Provider::Vercel),
436        ("ai-gateway", Provider::Vercel),
437    ];
438
439    /// Three-way lockstep drift guard: every parse alias must be accepted by
440    /// `name_matches`, and `name_matches` must never accept a spelling the
441    /// parser rejects. A one-sided edit on either hand-written table fails
442    /// here with the offending alias named.
443    #[test]
444    fn parse_aliases_and_name_matches_stay_in_lockstep() {
445        for (alias, provider) in PARSE_ALIASES {
446            assert!(provider.name_matches(alias), "{provider:?} does not match its parse alias {alias:?}");
447            assert_eq!(Provider::from_str(alias).ok(), Some(*provider), "test table drifted from from_str: {alias:?}");
448        }
449    }
450
451    /// Canonical `as_ref` identity must always match (the exact string this
452    /// crate itself emits must never be rejected by callers keyed on it).
453    #[test]
454    fn as_ref_identity_always_matches() {
455        for provider in Provider::all_providers() {
456            let canonical = provider.as_ref();
457            assert!(provider.name_matches(canonical), "{canonical} must match itself");
458        }
459    }
460
461    /// Asymmetric both directions: case-insensitivity works, and unknown
462    /// spellings never match — including cross-provider confusions.
463    #[test]
464    fn case_insensitive_matches_and_unknowns_reject() {
465        assert!(Provider::Anthropic.name_matches("Anthropic"));
466        assert!(Provider::MergeGateway.name_matches("MERGE-GATEWAY"));
467        assert!(!Provider::Anthropic.name_matches("openai"));
468        assert!(!Provider::OpenAI.name_matches("OpenAICompat"));
469        assert!(!Provider::Anthropic.name_matches(""));
470    }
471}