everruns_core/
credential_provider.rs1use crate::provider::DriverId;
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct ProviderCredentials {
23 pub api_key: Option<String>,
25 pub base_url: Option<String>,
27}
28
29impl ProviderCredentials {
30 pub fn is_empty(&self) -> bool {
32 self.api_key.is_none() && self.base_url.is_none()
33 }
34}
35
36pub trait CredentialProvider: Send + Sync {
43 fn resolve(&self, driver: &DriverId) -> Option<ProviderCredentials>;
46}
47
48#[derive(Debug, Clone, Copy, Default)]
72pub struct EnvCredentialProvider;
73
74impl EnvCredentialProvider {
75 pub fn new() -> Self {
77 Self
78 }
79
80 fn resolve_with<F>(driver: &DriverId, lookup: F) -> Option<ProviderCredentials>
83 where
84 F: Fn(&str) -> Option<String>,
85 {
86 let (key_var, url_var) = match driver {
87 DriverId::OpenAI | DriverId::OpenAICompletions => ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
88 DriverId::OpenRouter => ("OPENROUTER_API_KEY", "OPENROUTER_BASE_URL"),
89 DriverId::Anthropic => ("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"),
90 DriverId::Gemini => ("GEMINI_API_KEY", "GEMINI_BASE_URL"),
91 _ => return None,
92 };
93
94 let non_empty = |s: String| (!s.is_empty()).then_some(s);
95 let api_key = lookup(key_var).and_then(non_empty);
96 let base_url = lookup(url_var).and_then(non_empty);
97
98 let creds = ProviderCredentials { api_key, base_url };
99 (!creds.is_empty()).then_some(creds)
100 }
101}
102
103impl CredentialProvider for EnvCredentialProvider {
104 fn resolve(&self, driver: &DriverId) -> Option<ProviderCredentials> {
105 Self::resolve_with(driver, |name| std::env::var(name).ok())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::collections::HashMap;
113
114 fn lookup_from(map: &HashMap<&'static str, &'static str>) -> impl Fn(&str) -> Option<String> {
115 let owned: HashMap<String, String> = map
116 .iter()
117 .map(|(k, v)| (k.to_string(), v.to_string()))
118 .collect();
119 move |name: &str| owned.get(name).cloned()
120 }
121
122 #[test]
123 fn openai_reads_key_and_base_url() {
124 let env = HashMap::from([
125 ("OPENAI_API_KEY", "sk-test"),
126 ("OPENAI_BASE_URL", "https://proxy.example/v1"),
127 ]);
128 let creds = EnvCredentialProvider::resolve_with(&DriverId::OpenAI, lookup_from(&env))
129 .expect("credentials");
130 assert_eq!(creds.api_key.as_deref(), Some("sk-test"));
131 assert_eq!(creds.base_url.as_deref(), Some("https://proxy.example/v1"));
132 }
133
134 #[test]
135 fn openai_completions_shares_openai_key() {
136 let env = HashMap::from([("OPENAI_API_KEY", "sk-test")]);
137 let creds =
138 EnvCredentialProvider::resolve_with(&DriverId::OpenAICompletions, lookup_from(&env))
139 .expect("credentials");
140 assert_eq!(creds.api_key.as_deref(), Some("sk-test"));
141 assert!(creds.base_url.is_none());
142 }
143
144 #[test]
145 fn anthropic_and_gemini_keys() {
146 let env = HashMap::from([("ANTHROPIC_API_KEY", "sk-ant"), ("GEMINI_API_KEY", "g-key")]);
147 let lookup = lookup_from(&env);
148 assert_eq!(
149 EnvCredentialProvider::resolve_with(&DriverId::Anthropic, &lookup)
150 .and_then(|c| c.api_key)
151 .as_deref(),
152 Some("sk-ant"),
153 );
154 assert_eq!(
155 EnvCredentialProvider::resolve_with(&DriverId::Gemini, &lookup)
156 .and_then(|c| c.api_key)
157 .as_deref(),
158 Some("g-key"),
159 );
160 }
161
162 #[test]
163 fn empty_or_missing_yields_none() {
164 let env = HashMap::from([("OPENAI_API_KEY", "")]);
165 assert!(
166 EnvCredentialProvider::resolve_with(&DriverId::OpenAI, lookup_from(&env)).is_none()
167 );
168
169 let empty = HashMap::new();
170 assert!(
171 EnvCredentialProvider::resolve_with(&DriverId::Anthropic, lookup_from(&empty))
172 .is_none()
173 );
174 }
175
176 #[test]
177 fn unsupported_drivers_return_none() {
178 let env = HashMap::from([("AWS_ACCESS_KEY_ID", "x")]);
179 let lookup = lookup_from(&env);
180 assert!(EnvCredentialProvider::resolve_with(&DriverId::Bedrock, &lookup).is_none());
181 assert!(EnvCredentialProvider::resolve_with(&DriverId::LlmSim, &lookup).is_none());
182 }
183}