use crate::provider::DriverId;
#[derive(Clone, Default, PartialEq, Eq)]
pub struct ProviderCredentials {
pub api_key: Option<String>,
pub base_url: Option<String>,
}
impl ProviderCredentials {
pub fn is_empty(&self) -> bool {
self.api_key.is_none() && self.base_url.is_none()
}
}
impl std::fmt::Debug for ProviderCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProviderCredentials")
.field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
.field("base_url", &self.base_url)
.finish()
}
}
pub trait CredentialProvider: Send + Sync {
fn resolve(&self, driver: &DriverId) -> Option<ProviderCredentials>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EnvCredentialProvider;
impl EnvCredentialProvider {
pub fn new() -> Self {
Self
}
fn resolve_with<F>(driver: &DriverId, lookup: F) -> Option<ProviderCredentials>
where
F: Fn(&str) -> Option<String>,
{
let stem = driver
.as_str()
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_uppercase()
} else {
'_'
}
})
.collect::<String>();
let key_var = format!("{stem}_API_KEY");
let url_var = format!("{stem}_BASE_URL");
let non_empty = |s: String| (!s.is_empty()).then_some(s);
let mut api_key = lookup(&key_var).and_then(non_empty);
let mut base_url = lookup(&url_var).and_then(non_empty);
if driver == &DriverId::OpenAICompletions {
api_key = api_key.or_else(|| lookup("OPENAI_API_KEY").and_then(non_empty));
base_url = base_url.or_else(|| lookup("OPENAI_BASE_URL").and_then(non_empty));
}
let creds = ProviderCredentials { api_key, base_url };
(!creds.is_empty()).then_some(creds)
}
}
impl CredentialProvider for EnvCredentialProvider {
fn resolve(&self, driver: &DriverId) -> Option<ProviderCredentials> {
Self::resolve_with(driver, |name| std::env::var(name).ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn resolve(driver: &str, entries: &[(&str, &str)]) -> Option<ProviderCredentials> {
EnvCredentialProvider::resolve_with(&DriverId::external(driver), |key| {
entries
.iter()
.find(|(name, _)| *name == key)
.map(|(_, value)| value.to_string())
})
}
fn credentials(key: Option<&str>, url: Option<&str>) -> Option<ProviderCredentials> {
Some(ProviderCredentials {
api_key: key.map(str::to_owned),
base_url: url.map(str::to_owned),
})
}
#[test]
fn explicit_driver_environment_names_preserve_both_fields_and_isolate_other_keys() {
for (driver, key_name, url_name) in [
("openai", "OPENAI_API_KEY", "OPENAI_BASE_URL"),
("anthropic", "ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"),
("gemini", "GEMINI_API_KEY", "GEMINI_BASE_URL"),
(
"custom-driver.v2",
"CUSTOM_DRIVER_V2_API_KEY",
"CUSTOM_DRIVER_V2_BASE_URL",
),
] {
let entries = [
(key_name, "key"),
(url_name, "https://proxy.example/v1"),
("OTHER_API_KEY", "unrelated"),
];
assert_eq!(
resolve(driver, &entries),
credentials(Some("key"), Some("https://proxy.example/v1")),
"{driver}"
);
assert_eq!(
resolve(driver, &[(url_name, "https://proxy.example/v1")]),
credentials(None, Some("https://proxy.example/v1"))
);
assert_eq!(resolve(driver, &[(key_name, ""), (url_name, "")]), None);
assert_eq!(resolve(driver, &[]), None);
}
for driver in ["bedrock", "llmsim"] {
assert_eq!(
resolve(
driver,
&[
("AWS_ACCESS_KEY_ID", "unrelated"),
("OPENAI_API_KEY", "other provider")
]
),
None
);
}
}
#[test]
fn completions_fallback_is_independent_per_field_and_never_overrides_specific_values() {
for (specific_key, specific_url, expected_key, expected_url) in [
(None, None, "legacy-key", "https://legacy.example/v1"),
(
Some(""),
Some(""),
"legacy-key",
"https://legacy.example/v1",
),
(
Some("specific-key"),
None,
"specific-key",
"https://legacy.example/v1",
),
(
None,
Some("https://specific.example/v1"),
"legacy-key",
"https://specific.example/v1",
),
(
Some("specific-key"),
Some("https://specific.example/v1"),
"specific-key",
"https://specific.example/v1",
),
] {
let mut entries = vec![
("OPENAI_API_KEY", "legacy-key"),
("OPENAI_BASE_URL", "https://legacy.example/v1"),
];
if let Some(key) = specific_key {
entries.push(("OPENAI_COMPLETIONS_API_KEY", key));
}
if let Some(url) = specific_url {
entries.push(("OPENAI_COMPLETIONS_BASE_URL", url));
}
assert_eq!(
resolve("openai_completions", &entries),
credentials(Some(expected_key), Some(expected_url))
);
}
assert_eq!(
resolve("openai_completions", &[("OPENAI_API_KEY", "legacy-key")]),
credentials(Some("legacy-key"), None)
);
assert_eq!(
resolve(
"openai_completions",
&[("OPENAI_API_KEY", ""), ("OPENAI_BASE_URL", "")]
),
None
);
}
#[test]
fn debug_output_redacts_the_api_key() {
for api_key in [None, Some(""), Some("sk-super-secret")] {
let creds = ProviderCredentials {
api_key: api_key.map(str::to_owned),
base_url: Some("https://proxy.example/v1".into()),
};
let expected = if api_key.is_some() {
"ProviderCredentials { api_key: Some(\"[REDACTED]\"), base_url: Some(\"https://proxy.example/v1\") }"
} else {
"ProviderCredentials { api_key: None, base_url: Some(\"https://proxy.example/v1\") }"
};
assert_eq!(format!("{creds:?}"), expected);
}
}
}