fix(cli): harden providers listing and keep provider map aligned
This commit is contained in:
parent
feaa4aba60
commit
ce23cbaeea
2 changed files with 239 additions and 32 deletions
20
src/main.rs
20
src/main.rs
|
|
@ -556,25 +556,37 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
Commands::Providers => {
|
Commands::Providers => {
|
||||||
let providers = providers::list_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!("Supported providers ({} total):\n", providers.len());
|
||||||
println!(" {:<19} {}", "ID (use in config)", "DESCRIPTION");
|
println!(" {:<19} {}", "ID (use in config)", "DESCRIPTION");
|
||||||
println!(" {:<19} {}", "───────────────────", "───────────");
|
println!(" {:<19} {}", "───────────────────", "───────────");
|
||||||
for p in &providers {
|
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 local_tag = if p.local { " [local]" } else { "" };
|
||||||
let aliases = if p.aliases.is_empty() {
|
let aliases = if p.aliases.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!(" (aliases: {})", p.aliases.join(", "))
|
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:<URL> Any OpenAI-compatible endpoint");
|
println!("\n custom:<URL> Any OpenAI-compatible endpoint");
|
||||||
println!(" anthropic-custom:<URL> Any Anthropic-compatible endpoint");
|
println!(" anthropic-custom:<URL> Any Anthropic-compatible endpoint");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Service { service_command } => service::handle_command(&service_command, &config),
|
Commands::Service { service_command } => service::handle_command(&service_command, &config),
|
||||||
|
|
||||||
Commands::Doctor => doctor::run(&config),
|
Commands::Doctor => doctor::run(&config),
|
||||||
|
|
|
||||||
|
|
@ -579,35 +579,181 @@ pub struct ProviderInfo {
|
||||||
pub fn list_providers() -> Vec<ProviderInfo> {
|
pub fn list_providers() -> Vec<ProviderInfo> {
|
||||||
vec![
|
vec![
|
||||||
// ── Primary providers ────────────────────────────────
|
// ── Primary providers ────────────────────────────────
|
||||||
ProviderInfo { name: "openrouter", display_name: "OpenRouter", aliases: &[], local: false },
|
ProviderInfo {
|
||||||
ProviderInfo { name: "anthropic", display_name: "Anthropic", aliases: &[], local: false },
|
name: "openrouter",
|
||||||
ProviderInfo { name: "openai", display_name: "OpenAI", aliases: &[], local: false },
|
display_name: "OpenRouter",
|
||||||
ProviderInfo { name: "ollama", display_name: "Ollama", aliases: &[], local: true },
|
aliases: &[],
|
||||||
ProviderInfo { name: "gemini", display_name: "Google Gemini", aliases: &["google", "google-gemini"], local: false },
|
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 ──────────────────────
|
// ── OpenAI-compatible providers ──────────────────────
|
||||||
ProviderInfo { name: "venice", display_name: "Venice", aliases: &[], local: false },
|
ProviderInfo {
|
||||||
ProviderInfo { name: "vercel", display_name: "Vercel AI Gateway", aliases: &["vercel-ai"], local: false },
|
name: "venice",
|
||||||
ProviderInfo { name: "cloudflare", display_name: "Cloudflare AI", aliases: &["cloudflare-ai"], local: false },
|
display_name: "Venice",
|
||||||
ProviderInfo { name: "moonshot", display_name: "Moonshot", aliases: &["kimi"], local: false },
|
aliases: &[],
|
||||||
ProviderInfo { name: "synthetic", display_name: "Synthetic", aliases: &[], local: false },
|
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 {
|
||||||
ProviderInfo { name: "glm", display_name: "GLM (Zhipu)", aliases: &["zhipu"], local: false },
|
name: "vercel",
|
||||||
ProviderInfo { name: "minimax", display_name: "MiniMax", aliases: &[], local: false },
|
display_name: "Vercel AI Gateway",
|
||||||
ProviderInfo { name: "bedrock", display_name: "Amazon Bedrock", aliases: &["aws-bedrock"], local: false },
|
aliases: &["vercel-ai"],
|
||||||
ProviderInfo { name: "qianfan", display_name: "Qianfan (Baidu)", aliases: &["baidu"], local: false },
|
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 {
|
||||||
ProviderInfo { name: "mistral", display_name: "Mistral", aliases: &[], local: false },
|
name: "cloudflare",
|
||||||
ProviderInfo { name: "xai", display_name: "xAI (Grok)", aliases: &["grok"], local: false },
|
display_name: "Cloudflare AI",
|
||||||
ProviderInfo { name: "deepseek", display_name: "DeepSeek", aliases: &[], local: false },
|
aliases: &["cloudflare-ai"],
|
||||||
ProviderInfo { name: "together", display_name: "Together AI", aliases: &["together-ai"], local: false },
|
local: false,
|
||||||
ProviderInfo { name: "fireworks", display_name: "Fireworks AI", aliases: &["fireworks-ai"], local: false },
|
},
|
||||||
ProviderInfo { name: "perplexity", display_name: "Perplexity", aliases: &[], local: false },
|
ProviderInfo {
|
||||||
ProviderInfo { name: "cohere", display_name: "Cohere", aliases: &[], local: false },
|
name: "moonshot",
|
||||||
ProviderInfo { name: "copilot", display_name: "GitHub Copilot", aliases: &["github-copilot"], local: false },
|
display_name: "Moonshot",
|
||||||
ProviderInfo { name: "lmstudio", display_name: "LM Studio", aliases: &["lm-studio"], local: true },
|
aliases: &["kimi"],
|
||||||
ProviderInfo { name: "nvidia", display_name: "NVIDIA NIM", aliases: &["nvidia-nim", "build.nvidia.com"], local: false },
|
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 ───────────────────────────────
|
// ── API error sanitization ───────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue