use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ProviderParseError {
#[error("Invalid provider: '{}'. Supported providers: {}", .0, crate::provider::Provider::all_providers().iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "))]
InvalidProvider(String),
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum Provider {
Gemini,
#[default]
OpenAI,
Anthropic,
Copilot,
DeepSeek,
Meta,
OpenRouter,
Ollama,
OllamaCloud,
LmStudio,
LlamaCpp,
Moonshot,
ZAI,
Minimax,
MiMo,
Mistral,
HuggingFace,
OpenCodeZen,
OpenCodeGo,
Qwen,
StepFun,
Evolink,
Poolside,
XAI,
NVIDIA,
MergeGateway,
Vercel,
}
impl Provider {
pub fn name_matches(&self, candidate: &str) -> bool {
let normalized = candidate.to_lowercase();
self.aliases().contains(&normalized.as_str())
}
fn aliases(self) -> &'static [&'static str] {
match self {
Provider::Gemini => &["gemini", "google"],
Provider::OpenAI => &["openai"],
Provider::Anthropic => &["anthropic"],
Provider::Copilot => &["copilot"],
Provider::DeepSeek => &["deepseek"],
Provider::Meta => &["meta", "meta-ai"],
Provider::OpenRouter => &["openrouter"],
Provider::Ollama => &["ollama"],
Provider::OllamaCloud => &["ollama-cloud", "ollamacloud", "ollama_cloud"],
Provider::LmStudio => &["lmstudio"],
Provider::LlamaCpp => &["llamacpp", "llama.cpp", "llama-cpp"],
Provider::Moonshot => &["moonshot"],
Provider::ZAI => &["zai"],
Provider::Minimax => &["minimax"],
Provider::MiMo => &["mimo"],
Provider::Mistral => &["mistral"],
Provider::HuggingFace => &["huggingface"],
Provider::OpenCodeZen => &["opencode-zen", "opencodezen"],
Provider::OpenCodeGo => &["opencode-go", "opencodego"],
Provider::Qwen => &["qwen"],
Provider::StepFun => &["stepfun"],
Provider::Evolink => &["evolink"],
Provider::Poolside => &["poolside"],
Provider::XAI => &["xai"],
Provider::NVIDIA => &["nvidia", "nvidia-nim"],
Provider::MergeGateway => &["merge-gateway", "merge_gateway", "mergegateway"],
Provider::Vercel => &["vercel", "vercel-ai-gateway", "ai-gateway"],
}
}
pub fn default_api_key_env(&self) -> &'static str {
match self {
Provider::Gemini => "GEMINI_API_KEY",
Provider::OpenAI => "OPENAI_API_KEY",
Provider::Anthropic => "ANTHROPIC_API_KEY",
Provider::Copilot => "",
Provider::DeepSeek => "DEEPSEEK_API_KEY",
Provider::Meta => "META_API_KEY",
Provider::OpenRouter => "OPENROUTER_API_KEY",
Provider::Ollama => "OLLAMA_API_KEY",
Provider::OllamaCloud => "OLLAMA_API_KEY",
Provider::LmStudio => "LMSTUDIO_API_KEY",
Provider::LlamaCpp => "LLAMACPP_API_KEY",
Provider::Moonshot => "MOONSHOT_API_KEY",
Provider::ZAI => "ZAI_API_KEY",
Provider::Minimax => "MINIMAX_API_KEY",
Provider::MiMo => "MIMO_API_KEY",
Provider::Mistral => "MISTRAL_API_KEY",
Provider::HuggingFace => "HF_TOKEN",
Provider::OpenCodeZen => "OPENCODE_ZEN_API_KEY",
Provider::OpenCodeGo => "OPENCODE_GO_API_KEY",
Provider::Qwen => "QWEN_API_KEY",
Provider::StepFun => "STEPFUN_API_KEY",
Provider::Evolink => "EVOLINK_API_KEY",
Provider::Poolside => "POOLSIDE_API_KEY",
Provider::XAI => "XAI_API_KEY",
Provider::NVIDIA => "NVIDIA_API_KEY",
Provider::MergeGateway => "MERGE_GATEWAY_API_KEY",
Provider::Vercel => "AI_GATEWAY_API_KEY",
}
}
pub fn all_providers() -> Vec<Provider> {
vec![
Provider::OpenAI,
Provider::Anthropic,
Provider::Copilot,
Provider::Minimax,
Provider::MiMo,
Provider::Mistral,
Provider::Gemini,
Provider::DeepSeek,
Provider::Meta,
Provider::HuggingFace,
Provider::OpenRouter,
Provider::Ollama,
Provider::OllamaCloud,
Provider::LmStudio,
Provider::LlamaCpp,
Provider::Moonshot,
Provider::ZAI,
Provider::OpenCodeZen,
Provider::OpenCodeGo,
Provider::Qwen,
Provider::StepFun,
Provider::Evolink,
Provider::Poolside,
Provider::XAI,
Provider::NVIDIA,
Provider::MergeGateway,
Provider::Vercel,
]
}
pub fn label(&self) -> &'static str {
match self {
Provider::Gemini => "Gemini",
Provider::OpenAI => "OpenAI",
Provider::Anthropic => "Anthropic",
Provider::Copilot => "GitHub Copilot",
Provider::DeepSeek => "DeepSeek",
Provider::Meta => "Meta AI",
Provider::OpenRouter => "OpenRouter",
Provider::Ollama => "Ollama",
Provider::OllamaCloud => "Ollama Cloud",
Provider::LmStudio => "LM Studio",
Provider::LlamaCpp => "llama.cpp",
Provider::Moonshot => "Moonshot",
Provider::ZAI => "Z.AI",
Provider::Minimax => "MiniMax",
Provider::MiMo => "Xiaomi MiMo",
Provider::Mistral => "Mistral",
Provider::HuggingFace => "Hugging Face",
Provider::OpenCodeZen => "OpenCode Zen",
Provider::OpenCodeGo => "OpenCode Go",
Provider::Qwen => "Qwen",
Provider::StepFun => "StepFun",
Provider::Evolink => "Evolink",
Provider::Poolside => "Poolside",
Provider::XAI => "xAI",
Provider::NVIDIA => "NVIDIA",
Provider::MergeGateway => "Merge Gateway",
Provider::Vercel => "Vercel AI Gateway",
}
}
pub fn is_dynamic(&self) -> bool {
matches!(self, Provider::Copilot) || self.is_local()
}
pub fn is_local(&self) -> bool {
matches!(self, Provider::Ollama | Provider::LmStudio | Provider::LlamaCpp)
}
pub fn local_install_instructions(&self) -> Option<&'static str> {
match self {
Provider::Ollama => Some(
"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')",
),
Provider::LmStudio => Some(
"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",
),
Provider::LlamaCpp => Some(
"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",
),
_ => None,
}
}
pub fn uses_managed_auth(&self) -> bool {
matches!(self, Provider::Copilot)
}
pub fn platform_url(&self) -> Option<&'static str> {
match self {
Provider::Gemini => Some("https://aistudio.google.com/app/apikey"),
Provider::OpenAI => Some("https://platform.openai.com/api-keys"),
Provider::Anthropic => Some("https://console.anthropic.com"),
Provider::Copilot => None,
Provider::DeepSeek => Some("https://platform.deepseek.com"),
Provider::Meta => Some("https://developer.meta.com/ai/models"),
Provider::OpenRouter => Some("https://openrouter.ai/api/v1/auth/keys"),
Provider::Ollama => None,
Provider::OllamaCloud => Some("https://ollama.com"),
Provider::LmStudio => None,
Provider::LlamaCpp => None,
Provider::Moonshot => Some("https://platform.kimi.ai/console/api-keys"),
Provider::ZAI => Some("https://z.ai/docs"),
Provider::Minimax => Some("https://platform.minimax.io/docs"),
Provider::MiMo => Some("https://platform.xiaomimimo.com/docs/en-US/welcome"),
Provider::Mistral => Some("https://console.mistral.ai"),
Provider::HuggingFace => Some("https://huggingface.co/settings/tokens"),
Provider::OpenCodeZen => Some("https://opencode.ai/docs/zen/"),
Provider::OpenCodeGo => Some("https://opencode.ai/docs/go/"),
Provider::Qwen => Some("https://dashscope.console.aliyun.com"),
Provider::StepFun => Some("https://platform.stepfun.com"),
Provider::Evolink => Some("https://evolink.ai/dashboard/keys"),
Provider::Poolside => Some("https://poolside.ai"),
Provider::XAI => Some("https://docs.x.ai"),
Provider::NVIDIA => Some("https://build.nvidia.com"),
Provider::MergeGateway => Some("https://dashboard.merge.dev"),
Provider::Vercel => Some("https://vercel.com/docs/ai-gateway/getting-started"),
}
}
}
impl fmt::Display for Provider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Provider::Gemini => write!(f, "gemini"),
Provider::OpenAI => write!(f, "openai"),
Provider::Anthropic => write!(f, "anthropic"),
Provider::Copilot => write!(f, "copilot"),
Provider::DeepSeek => write!(f, "deepseek"),
Provider::Meta => write!(f, "meta"),
Provider::OpenRouter => write!(f, "openrouter"),
Provider::Ollama => write!(f, "ollama"),
Provider::OllamaCloud => write!(f, "ollama-cloud"),
Provider::LmStudio => write!(f, "lmstudio"),
Provider::LlamaCpp => write!(f, "llamacpp"),
Provider::Moonshot => write!(f, "moonshot"),
Provider::ZAI => write!(f, "zai"),
Provider::Minimax => write!(f, "minimax"),
Provider::MiMo => write!(f, "mimo"),
Provider::Mistral => write!(f, "mistral"),
Provider::HuggingFace => write!(f, "huggingface"),
Provider::OpenCodeZen => write!(f, "opencode-zen"),
Provider::OpenCodeGo => write!(f, "opencode-go"),
Provider::Qwen => write!(f, "qwen"),
Provider::StepFun => write!(f, "stepfun"),
Provider::Evolink => write!(f, "evolink"),
Provider::Poolside => write!(f, "poolside"),
Provider::XAI => write!(f, "xai"),
Provider::NVIDIA => write!(f, "nvidia"),
Provider::MergeGateway => write!(f, "merge-gateway"),
Provider::Vercel => write!(f, "vercel"),
}
}
}
impl AsRef<str> for Provider {
fn as_ref(&self) -> &str {
match self {
Provider::Gemini => "gemini",
Provider::OpenAI => "openai",
Provider::Anthropic => "anthropic",
Provider::Copilot => "copilot",
Provider::DeepSeek => "deepseek",
Provider::Meta => "meta",
Provider::OpenRouter => "openrouter",
Provider::Ollama => "ollama",
Provider::OllamaCloud => "ollama-cloud",
Provider::LmStudio => "lmstudio",
Provider::LlamaCpp => "llamacpp",
Provider::Moonshot => "moonshot",
Provider::ZAI => "zai",
Provider::Minimax => "minimax",
Provider::MiMo => "mimo",
Provider::Mistral => "mistral",
Provider::HuggingFace => "huggingface",
Provider::OpenCodeZen => "opencode-zen",
Provider::OpenCodeGo => "opencode-go",
Provider::Qwen => "qwen",
Provider::StepFun => "stepfun",
Provider::Evolink => "evolink",
Provider::Poolside => "poolside",
Provider::XAI => "xai",
Provider::NVIDIA => "nvidia",
Provider::MergeGateway => "merge-gateway",
Provider::Vercel => "vercel",
}
}
}
impl FromStr for Provider {
type Err = ProviderParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"gemini" | "google" => Ok(Provider::Gemini),
"openai" => Ok(Provider::OpenAI),
"anthropic" => Ok(Provider::Anthropic),
"copilot" => Ok(Provider::Copilot),
"deepseek" => Ok(Provider::DeepSeek),
"meta" | "meta-ai" => Ok(Provider::Meta),
"openrouter" => Ok(Provider::OpenRouter),
"ollama" => Ok(Provider::Ollama),
"ollama-cloud" | "ollamacloud" | "ollama_cloud" => Ok(Provider::OllamaCloud),
"lmstudio" => Ok(Provider::LmStudio),
"llamacpp" | "llama.cpp" | "llama-cpp" => Ok(Provider::LlamaCpp),
"moonshot" => Ok(Provider::Moonshot),
"zai" => Ok(Provider::ZAI),
"minimax" => Ok(Provider::Minimax),
"mimo" => Ok(Provider::MiMo),
"mistral" => Ok(Provider::Mistral),
"huggingface" => Ok(Provider::HuggingFace),
"opencode-zen" | "opencodezen" => Ok(Provider::OpenCodeZen),
"opencode-go" | "opencodego" => Ok(Provider::OpenCodeGo),
"qwen" => Ok(Provider::Qwen),
"stepfun" => Ok(Provider::StepFun),
"evolink" => Ok(Provider::Evolink),
"poolside" => Ok(Provider::Poolside),
"xai" => Ok(Provider::XAI),
"nvidia" | "nvidia-nim" => Ok(Provider::NVIDIA),
"merge-gateway" | "merge_gateway" | "mergegateway" => Ok(Provider::MergeGateway),
"vercel" | "vercel-ai-gateway" | "ai-gateway" => Ok(Provider::Vercel),
_ => Err(ProviderParseError::InvalidProvider(s.to_string())),
}
}
}
#[cfg(test)]
mod name_match_tests {
use super::*;
const PARSE_ALIASES: &[(&str, Provider)] = &[
("gemini", Provider::Gemini),
("google", Provider::Gemini),
("openai", Provider::OpenAI),
("anthropic", Provider::Anthropic),
("copilot", Provider::Copilot),
("deepseek", Provider::DeepSeek),
("meta", Provider::Meta),
("meta-ai", Provider::Meta),
("openrouter", Provider::OpenRouter),
("ollama", Provider::Ollama),
("ollama-cloud", Provider::OllamaCloud),
("ollamacloud", Provider::OllamaCloud),
("ollama_cloud", Provider::OllamaCloud),
("lmstudio", Provider::LmStudio),
("llamacpp", Provider::LlamaCpp),
("llama.cpp", Provider::LlamaCpp),
("llama-cpp", Provider::LlamaCpp),
("moonshot", Provider::Moonshot),
("zai", Provider::ZAI),
("minimax", Provider::Minimax),
("mimo", Provider::MiMo),
("mistral", Provider::Mistral),
("huggingface", Provider::HuggingFace),
("opencode-zen", Provider::OpenCodeZen),
("opencodezen", Provider::OpenCodeZen),
("opencode-go", Provider::OpenCodeGo),
("opencodego", Provider::OpenCodeGo),
("qwen", Provider::Qwen),
("stepfun", Provider::StepFun),
("evolink", Provider::Evolink),
("poolside", Provider::Poolside),
("xai", Provider::XAI),
("nvidia", Provider::NVIDIA),
("nvidia-nim", Provider::NVIDIA),
("merge-gateway", Provider::MergeGateway),
("merge_gateway", Provider::MergeGateway),
("mergegateway", Provider::MergeGateway),
("vercel", Provider::Vercel),
("vercel-ai-gateway", Provider::Vercel),
("ai-gateway", Provider::Vercel),
];
#[test]
fn parse_aliases_and_name_matches_stay_in_lockstep() {
for (alias, provider) in PARSE_ALIASES {
assert!(provider.name_matches(alias), "{provider:?} does not match its parse alias {alias:?}");
assert_eq!(Provider::from_str(alias).ok(), Some(*provider), "test table drifted from from_str: {alias:?}");
}
}
#[test]
fn as_ref_identity_always_matches() {
for provider in Provider::all_providers() {
let canonical = provider.as_ref();
assert!(provider.name_matches(canonical), "{canonical} must match itself");
}
}
#[test]
fn case_insensitive_matches_and_unknowns_reject() {
assert!(Provider::Anthropic.name_matches("Anthropic"));
assert!(Provider::MergeGateway.name_matches("MERGE-GATEWAY"));
assert!(!Provider::Anthropic.name_matches("openai"));
assert!(!Provider::OpenAI.name_matches("OpenAICompat"));
assert!(!Provider::Anthropic.name_matches(""));
}
}