use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct ModelPricingSpec {
pub input_per_million: f64,
pub output_per_million: f64,
pub cache_hit_input_per_million: Option<f64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ModelSpec {
pub name: String,
pub context_window: usize,
pub pricing: Option<ModelPricingSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProviderPreset {
pub id: String,
#[serde(default = "default_name")]
pub name: String,
pub provider_type: String,
pub api_base: String,
pub anthropic_api_base: Option<String>,
pub default_model: String,
pub models: Vec<ModelSpec>,
#[serde(default)]
pub mainland_accessible: bool,
#[serde(default)]
pub key_env: String,
#[serde(default)]
pub key_url: String,
}
fn default_name() -> String {
String::new()
}
#[derive(Deserialize)]
struct PresetsFile {
providers: Vec<ProviderPreset>,
}
static PRESETS_TOML: &str = include_str!("../providers.toml");
pub fn all_presets() -> &'static [ProviderPreset] {
use std::sync::OnceLock;
static CACHE: OnceLock<Vec<ProviderPreset>> = OnceLock::new();
CACHE.get_or_init(|| {
#[allow(
clippy::expect_used,
reason = "TOML is bundled at compile time and always valid"
)]
toml::from_str::<PresetsFile>(PRESETS_TOML)
.expect("providers.toml is bundled at compile time and must be valid")
.providers
})
}
pub fn find_preset(id: &str) -> Option<&'static ProviderPreset> {
all_presets().iter().find(|p| p.id == id)
}
pub fn find_preset_by_api_base(url: &str) -> Option<&'static ProviderPreset> {
all_presets().iter().find(|p| p.api_base == url)
}
pub fn bundled_presets() -> &'static [ProviderPreset] {
all_presets()
}
fn providers_d_dir() -> Option<std::path::PathBuf> {
Some(crate::paths::user_data_dir().join("providers.d"))
}
pub fn additional_presets() -> Vec<ProviderPreset> {
let dir = match providers_d_dir() {
Some(d) => d,
None => return Vec::new(),
};
if !dir.exists() {
return Vec::new();
}
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) => {
tracing::warn!("providers.d: read_dir failed: {e}");
return Vec::new();
}
};
let mut paths: Vec<std::path::PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("toml"))
.collect();
paths.sort();
let mut out: Vec<ProviderPreset> = Vec::new();
for path in paths {
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
tracing::warn!("providers.d: failed to read {}: {e}", path.display());
continue;
}
};
match toml::from_str::<PresetsFile>(&content) {
Ok(file) => {
for mut p in file.providers {
if p.name.is_empty() {
p.name = p.id.clone();
}
out.push(p);
}
}
Err(e) => {
tracing::warn!("providers.d: failed to parse {}: {e}", path.display());
}
}
}
out
}
pub fn all_presets_dynamic() -> Vec<ProviderPreset> {
let mut all: Vec<ProviderPreset> = bundled_presets().to_vec();
all.extend(additional_presets());
all
}
pub fn find_preset_extended(id: &str) -> Option<ProviderPreset> {
all_presets_dynamic().into_iter().find(|p| p.id == id)
}
fn merge_models(existing: &mut Vec<ModelSpec>, override_models: Vec<ModelSpec>) {
for om in override_models {
if let Some(slot) = existing.iter_mut().find(|m| m.name == om.name) {
*slot = om;
} else {
existing.push(om);
}
}
}
fn merge_preset_into(existing: &mut ProviderPreset, override_p: ProviderPreset) {
if !override_p.api_base.is_empty() {
existing.api_base = override_p.api_base;
}
if !override_p.provider_type.is_empty() {
existing.provider_type = override_p.provider_type;
}
if !override_p.default_model.is_empty() {
existing.default_model = override_p.default_model;
}
if override_p.anthropic_api_base.is_some() {
existing.anthropic_api_base = override_p.anthropic_api_base;
}
if !override_p.key_env.is_empty() {
existing.key_env = override_p.key_env;
}
if !override_p.key_url.is_empty() {
existing.key_url = override_p.key_url;
}
merge_models(&mut existing.models, override_p.models);
}
fn compute_effective_presets() -> Vec<ProviderPreset> {
let bundled = bundled_presets().to_vec();
let with_cache = match crate::providers_cache::load_cache() {
Some(cache) => crate::providers_cache::merge_over_bundled(&bundled, &cache.providers),
None => bundled,
};
let mut result = with_cache;
for p in additional_presets() {
match result.iter_mut().find(|r| r.id == p.id) {
Some(existing) => merge_preset_into(existing, p),
None => result.push(p),
}
}
result
}
pub fn all_presets_effective() -> Vec<ProviderPreset> {
compute_effective_presets()
}
pub fn find_preset_effective(id: &str) -> Option<ProviderPreset> {
all_presets_effective().into_iter().find(|p| p.id == id)
}
pub fn find_model_pricing_effective(model: &str) -> Option<ModelPricingSpec> {
for preset in all_presets_effective() {
for spec in &preset.models {
if spec.name == model {
if let Some(pricing) = spec.pricing {
return Some(pricing);
}
}
}
}
None
}
#[derive(Debug, Clone)]
pub struct WrittenPreset {
pub path: std::path::PathBuf,
pub id: String,
}
#[derive(serde::Serialize)]
struct PresetsFileSer<'a> {
providers: &'a [ProviderPreset],
}
pub fn write_user_preset(preset: &ProviderPreset) -> crate::error::Result<WrittenPreset> {
use crate::error::Error;
let dir = providers_d_dir().ok_or(Error::Config {
message: "cannot resolve user data directory for providers.d".into(),
})?;
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
let path = dir.join(format!("{}.toml", preset.id));
let body = toml::to_string_pretty(&PresetsFileSer {
providers: std::slice::from_ref(preset),
})
.map_err(|e| Error::Config {
message: format!("serialize preset {}: {e}", preset.id),
})?;
std::fs::write(&path, body).map_err(Error::Io)?;
Ok(WrittenPreset {
path,
id: preset.id.clone(),
})
}
pub fn find_model_pricing(model: &str) -> Option<&'static ModelPricingSpec> {
for preset in all_presets() {
for spec in &preset.models {
if spec.name == model {
return spec.pricing.as_ref();
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_presets_non_empty() {
assert!(all_presets().len() > 10);
}
#[test]
fn find_preset_anthropic() {
let preset = find_preset("anthropic").unwrap();
assert_eq!(preset.provider_type, "anthropic");
}
#[test]
fn find_preset_unknown_returns_none() {
assert!(find_preset("bogus").is_none());
}
#[test]
fn all_presets_have_valid_provider_type() {
for p in all_presets() {
assert!(
p.provider_type == "openai" || p.provider_type == "anthropic",
"preset {} has invalid provider_type: {}",
p.id,
p.provider_type
);
}
}
#[test]
fn default_preset_is_anthropic() {
assert_eq!(all_presets()[0].id, "anthropic");
}
#[test]
fn find_preset_deepseek_api_base() {
let preset = find_preset("deepseek").unwrap();
assert_eq!(preset.api_base, "https://api.deepseek.com/v1");
}
#[test]
fn find_preset_minimax_cn_uses_domestic_endpoint() {
let preset = find_preset("minimax-cn").expect("minimax-cn preset must exist");
assert_eq!(preset.api_base, "https://api.minimaxi.com/v1");
assert_eq!(
preset.anthropic_api_base.as_deref(),
Some("https://api.minimaxi.com/anthropic"),
);
assert_eq!(preset.key_env, "MINIMAX_API_KEY");
assert!(preset.mainland_accessible);
}
#[test]
fn find_preset_by_api_base_known() {
let preset =
find_preset_by_api_base("https://api.deepseek.com/v1").expect("deepseek preset");
assert_eq!(preset.id, "deepseek");
}
#[test]
fn find_preset_by_api_base_unknown() {
assert!(find_preset_by_api_base("https://example.com/v1").is_none());
}
fn write_user_override(
dir: &std::path::Path,
name: &str,
body: &str,
) -> Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(dir)?;
std::fs::write(dir.join(name), body)?;
Ok(())
}
#[test]
fn additional_presets_returns_empty_when_dir_absent() -> Result<(), Box<dyn std::error::Error>>
{
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
assert!(
additional_presets().is_empty(),
"no providers.d/ under temp home, expected empty list"
);
Ok(())
}
#[test]
fn additional_presets_loads_valid_file() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let dir = tmp.path().join(".recursive").join("providers.d");
write_user_override(
&dir,
"test-vendor.toml",
r#"
[[providers]]
id = "test-vendor"
name = "Test Vendor"
provider_type = "openai"
api_base = "https://test.example.com/v1"
default_model = "t1"
models = [
{name = "t1", context_window = 32000},
]
mainland_accessible = false
key_env = "TEST_API_KEY"
key_url = "https://x"
"#,
)?;
let loaded = additional_presets();
assert_eq!(loaded.len(), 1, "expected exactly one override");
let p = &loaded[0];
assert_eq!(p.id, "test-vendor");
assert_eq!(p.provider_type, "openai");
assert_eq!(p.api_base, "https://test.example.com/v1");
assert_eq!(p.default_model, "t1");
assert_eq!(p.key_env, "TEST_API_KEY");
assert_eq!(p.key_url, "https://x");
assert_eq!(p.models.len(), 1);
assert_eq!(p.models[0].name, "t1");
Ok(())
}
#[test]
fn effective_presets_merge_user_models_into_existing_provider(
) -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let dir = tmp.path().join(".recursive").join("providers.d");
write_user_override(
&dir,
"zhipu.toml",
r#"
[[providers]]
id = "zhipu"
name = "智谱 AI (GLM)"
provider_type = "openai"
api_base = "https://open.bigmodel.cn/api/paas/v4"
default_model = "glm-4.7"
models = [
{ name = "glm-user-only", context_window = 12345, pricing = { input_per_million = 0.5, output_per_million = 1.0 } },
]
"#,
)?;
let zhipu = all_presets_effective()
.into_iter()
.find(|p| p.id == "zhipu")
.expect("zhipu must exist in effective catalog");
assert!(
zhipu.models.iter().any(|m| m.name == "glm-5.2"),
"bundled glm-5.2 should survive the merge"
);
assert!(
zhipu.models.iter().any(|m| m.name == "glm-4.7"),
"bundled glm-4.7 should survive the merge"
);
let user_only = zhipu
.models
.iter()
.find(|m| m.name == "glm-user-only")
.expect("user-only model should be merged in from providers.d");
assert_eq!(user_only.context_window, 12345);
Ok(())
}
#[test]
fn effective_presets_user_model_overrides_existing_pricing(
) -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let dir = tmp.path().join(".recursive").join("providers.d");
write_user_override(
&dir,
"zhipu.toml",
r#"
[[providers]]
id = "zhipu"
provider_type = "openai"
api_base = "https://open.bigmodel.cn/api/paas/v4"
default_model = "glm-4.7"
models = [
{ name = "glm-4.7", context_window = 999999, pricing = { input_per_million = 0.01, output_per_million = 0.02 } },
]
"#,
)?;
let zhipu = all_presets_effective()
.into_iter()
.find(|p| p.id == "zhipu")
.expect("zhipu must exist");
let glm47 = zhipu
.models
.iter()
.find(|m| m.name == "glm-4.7")
.expect("glm-4.7 must still exist");
assert_eq!(
glm47.context_window, 999999,
"user override must win on same name"
);
assert!(zhipu.models.iter().any(|m| m.name == "glm-5.2"));
Ok(())
}
#[test]
fn additional_presets_skips_invalid_file() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let dir = tmp.path().join(".recursive").join("providers.d");
write_user_override(&dir, "bad.toml", "this is [[[not valid toml")?;
let loaded = additional_presets();
assert!(
loaded.is_empty(),
"malformed override file must be skipped silently, got {loaded:?}"
);
Ok(())
}
#[test]
fn bundled_presets_is_same_as_all_presets() {
let bundled = bundled_presets();
let all = all_presets();
assert_eq!(bundled.len(), all.len());
}
#[test]
fn all_presets_dynamic_includes_bundled() {
let dynamic = all_presets_dynamic();
assert!(
dynamic.iter().any(|p| p.id == "anthropic"),
"anthropic must be in dynamic list, got: {:?}",
dynamic.iter().map(|p| &p.id).collect::<Vec<_>>()
);
}
#[test]
fn find_preset_effective_finds_bundled_anthropic() {
let p = find_preset_effective("anthropic").expect("anthropic must be found");
assert_eq!(p.id, "anthropic");
}
#[test]
fn find_preset_effective_returns_none_for_unknown() {
assert!(find_preset_effective("completely-unknown-preset-xyz").is_none());
}
#[test]
fn find_model_pricing_returns_none_for_unknown_model() {
assert!(find_model_pricing("no-such-model-xyz-999").is_none());
}
#[test]
fn find_model_pricing_effective_returns_none_for_unknown_model() {
assert!(find_model_pricing_effective("no-such-model-xyz-999").is_none());
}
#[test]
fn find_preset_extended_finds_user_override() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let dir = tmp.path().join(".recursive").join("providers.d");
write_user_override(
&dir,
"custom.toml",
r#"
[[providers]]
id = "custom-runtime"
name = "Custom Runtime"
provider_type = "openai"
api_base = "https://custom.example.com/v1"
default_model = "c1"
models = [
{name = "c1", context_window = 16000},
]
mainland_accessible = false
key_env = "CUSTOM_API_KEY"
key_url = "https://custom.example.com/keys"
"#,
)?;
let ext = find_preset_extended("custom-runtime")
.ok_or("extended lookup should find user override")?;
assert_eq!(ext.id, "custom-runtime");
assert_eq!(ext.api_base, "https://custom.example.com/v1");
assert!(
find_preset("custom-runtime").is_none(),
"find_preset must remain strict: bundled-catalog only"
);
assert!(find_preset("anthropic").is_some());
Ok(())
}
fn sample_test_preset(id: &str) -> ProviderPreset {
ProviderPreset {
id: id.to_string(),
name: "Test Write Helper".to_string(),
provider_type: "openai".to_string(),
api_base: "https://write-helper.example.com/v1".to_string(),
anthropic_api_base: None,
default_model: "wh-1".to_string(),
models: vec![ModelSpec {
name: "wh-1".to_string(),
context_window: 8192,
max_tokens: None,
pricing: Some(ModelPricingSpec {
input_per_million: 0.10,
output_per_million: 0.20,
cache_hit_input_per_million: None,
}),
}],
mainland_accessible: false,
key_env: "WH_KEY".to_string(),
key_url: "https://write-helper.example.com/keys".to_string(),
}
}
#[test]
fn write_user_preset_round_trips_through_additional_presets(
) -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let written = write_user_preset(&sample_test_preset("write-rt"))?;
assert_eq!(written.id, "write-rt");
assert!(
written.path.ends_with("providers.d/write-rt.toml"),
"file must land at <user_data_dir>/providers.d/<id>.toml, got {}",
written.path.display()
);
let loaded = additional_presets();
assert_eq!(loaded.len(), 1, "exactly the preset we wrote should load");
assert_eq!(loaded[0].id, "write-rt");
assert_eq!(loaded[0].api_base, "https://write-helper.example.com/v1");
assert_eq!(loaded[0].models.len(), 1);
assert_eq!(loaded[0].models[0].name, "wh-1");
Ok(())
}
#[test]
fn write_user_preset_overwrites_existing_file() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let _pin = crate::test_util::PinnedRecursiveHome::new(tmp.path().join(".recursive"));
let mut first = sample_test_preset("over");
first.default_model = "old".to_string();
write_user_preset(&first)?;
let mut second = sample_test_preset("over");
second.default_model = "new".to_string();
write_user_preset(&second)?;
let loaded = additional_presets();
assert_eq!(loaded.len(), 1, "overwrite must not duplicate files");
assert_eq!(
loaded[0].default_model, "new",
"second write must replace the first"
);
Ok(())
}
}