From ce23cbaeea7f5edae93500a4cb4fc8d1169e0035 Mon Sep 17 00:00:00 2001 From: Chummy Date: Wed, 18 Feb 2026 00:34:23 +0800 Subject: [PATCH] fix(cli): harden providers listing and keep provider map aligned --- src/main.rs | 20 +++- src/providers/mod.rs | 251 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 239 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1919afd..e14fcc9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -556,25 +556,37 @@ async fn main() -> Result<()> { Commands::Providers => { let providers = providers::list_providers(); - let current = config.default_provider.as_deref().unwrap_or("openrouter"); + let current = config + .default_provider + .as_deref() + .unwrap_or("openrouter") + .trim() + .to_ascii_lowercase(); println!("Supported providers ({} total):\n", providers.len()); println!(" {:<19} {}", "ID (use in config)", "DESCRIPTION"); println!(" {:<19} {}", "───────────────────", "───────────"); for p in &providers { - let marker = if p.name == current { " (active)" } else { "" }; + let is_active = p.name.eq_ignore_ascii_case(¤t) + || p.aliases + .iter() + .any(|alias| alias.eq_ignore_ascii_case(¤t)); + let marker = if is_active { " (active)" } else { "" }; let local_tag = if p.local { " [local]" } else { "" }; let aliases = if p.aliases.is_empty() { String::new() } else { format!(" (aliases: {})", p.aliases.join(", ")) }; - println!(" {:<19} {}{}{}{}", p.name, p.display_name, local_tag, marker, aliases); + println!( + " {:<19} {}{}{}{}", + p.name, p.display_name, local_tag, marker, aliases + ); } println!("\n custom: Any OpenAI-compatible endpoint"); println!(" anthropic-custom: Any Anthropic-compatible endpoint"); Ok(()) } - + Commands::Service { service_command } => service::handle_command(&service_command, &config), Commands::Doctor => doctor::run(&config), diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 89d4b82..d624999 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -579,35 +579,181 @@ pub struct ProviderInfo { pub fn list_providers() -> Vec { vec![ // ── Primary providers ──────────────────────────────── - ProviderInfo { name: "openrouter", display_name: "OpenRouter", aliases: &[], local: false }, - ProviderInfo { name: "anthropic", display_name: "Anthropic", aliases: &[], local: false }, - ProviderInfo { name: "openai", display_name: "OpenAI", aliases: &[], local: false }, - ProviderInfo { name: "ollama", display_name: "Ollama", aliases: &[], local: true }, - ProviderInfo { name: "gemini", display_name: "Google Gemini", aliases: &["google", "google-gemini"], local: false }, + ProviderInfo { + name: "openrouter", + display_name: "OpenRouter", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "anthropic", + display_name: "Anthropic", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "openai", + display_name: "OpenAI", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "ollama", + display_name: "Ollama", + aliases: &[], + local: true, + }, + ProviderInfo { + name: "gemini", + display_name: "Google Gemini", + aliases: &["google", "google-gemini"], + local: false, + }, // ── OpenAI-compatible providers ────────────────────── - ProviderInfo { name: "venice", display_name: "Venice", aliases: &[], local: false }, - ProviderInfo { name: "vercel", display_name: "Vercel AI Gateway", aliases: &["vercel-ai"], local: false }, - ProviderInfo { name: "cloudflare", display_name: "Cloudflare AI", aliases: &["cloudflare-ai"], local: false }, - ProviderInfo { name: "moonshot", display_name: "Moonshot", aliases: &["kimi"], local: false }, - ProviderInfo { name: "synthetic", display_name: "Synthetic", aliases: &[], local: false }, - ProviderInfo { name: "opencode", display_name: "OpenCode Zen", aliases: &["opencode-zen"], local: false }, - ProviderInfo { name: "zai", display_name: "Z.AI", aliases: &["z.ai"], local: false }, - ProviderInfo { name: "glm", display_name: "GLM (Zhipu)", aliases: &["zhipu"], local: false }, - ProviderInfo { name: "minimax", display_name: "MiniMax", aliases: &[], local: false }, - ProviderInfo { name: "bedrock", display_name: "Amazon Bedrock", aliases: &["aws-bedrock"], local: false }, - ProviderInfo { name: "qianfan", display_name: "Qianfan (Baidu)", aliases: &["baidu"], local: false }, - ProviderInfo { name: "qwen", display_name: "Qwen (DashScope)", aliases: &["dashscope", "qwen-intl", "dashscope-intl", "qwen-us", "dashscope-us"], local: false }, - ProviderInfo { name: "groq", display_name: "Groq", aliases: &[], local: false }, - ProviderInfo { name: "mistral", display_name: "Mistral", aliases: &[], local: false }, - ProviderInfo { name: "xai", display_name: "xAI (Grok)", aliases: &["grok"], local: false }, - ProviderInfo { name: "deepseek", display_name: "DeepSeek", aliases: &[], local: false }, - ProviderInfo { name: "together", display_name: "Together AI", aliases: &["together-ai"], local: false }, - ProviderInfo { name: "fireworks", display_name: "Fireworks AI", aliases: &["fireworks-ai"], local: false }, - ProviderInfo { name: "perplexity", display_name: "Perplexity", aliases: &[], local: false }, - ProviderInfo { name: "cohere", display_name: "Cohere", aliases: &[], local: false }, - ProviderInfo { name: "copilot", display_name: "GitHub Copilot", aliases: &["github-copilot"], local: false }, - ProviderInfo { name: "lmstudio", display_name: "LM Studio", aliases: &["lm-studio"], local: true }, - ProviderInfo { name: "nvidia", display_name: "NVIDIA NIM", aliases: &["nvidia-nim", "build.nvidia.com"], local: false }, + ProviderInfo { + name: "venice", + display_name: "Venice", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "vercel", + display_name: "Vercel AI Gateway", + aliases: &["vercel-ai"], + local: false, + }, + ProviderInfo { + name: "cloudflare", + display_name: "Cloudflare AI", + aliases: &["cloudflare-ai"], + local: false, + }, + ProviderInfo { + name: "moonshot", + display_name: "Moonshot", + aliases: &["kimi"], + local: false, + }, + ProviderInfo { + name: "synthetic", + display_name: "Synthetic", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "opencode", + display_name: "OpenCode Zen", + aliases: &["opencode-zen"], + local: false, + }, + ProviderInfo { + name: "zai", + display_name: "Z.AI", + aliases: &["z.ai"], + local: false, + }, + ProviderInfo { + name: "glm", + display_name: "GLM (Zhipu)", + aliases: &["zhipu"], + local: false, + }, + ProviderInfo { + name: "minimax", + display_name: "MiniMax", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "bedrock", + display_name: "Amazon Bedrock", + aliases: &["aws-bedrock"], + local: false, + }, + ProviderInfo { + name: "qianfan", + display_name: "Qianfan (Baidu)", + aliases: &["baidu"], + local: false, + }, + ProviderInfo { + name: "qwen", + display_name: "Qwen (DashScope)", + aliases: &[ + "dashscope", + "qwen-intl", + "dashscope-intl", + "qwen-us", + "dashscope-us", + ], + local: false, + }, + ProviderInfo { + name: "groq", + display_name: "Groq", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "mistral", + display_name: "Mistral", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "xai", + display_name: "xAI (Grok)", + aliases: &["grok"], + local: false, + }, + ProviderInfo { + name: "deepseek", + display_name: "DeepSeek", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "together", + display_name: "Together AI", + aliases: &["together-ai"], + local: false, + }, + ProviderInfo { + name: "fireworks", + display_name: "Fireworks AI", + aliases: &["fireworks-ai"], + local: false, + }, + ProviderInfo { + name: "perplexity", + display_name: "Perplexity", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "cohere", + display_name: "Cohere", + aliases: &[], + local: false, + }, + ProviderInfo { + name: "copilot", + display_name: "GitHub Copilot", + aliases: &["github-copilot"], + local: false, + }, + ProviderInfo { + name: "lmstudio", + display_name: "LM Studio", + aliases: &["lm-studio"], + local: true, + }, + ProviderInfo { + name: "nvidia", + display_name: "NVIDIA NIM", + aliases: &["nvidia-nim", "build.nvidia.com"], + local: false, + }, ] } @@ -1084,6 +1230,55 @@ mod tests { } } + #[test] + fn listed_providers_have_unique_ids_and_aliases() { + let providers = list_providers(); + let mut canonical_ids = std::collections::HashSet::new(); + let mut aliases = std::collections::HashSet::new(); + + for provider in providers { + assert!( + canonical_ids.insert(provider.name), + "Duplicate canonical provider id: {}", + provider.name + ); + + for alias in provider.aliases { + assert_ne!( + *alias, provider.name, + "Alias must differ from canonical id: {}", + provider.name + ); + assert!( + !canonical_ids.contains(alias), + "Alias conflicts with canonical provider id: {}", + alias + ); + assert!(aliases.insert(alias), "Duplicate provider alias: {}", alias); + } + } + } + + #[test] + fn listed_providers_and_aliases_are_constructible() { + for provider in list_providers() { + assert!( + create_provider(provider.name, Some("provider-test-credential")).is_ok(), + "Canonical provider id should be constructible: {}", + provider.name + ); + + for alias in provider.aliases { + assert!( + create_provider(alias, Some("provider-test-credential")).is_ok(), + "Provider alias should be constructible: {} (for {})", + alias, + provider.name + ); + } + } + } + // ── API error sanitization ─────────────────────────────── #[test]