use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct ProviderOverrideConfig {
#[serde(default)]
pub models: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_env: Option<String>,
}
impl ProviderOverrideConfig {
pub fn validate(&self, provider_name: &str) -> Result<(), String> {
let mut seen = std::collections::HashSet::new();
for model in &self.models {
let trimmed = model.trim();
if trimmed.is_empty() {
return Err(format!(
"providers[{provider_name}]: `models` entries must not be empty"
));
}
if !seen.insert(trimmed.to_lowercase()) {
return Err(format!(
"providers[{provider_name}]: duplicate model `{trimmed}`"
));
}
}
if let Some(base_url) = &self.base_url {
if base_url.trim().is_empty() {
return Err(format!(
"providers[{provider_name}]: `base_url` must not be empty"
));
}
}
if let Some(api_key_env) = &self.api_key_env {
if api_key_env.trim().is_empty() {
return Err(format!(
"providers[{provider_name}]: `api_key_env` must not be empty"
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::ProviderOverrideConfig;
#[test]
fn default_config_is_empty() {
let config = ProviderOverrideConfig::default();
assert!(config.models.is_empty());
assert!(config.base_url.is_none());
assert!(config.api_key_env.is_none());
}
#[test]
fn validate_accepts_valid_config() {
let config = ProviderOverrideConfig {
models: vec!["model-a".to_string(), "model-b".to_string()],
base_url: Some("https://example.com".to_string()),
api_key_env: Some("MY_KEY".to_string()),
};
assert!(config.validate("test-provider").is_ok());
}
#[test]
fn validate_rejects_empty_model_entry() {
let config = ProviderOverrideConfig {
models: vec!["model-a".to_string(), " ".to_string()],
base_url: None,
api_key_env: None,
};
let err = config
.validate("test-provider")
.expect_err("blank model should fail");
assert!(err.contains("`models` entries must not be empty"));
}
#[test]
fn validate_rejects_empty_base_url() {
let config = ProviderOverrideConfig {
models: vec!["model-a".to_string()],
base_url: Some(" ".to_string()),
api_key_env: None,
};
let err = config
.validate("test-provider")
.expect_err("blank base_url should fail");
assert!(err.contains("`base_url` must not be empty"));
}
#[test]
fn validate_rejects_empty_api_key_env() {
let config = ProviderOverrideConfig {
models: vec!["model-a".to_string()],
base_url: None,
api_key_env: Some(" ".to_string()),
};
let err = config
.validate("test-provider")
.expect_err("blank api_key_env should fail");
assert!(err.contains("`api_key_env` must not be empty"));
}
#[test]
fn validate_rejects_duplicate_models() {
let config = ProviderOverrideConfig {
models: vec![
"model-a".to_string(),
"model-b".to_string(),
"model-a".to_string(),
],
base_url: None,
api_key_env: None,
};
let err = config
.validate("test-provider")
.expect_err("duplicate model should fail");
assert!(err.contains("duplicate model"));
}
#[test]
fn validate_rejects_duplicate_models_case_insensitive() {
let config = ProviderOverrideConfig {
models: vec!["Model-A".to_string(), "model-a".to_string()],
base_url: None,
api_key_env: None,
};
let err = config
.validate("test-provider")
.expect_err("case-insensitive duplicate should fail");
assert!(err.contains("duplicate model"));
}
}