1use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12#[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
22pub enum Provider {
23 Gemini,
25 #[default]
27 OpenAI,
28 Anthropic,
30 Copilot,
32 DeepSeek,
34 Meta,
36 OpenRouter,
38 Ollama,
40 OllamaCloud,
42 LmStudio,
44 LlamaCpp,
46 Moonshot,
48 ZAI,
50 Minimax,
52 MiMo,
54 Mistral,
56 HuggingFace,
58 OpenCodeZen,
60 OpenCodeGo,
62 Qwen,
64 StepFun,
66 Evolink,
68 Poolside,
70 XAI,
72 NVIDIA,
74 MergeGateway,
76 Vercel,
78}
79
80impl Provider {
81 pub fn name_matches(&self, candidate: &str) -> bool {
86 let normalized = candidate.to_lowercase();
87 self.aliases().contains(&normalized.as_str())
88 }
89
90 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 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 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 pub fn picker_display_order() -> Vec<Provider> {
199 const PINNED_HEAD: &[Provider] = &[
200 Provider::OpenAI,
201 Provider::Anthropic,
202 Provider::HuggingFace,
203 Provider::Ollama,
204 Provider::OllamaCloud,
205 Provider::LmStudio,
206 Provider::LlamaCpp,
207 ];
208 let mut order = PINNED_HEAD.to_vec();
209 order.extend(
210 Self::all_providers()
211 .into_iter()
212 .filter(|provider| !PINNED_HEAD.contains(provider)),
213 );
214 order
215 }
216
217 pub fn label(&self) -> &'static str {
219 match self {
220 Provider::Gemini => "Gemini",
221 Provider::OpenAI => "OpenAI",
222 Provider::Anthropic => "Anthropic",
223 Provider::Copilot => "GitHub Copilot",
224 Provider::DeepSeek => "DeepSeek",
225 Provider::Meta => "Meta AI",
226 Provider::OpenRouter => "OpenRouter",
227 Provider::Ollama => "Ollama",
228 Provider::OllamaCloud => "Ollama Cloud",
229 Provider::LmStudio => "LM Studio",
230 Provider::LlamaCpp => "llama.cpp",
231 Provider::Moonshot => "Moonshot",
232 Provider::ZAI => "Z.AI",
233 Provider::Minimax => "MiniMax",
234 Provider::MiMo => "Xiaomi MiMo",
235 Provider::Mistral => "Mistral",
236 Provider::HuggingFace => "Hugging Face",
237 Provider::OpenCodeZen => "OpenCode Zen",
238 Provider::OpenCodeGo => "OpenCode Go",
239 Provider::Qwen => "Qwen",
240 Provider::StepFun => "StepFun",
241 Provider::Evolink => "Evolink",
242 Provider::Poolside => "Poolside",
243 Provider::XAI => "xAI",
244 Provider::NVIDIA => "NVIDIA",
245 Provider::MergeGateway => "Merge Gateway",
246 Provider::Vercel => "Vercel AI Gateway",
247 }
248 }
249
250 pub fn is_dynamic(&self) -> bool {
251 matches!(self, Provider::Copilot) || self.is_local()
252 }
253
254 pub fn is_local(&self) -> bool {
255 matches!(self, Provider::Ollama | Provider::LmStudio | Provider::LlamaCpp)
256 }
257
258 pub fn local_install_instructions(&self) -> Option<&'static str> {
259 match self {
260 Provider::Ollama => Some(
261 "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')",
262 ),
263 Provider::LmStudio => Some(
264 "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",
265 ),
266 Provider::LlamaCpp => Some(
267 "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",
268 ),
269 _ => None,
270 }
271 }
272
273 pub fn uses_managed_auth(&self) -> bool {
274 matches!(self, Provider::Copilot)
275 }
276
277 pub fn platform_url(&self) -> Option<&'static str> {
278 match self {
279 Provider::Gemini => Some("https://aistudio.google.com/app/apikey"),
280 Provider::OpenAI => Some("https://platform.openai.com/api-keys"),
281 Provider::Anthropic => Some("https://console.anthropic.com"),
282 Provider::Copilot => None,
283 Provider::DeepSeek => Some("https://platform.deepseek.com"),
284 Provider::Meta => Some("https://developer.meta.com/ai/models"),
285 Provider::OpenRouter => Some("https://openrouter.ai/api/v1/auth/keys"),
286 Provider::Ollama => None,
287 Provider::OllamaCloud => Some("https://ollama.com"),
288 Provider::LmStudio => None,
289 Provider::LlamaCpp => None,
290 Provider::Moonshot => Some("https://platform.kimi.ai/console/api-keys"),
291 Provider::ZAI => Some("https://z.ai/docs"),
292 Provider::Minimax => Some("https://platform.minimax.io/docs"),
293 Provider::MiMo => Some("https://platform.xiaomimimo.com/docs/en-US/welcome"),
294 Provider::Mistral => Some("https://console.mistral.ai"),
295 Provider::HuggingFace => Some("https://huggingface.co/settings/tokens"),
296 Provider::OpenCodeZen => Some("https://opencode.ai/docs/zen/"),
297 Provider::OpenCodeGo => Some("https://opencode.ai/docs/go/"),
298 Provider::Qwen => Some("https://dashscope.console.aliyun.com"),
299 Provider::StepFun => Some("https://platform.stepfun.com"),
300 Provider::Evolink => Some("https://evolink.ai/dashboard/keys"),
301 Provider::Poolside => Some("https://poolside.ai"),
302 Provider::XAI => Some("https://docs.x.ai"),
303 Provider::NVIDIA => Some("https://build.nvidia.com"),
304 Provider::MergeGateway => Some("https://dashboard.merge.dev"),
305 Provider::Vercel => Some("https://vercel.com/docs/ai-gateway/getting-started"),
306 }
307 }
308}
309
310impl fmt::Display for Provider {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 match self {
313 Provider::Gemini => write!(f, "gemini"),
314 Provider::OpenAI => write!(f, "openai"),
315 Provider::Anthropic => write!(f, "anthropic"),
316 Provider::Copilot => write!(f, "copilot"),
317 Provider::DeepSeek => write!(f, "deepseek"),
318 Provider::Meta => write!(f, "meta"),
319 Provider::OpenRouter => write!(f, "openrouter"),
320 Provider::Ollama => write!(f, "ollama"),
321 Provider::OllamaCloud => write!(f, "ollama-cloud"),
322 Provider::LmStudio => write!(f, "lmstudio"),
323 Provider::LlamaCpp => write!(f, "llamacpp"),
324 Provider::Moonshot => write!(f, "moonshot"),
325 Provider::ZAI => write!(f, "zai"),
326 Provider::Minimax => write!(f, "minimax"),
327 Provider::MiMo => write!(f, "mimo"),
328 Provider::Mistral => write!(f, "mistral"),
329 Provider::HuggingFace => write!(f, "huggingface"),
330 Provider::OpenCodeZen => write!(f, "opencode-zen"),
331 Provider::OpenCodeGo => write!(f, "opencode-go"),
332 Provider::Qwen => write!(f, "qwen"),
333 Provider::StepFun => write!(f, "stepfun"),
334 Provider::Evolink => write!(f, "evolink"),
335 Provider::Poolside => write!(f, "poolside"),
336 Provider::XAI => write!(f, "xai"),
337 Provider::NVIDIA => write!(f, "nvidia"),
338 Provider::MergeGateway => write!(f, "merge-gateway"),
339 Provider::Vercel => write!(f, "vercel"),
340 }
341 }
342}
343
344impl AsRef<str> for Provider {
345 fn as_ref(&self) -> &str {
346 match self {
347 Provider::Gemini => "gemini",
348 Provider::OpenAI => "openai",
349 Provider::Anthropic => "anthropic",
350 Provider::Copilot => "copilot",
351 Provider::DeepSeek => "deepseek",
352 Provider::Meta => "meta",
353 Provider::OpenRouter => "openrouter",
354 Provider::Ollama => "ollama",
355 Provider::OllamaCloud => "ollama-cloud",
356 Provider::LmStudio => "lmstudio",
357 Provider::LlamaCpp => "llamacpp",
358 Provider::Moonshot => "moonshot",
359 Provider::ZAI => "zai",
360 Provider::Minimax => "minimax",
361 Provider::MiMo => "mimo",
362 Provider::Mistral => "mistral",
363 Provider::HuggingFace => "huggingface",
364 Provider::OpenCodeZen => "opencode-zen",
365 Provider::OpenCodeGo => "opencode-go",
366 Provider::Qwen => "qwen",
367 Provider::StepFun => "stepfun",
368 Provider::Evolink => "evolink",
369 Provider::Poolside => "poolside",
370 Provider::XAI => "xai",
371 Provider::NVIDIA => "nvidia",
372 Provider::MergeGateway => "merge-gateway",
373 Provider::Vercel => "vercel",
374 }
375 }
376}
377
378impl FromStr for Provider {
379 type Err = ProviderParseError;
380
381 fn from_str(s: &str) -> Result<Self, Self::Err> {
382 match s.to_lowercase().as_str() {
383 "gemini" | "google" => Ok(Provider::Gemini),
384 "openai" => Ok(Provider::OpenAI),
385 "anthropic" => Ok(Provider::Anthropic),
386 "copilot" => Ok(Provider::Copilot),
387 "deepseek" => Ok(Provider::DeepSeek),
388 "meta" | "meta-ai" => Ok(Provider::Meta),
389 "openrouter" => Ok(Provider::OpenRouter),
390 "ollama" => Ok(Provider::Ollama),
391 "ollama-cloud" | "ollamacloud" | "ollama_cloud" => Ok(Provider::OllamaCloud),
392 "lmstudio" => Ok(Provider::LmStudio),
393 "llamacpp" | "llama.cpp" | "llama-cpp" => Ok(Provider::LlamaCpp),
394 "moonshot" => Ok(Provider::Moonshot),
395 "zai" => Ok(Provider::ZAI),
396 "minimax" => Ok(Provider::Minimax),
397 "mimo" => Ok(Provider::MiMo),
398 "mistral" => Ok(Provider::Mistral),
399 "huggingface" => Ok(Provider::HuggingFace),
400 "opencode-zen" | "opencodezen" => Ok(Provider::OpenCodeZen),
401 "opencode-go" | "opencodego" => Ok(Provider::OpenCodeGo),
402 "qwen" => Ok(Provider::Qwen),
403 "stepfun" => Ok(Provider::StepFun),
404 "evolink" => Ok(Provider::Evolink),
405 "poolside" => Ok(Provider::Poolside),
406 "xai" => Ok(Provider::XAI),
407 "nvidia" | "nvidia-nim" => Ok(Provider::NVIDIA),
408 "merge-gateway" | "merge_gateway" | "mergegateway" => Ok(Provider::MergeGateway),
409 "vercel" | "vercel-ai-gateway" | "ai-gateway" => Ok(Provider::Vercel),
410 _ => Err(ProviderParseError::InvalidProvider(s.to_string())),
411 }
412 }
413}
414
415#[cfg(test)]
416mod name_match_tests {
417 use super::*;
418
419 const PARSE_ALIASES: &[(&str, Provider)] = &[
423 ("gemini", Provider::Gemini),
424 ("google", Provider::Gemini),
425 ("openai", Provider::OpenAI),
426 ("anthropic", Provider::Anthropic),
427 ("copilot", Provider::Copilot),
428 ("deepseek", Provider::DeepSeek),
429 ("meta", Provider::Meta),
430 ("meta-ai", Provider::Meta),
431 ("openrouter", Provider::OpenRouter),
432 ("ollama", Provider::Ollama),
433 ("ollama-cloud", Provider::OllamaCloud),
434 ("ollamacloud", Provider::OllamaCloud),
435 ("ollama_cloud", Provider::OllamaCloud),
436 ("lmstudio", Provider::LmStudio),
437 ("llamacpp", Provider::LlamaCpp),
438 ("llama.cpp", Provider::LlamaCpp),
439 ("llama-cpp", Provider::LlamaCpp),
440 ("moonshot", Provider::Moonshot),
441 ("zai", Provider::ZAI),
442 ("minimax", Provider::Minimax),
443 ("mimo", Provider::MiMo),
444 ("mistral", Provider::Mistral),
445 ("huggingface", Provider::HuggingFace),
446 ("opencode-zen", Provider::OpenCodeZen),
447 ("opencodezen", Provider::OpenCodeZen),
448 ("opencode-go", Provider::OpenCodeGo),
449 ("opencodego", Provider::OpenCodeGo),
450 ("qwen", Provider::Qwen),
451 ("stepfun", Provider::StepFun),
452 ("evolink", Provider::Evolink),
453 ("poolside", Provider::Poolside),
454 ("xai", Provider::XAI),
455 ("nvidia", Provider::NVIDIA),
456 ("nvidia-nim", Provider::NVIDIA),
457 ("merge-gateway", Provider::MergeGateway),
458 ("merge_gateway", Provider::MergeGateway),
459 ("mergegateway", Provider::MergeGateway),
460 ("vercel", Provider::Vercel),
461 ("vercel-ai-gateway", Provider::Vercel),
462 ("ai-gateway", Provider::Vercel),
463 ];
464
465 #[test]
470 fn parse_aliases_and_name_matches_stay_in_lockstep() {
471 for (alias, provider) in PARSE_ALIASES {
472 assert!(provider.name_matches(alias), "{provider:?} does not match its parse alias {alias:?}");
473 assert_eq!(Provider::from_str(alias).ok(), Some(*provider), "test table drifted from from_str: {alias:?}");
474 }
475 }
476
477 #[test]
480 fn as_ref_identity_always_matches() {
481 for provider in Provider::all_providers() {
482 let canonical = provider.as_ref();
483 assert!(provider.name_matches(canonical), "{canonical} must match itself");
484 }
485 }
486
487 #[test]
490 fn case_insensitive_matches_and_unknowns_reject() {
491 assert!(Provider::Anthropic.name_matches("Anthropic"));
492 assert!(Provider::MergeGateway.name_matches("MERGE-GATEWAY"));
493 assert!(!Provider::Anthropic.name_matches("openai"));
494 assert!(!Provider::OpenAI.name_matches("OpenAICompat"));
495 assert!(!Provider::Anthropic.name_matches(""));
496 }
497
498 #[test]
499 fn picker_display_order_pins_frontier_then_open_source() {
500 let head: Vec<Provider> = Provider::picker_display_order().into_iter().take(7).collect();
501 assert_eq!(
502 head,
503 [
504 Provider::OpenAI,
505 Provider::Anthropic,
506 Provider::HuggingFace,
507 Provider::Ollama,
508 Provider::OllamaCloud,
509 Provider::LmStudio,
510 Provider::LlamaCpp,
511 ]
512 );
513 }
514
515 #[test]
519 fn picker_display_order_is_permutation_of_all_providers() {
520 let order = Provider::picker_display_order();
521 let all = Provider::all_providers();
522 assert_eq!(order.len(), all.len());
523 assert!(all.iter().all(|provider| order.contains(provider)));
524 }
525}