vtcode_config/core/
provider_override.rs1use serde::{Deserialize, Serialize};
2
3#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
23pub struct ProviderOverrideConfig {
24 #[serde(default)]
29 pub models: Vec<String>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub base_url: Option<String>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub api_key_env: Option<String>,
46}
47
48impl ProviderOverrideConfig {
49 pub(crate) fn validate(&self, provider_name: &str) -> Result<(), String> {
52 let mut seen = std::collections::HashSet::new();
53 for model in &self.models {
54 let trimmed = model.trim();
55 if trimmed.is_empty() {
56 return Err(format!("providers[{provider_name}]: `models` entries must not be empty"));
57 }
58 if !seen.insert(trimmed.to_lowercase()) {
59 return Err(format!("providers[{provider_name}]: duplicate model `{trimmed}`"));
60 }
61 }
62 if let Some(base_url) = &self.base_url
63 && base_url.trim().is_empty()
64 {
65 return Err(format!("providers[{provider_name}]: `base_url` must not be empty"));
66 }
67 if let Some(api_key_env) = &self.api_key_env
68 && api_key_env.trim().is_empty()
69 {
70 return Err(format!("providers[{provider_name}]: `api_key_env` must not be empty"));
71 }
72 if let Some(api_key_env) = &self.api_key_env
73 && let Err(err) = crate::auth::CredentialIdentity::new(provider_name, api_key_env)
74 {
75 return Err(format!("providers[{provider_name}]: invalid `api_key_env`: {err}"));
76 }
77 Ok(())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::ProviderOverrideConfig;
84
85 #[test]
86 fn default_config_is_empty() {
87 let config = ProviderOverrideConfig::default();
88 assert!(config.models.is_empty());
89 assert!(config.base_url.is_none());
90 assert!(config.api_key_env.is_none());
91 }
92
93 #[test]
94 fn validate_accepts_valid_config() {
95 let config = ProviderOverrideConfig {
96 models: vec!["model-a".to_string(), "model-b".to_string()],
97 base_url: Some("https://example.com".to_string()),
98 api_key_env: Some("MY_KEY".to_string()),
99 };
100 assert!(config.validate("test-provider").is_ok());
101 }
102
103 #[test]
104 fn validate_rejects_empty_model_entry() {
105 let config = ProviderOverrideConfig {
106 models: vec!["model-a".to_string(), " ".to_string()],
107 base_url: None,
108 api_key_env: None,
109 };
110 let err = config.validate("test-provider").expect_err("blank model should fail");
111 assert!(err.contains("`models` entries must not be empty"));
112 }
113
114 #[test]
115 fn validate_rejects_empty_base_url() {
116 let config = ProviderOverrideConfig {
117 models: vec!["model-a".to_string()],
118 base_url: Some(" ".to_string()),
119 api_key_env: None,
120 };
121 let err = config.validate("test-provider").expect_err("blank base_url should fail");
122 assert!(err.contains("`base_url` must not be empty"));
123 }
124
125 #[test]
126 fn validate_rejects_empty_api_key_env() {
127 let config = ProviderOverrideConfig {
128 models: vec!["model-a".to_string()],
129 base_url: None,
130 api_key_env: Some(" ".to_string()),
131 };
132 let err = config.validate("test-provider").expect_err("blank api_key_env should fail");
133 assert!(err.contains("`api_key_env` must not be empty"));
134 }
135
136 #[test]
137 fn validate_rejects_duplicate_models() {
138 let config = ProviderOverrideConfig {
139 models: vec!["model-a".to_string(), "model-b".to_string(), "model-a".to_string()],
140 base_url: None,
141 api_key_env: None,
142 };
143 let err = config.validate("test-provider").expect_err("duplicate model should fail");
144 assert!(err.contains("duplicate model"));
145 }
146
147 #[test]
148 fn validate_rejects_duplicate_models_case_insensitive() {
149 let config = ProviderOverrideConfig {
150 models: vec!["Model-A".to_string(), "model-a".to_string()],
151 base_url: None,
152 api_key_env: None,
153 };
154 let err = config
155 .validate("test-provider")
156 .expect_err("case-insensitive duplicate should fail");
157 assert!(err.contains("duplicate model"));
158 }
159}