use serde::Deserialize;
use super::types::ProviderConfig;
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct ProviderSpec {
pub key: &'static str,
pub name: &'static str,
pub base_url: &'static str,
pub env_vars: &'static [&'static str],
pub default_model: &'static str,
pub models: &'static [(&'static str, &'static str, &'static str)], }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderModelInfo {
pub id: String,
pub name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ProviderModelsResponse {
data: Vec<ProviderModelsItem>,
}
#[derive(Debug, Deserialize)]
struct ProviderModelsItem {
id: String,
#[serde(default)]
name: Option<String>,
}
pub fn parse_provider_models_response(body: &str) -> Result<Vec<ProviderModelInfo>, serde_json::Error> {
let response: ProviderModelsResponse = serde_json::from_str(body)?;
Ok(response
.data
.into_iter()
.filter(|item| !item.id.trim().is_empty())
.map(|item| ProviderModelInfo {
id: item.id,
name: item.name.filter(|name| !name.trim().is_empty()),
})
.collect())
}
pub fn providers() -> &'static [ProviderSpec] {
static PROVIDERS: std::sync::LazyLock<Vec<ProviderSpec>> = std::sync::LazyLock::new(|| vec![
ProviderSpec {
key: "groq",
name: "Groq",
base_url: "https://api.groq.com/openai/v1",
env_vars: &["GROQ_API_KEY"],
default_model: "llama-3.3-70b-versatile",
models: &[
("llama-3.3-70b-versatile", "Llama 3.3 70B", "S"),
("llama-3.1-8b-instant", "Llama 3.1 8B", "B"),
("meta-llama/llama-4-scout-17b-16e-instruct", "Llama 4 Scout", "A"),
("meta-llama/llama-4-maverick-17b-128e-instruct", "Llama 4 Maverick", "S"),
],
},
ProviderSpec {
key: "cerebras",
name: "Cerebras",
base_url: "https://api.cerebras.ai/v1",
env_vars: &["CEREBRAS_API_KEY"],
default_model: "llama3.1-8b",
models: &[
("qwen-3-235b-a22b-instruct-2507", "Qwen3 235B", "S+"),
("llama3.1-8b", "Llama 3.1 8B", "B"),
],
},
ProviderSpec {
key: "nvidia",
name: "NVIDIA NIM",
base_url: "https://integrate.api.nvidia.com/v1",
env_vars: &["NVIDIA_API_KEY"],
default_model: "meta/llama-3.3-70b-instruct",
models: &[
("qwen/qwen3-coder-480b-a35b-instruct", "Qwen3 Coder 480B", "S+"),
("mistralai/mistral-large-3-675b-instruct-2512", "Mistral Large 675B", "A+"),
("meta/llama-3.3-70b-instruct", "Llama 3.3 70B", "A"),
("meta/llama-4-maverick-17b-128e-instruct", "Llama 4 Maverick", "S"),
("meta/llama-4-scout-17b-16e-instruct", "Llama 4 Scout", "A"),
("nvidia/llama-3.1-nemotron-ultra-253b-v1", "Nemotron Ultra 253B", "A+"),
("mistralai/devstral-2-123b-instruct-2512", "Devstral 2 123B", "S+"),
("minimaxai/minimax-m2.5", "MiniMax M2.5", "S+"),
("stepfun-ai/step-3.5-flash", "Step 3.5 Flash", "S+"),
],
},
ProviderSpec {
key: "sambanova",
name: "SambaNova",
base_url: "https://api.sambanova.ai/v1",
env_vars: &["SAMBANOVA_API_KEY"],
default_model: "Meta-Llama-3.3-70B-Instruct",
models: &[
("QwQ-32B", "QwQ 32B", "A+"),
("Meta-Llama-3.3-70B-Instruct", "Llama 3.3 70B", "S"),
("Meta-Llama-3.1-8B-Instruct", "Llama 3.1 8B", "B"),
("DeepSeek-R1", "DeepSeek R1", "S+"),
("DeepSeek-R1-Distill-Llama-70B", "R1 Distill 70B", "A"),
("Qwen3-32B", "Qwen3 32B", "A"),
],
},
ProviderSpec {
key: "openrouter",
name: "OpenRouter",
base_url: "https://openrouter.ai/api/v1",
env_vars: &["OPENROUTER_API_KEY"],
default_model: "meta-llama/llama-3.3-70b-instruct",
models: &[
("qwen/qwen3-coder", "Qwen3 Coder", "S+"),
("meta-llama/llama-3.3-70b-instruct", "Llama 3.3 70B", "S"),
("deepseek/deepseek-chat-v3-0324", "DeepSeek V3", "S"),
("google/gemma-3-27b-it", "Gemma 3 27B", "A"),
("mistralai/mistral-small-3.1-24b-instruct", "Mistral Small 3.1", "A"),
],
},
ProviderSpec {
key: "google",
name: "Google AI Studio",
base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
env_vars: &["GOOGLE_API_KEY"],
default_model: "gemini-2.5-flash",
models: &[
("gemini-2.5-flash", "Gemini 2.5 Flash", "A+"),
("gemini-2.0-flash", "Gemini 2.0 Flash", "B+"),
("gemma-3-27b-it", "Gemma 3 27B", "A"),
],
},
ProviderSpec {
key: "deepinfra",
name: "DeepInfra",
base_url: "https://api.deepinfra.com/v1/openai",
env_vars: &["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
default_model: "meta-llama/Llama-3.3-70B-Instruct",
models: &[
("meta-llama/Llama-3.3-70B-Instruct", "Llama 3.3 70B", "S"),
("Qwen/Qwen2.5-Coder-32B-Instruct", "Qwen2.5 Coder 32B", "A"),
("deepseek-ai/DeepSeek-V3-0324", "DeepSeek V3", "S"),
],
},
ProviderSpec {
key: "huggingface",
name: "HuggingFace",
base_url: "https://router.huggingface.co/v1",
env_vars: &["HUGGINGFACE_API_KEY", "HF_TOKEN"],
default_model: "meta-llama/Llama-3.3-70B-Instruct",
models: &[
("meta-llama/Llama-3.3-70B-Instruct", "Llama 3.3 70B", "S"),
("Qwen/Qwen2.5-72B-Instruct", "Qwen2.5 72B", "A"),
],
},
ProviderSpec {
key: "fireworks",
name: "Fireworks AI",
base_url: "https://api.fireworks.ai/inference/v1",
env_vars: &["FIREWORKS_API_KEY"],
default_model: "accounts/fireworks/models/llama-v3p3-70b-instruct",
models: &[
("accounts/fireworks/models/llama-v3p3-70b-instruct", "Llama 3.3 70B", "S"),
("accounts/fireworks/models/qwen2p5-coder-32b-instruct", "Qwen2.5 Coder 32B", "A"),
],
},
ProviderSpec {
key: "hyperbolic",
name: "Hyperbolic",
base_url: "https://api.hyperbolic.xyz/v1",
env_vars: &["HYPERBOLIC_API_KEY"],
default_model: "meta-llama/Llama-3.3-70B-Instruct",
models: &[
("meta-llama/Llama-3.3-70B-Instruct", "Llama 3.3 70B", "S"),
("Qwen/Qwen2.5-Coder-32B-Instruct", "Qwen2.5 Coder 32B", "A"),
("deepseek-ai/DeepSeek-V3-0324", "DeepSeek V3", "S"),
],
},
ProviderSpec {
key: "scaleway",
name: "Scaleway",
base_url: "https://api.scaleway.ai/v1",
env_vars: &["SCALEWAY_API_KEY"],
default_model: "llama-3.3-70b-instruct",
models: &[
("llama-3.3-70b-instruct", "Llama 3.3 70B", "S"),
("qwen3-235b-a22b", "Qwen3 235B", "S+"),
],
},
ProviderSpec {
key: "siliconflow",
name: "SiliconFlow",
base_url: "https://api.siliconflow.cn/v1",
env_vars: &["SILICONFLOW_API_KEY"],
default_model: "Qwen/Qwen3-8B",
models: &[
("Qwen/Qwen3-8B", "Qwen3 8B", "A-"),
("deepseek-ai/DeepSeek-R1", "DeepSeek R1", "S+"),
],
},
ProviderSpec {
key: "together",
name: "Together AI",
base_url: "https://api.together.xyz/v1",
env_vars: &["TOGETHER_API_KEY"],
default_model: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
models: &[
("meta-llama/Llama-3.3-70B-Instruct-Turbo", "Llama 3.3 70B", "S"),
("Qwen/Qwen2.5-Coder-32B-Instruct", "Qwen2.5 Coder 32B", "A"),
("deepseek-ai/DeepSeek-V3", "DeepSeek V3", "S"),
],
},
ProviderSpec {
key: "chutes",
name: "Chutes AI",
base_url: "https://llm.chutes.ai/v1",
env_vars: &["CHUTES_API_KEY"],
default_model: "deepseek-ai/DeepSeek-V3-0324",
models: &[
("deepseek-ai/DeepSeek-V3-0324", "DeepSeek V3", "S"),
],
},
ProviderSpec {
key: "codestral",
name: "Codestral (Mistral)",
base_url: "https://api.mistral.ai/v1",
env_vars: &["CODESTRAL_API_KEY"],
default_model: "codestral-latest",
models: &[
("codestral-latest", "Codestral", "B+"),
],
},
ProviderSpec {
key: "perplexity",
name: "Perplexity",
base_url: "https://api.perplexity.ai",
env_vars: &["PERPLEXITY_API_KEY", "PPLX_API_KEY"],
default_model: "llama-3.1-sonar-large-128k-online",
models: &[
("llama-3.1-sonar-large-128k-online", "Sonar Large", "A+"),
],
},
ProviderSpec {
key: "ovhcloud",
name: "OVHcloud",
base_url: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
env_vars: &["OVH_AI_ENDPOINTS_ACCESS_TOKEN"],
default_model: "Meta-Llama-3.3-70B-Instruct",
models: &[
("Meta-Llama-3.3-70B-Instruct", "Llama 3.3 70B", "S"),
("Qwen/QwQ-32B", "QwQ 32B", "A+"),
],
},
]);
&PROVIDERS
}
pub fn resolve_provider(
key: &str,
overrides: &BTreeMap<String, String>,
) -> Option<(ProviderConfig, &'static str)> {
let specs = providers();
let spec = specs.into_iter().find(|s| s.key == key)?;
let api_key = resolve_api_key(spec.key, spec.env_vars, overrides)?;
Some((
ProviderConfig {
base_url: spec.base_url.to_string(),
api_key,
model: spec.default_model.to_string(),
provider: spec.key.to_string(),
},
spec.default_model,
))
}
pub fn resolve_provider_model(
key: &str,
model: &str,
overrides: &BTreeMap<String, String>,
) -> Option<ProviderConfig> {
if key == "local" {
return Some(resolve_local(model, overrides));
}
let specs = providers();
let spec = specs.into_iter().find(|s| s.key == key)?;
let api_key = resolve_api_key(spec.key, spec.env_vars, overrides)?;
Some(ProviderConfig {
base_url: spec.base_url.to_string(),
api_key,
model: model.to_string(),
provider: spec.key.to_string(),
})
}
pub fn resolve_shorthand(s: &str, overrides: &BTreeMap<String, String>) -> Option<ProviderConfig> {
let (provider_key, model) = s.split_once('/')?;
resolve_provider_model(provider_key, model, overrides)
}
pub fn resolve_codex_shorthand(s: &str) -> Option<ProviderConfig> {
let (provider_key, model) = s.split_once('/')?;
if provider_key != "openai-codex" {
return None;
}
let token = std::env::var("OPENAI_CODEX_ACCESS_TOKEN")
.ok()
.filter(|v| !v.is_empty());
Some(ProviderConfig {
base_url: "https://chatgpt.com/backend-api".to_string(),
api_key: token.unwrap_or_default(),
model: model.to_string(),
provider: "openai-codex".to_string(),
})
}
fn resolve_local(model: &str, overrides: &BTreeMap<String, String>) -> ProviderConfig {
let base_url = overrides
.get("local.url")
.filter(|s| !s.is_empty())
.cloned()
.or_else(|| std::env::var("LOCAL_ENDPOINT").ok().filter(|s| !s.is_empty()))
.unwrap_or_else(|| "http://localhost:11434/v1".to_string());
let api_key = overrides
.get("local")
.filter(|s| !s.is_empty())
.cloned()
.or_else(|| std::env::var("LOCAL_API_KEY").ok().filter(|s| !s.is_empty()))
.unwrap_or_else(|| "local".to_string());
ProviderConfig {
base_url,
api_key,
model: model.to_string(),
provider: "local".to_string(),
}
}
pub async fn fetch_provider_models(
client: &reqwest::Client,
provider_key: &str,
overrides: &BTreeMap<String, String>,
) -> Result<Vec<ProviderModelInfo>, String> {
let spec = providers()
.iter()
.find(|spec| spec.key == provider_key)
.ok_or_else(|| format!("unknown provider: {provider_key}"))?;
let api_key = resolve_api_key(spec.key, spec.env_vars, overrides)
.ok_or_else(|| format!("{} is not configured", spec.name))?;
let url = format!("{}/models", spec.base_url.trim_end_matches('/'));
let response = client
.get(url)
.bearer_auth(api_key)
.send()
.await
.map_err(|e| format!("request failed: {e}"))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|e| format!("failed to read response: {e}"))?;
if !status.is_success() {
return Err(format!("model list failed: HTTP {status}"));
}
parse_provider_models_response(&body).map_err(|e| format!("failed to parse model list: {e}"))
}
pub fn list_providers(
overrides: &BTreeMap<String, String>,
) -> Vec<(&'static str, &'static str, bool, usize)> {
providers()
.into_iter()
.map(|s| {
let has_key = resolve_api_key(s.key, s.env_vars, overrides).is_some();
(s.key, s.name, has_key, s.models.len())
})
.collect()
}
pub fn list_models(key: &str) -> Option<Vec<(&'static str, &'static str, &'static str)>> {
let specs = providers();
let spec = specs.into_iter().find(|s| s.key == key)?;
Some(spec.models.to_vec())
}
pub fn configured_providers(
overrides: &BTreeMap<String, String>,
) -> Vec<(&'static str, &'static str, &'static str)> {
providers()
.into_iter()
.filter_map(|s| {
resolve_api_key(s.key, s.env_vars, overrides)
.map(|_| (s.key, s.name, s.default_model))
})
.collect()
}
fn resolve_api_key(
provider_key: &str,
env_vars: &[&str],
overrides: &BTreeMap<String, String>,
) -> Option<String> {
if let Some(v) = overrides.get(provider_key) {
if !v.is_empty() {
return Some(v.clone());
}
}
env_vars.iter().find_map(|var| {
std::env::var(var).ok().filter(|v| !v.is_empty())
})
}
#[cfg(test)]
mod model_list_tests {
use super::*;
#[test]
fn parses_openrouter_models_response() {
let json = r#"{
"data": [
{ "id": "qwen/qwen3-coder", "name": "Qwen: Qwen3 Coder" },
{ "id": "openai/gpt-oss-120b" }
]
}"#;
let models = parse_provider_models_response(json).expect("parse models");
assert_eq!(models.len(), 2);
assert_eq!(models[0].id, "qwen/qwen3-coder");
assert_eq!(models[0].name.as_deref(), Some("Qwen: Qwen3 Coder"));
assert_eq!(models[1].id, "openai/gpt-oss-120b");
assert_eq!(models[1].name, None);
}
}