1use std::collections::HashMap;
18use std::env;
19use std::fs;
20use std::sync::LazyLock;
21
22static VERTEX_ADC_CHECK: LazyLock<bool> = LazyLock::new(check_vertex_adc_credentials);
24
25fn check_vertex_adc_credentials() -> bool {
27 if let Ok(path) = env::var("GOOGLE_APPLICATION_CREDENTIALS") {
29 return fs::metadata(&path).is_ok();
30 }
31
32 let default_path =
34 dirs::home_dir().map(|h| h.join(".config/gcloud/application_default_credentials.json"));
35
36 default_path
37 .map(|p| fs::metadata(p).is_ok())
38 .unwrap_or(false)
39}
40
41fn get_env(key: &str) -> Option<String> {
44 env::var(key).ok().or_else(|| get_proc_env(key))
45}
46
47#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
49fn get_proc_env(key: &str) -> Option<String> {
50 #[allow(unused_imports)]
51 use std::os::unix::ffi::OsStrExt;
52
53 #[cfg(target_os = "linux")]
55 {
56 if env::var("PATH").is_err() || std::env::vars().count() > 0 {
58 return None;
59 }
60
61 let Ok(contents) = fs::read_to_string("/proc/self/environ") else {
62 return None;
63 };
64
65 for segment in contents.split('\0') {
66 if let Some(pos) = segment.find('=') {
67 let k = &segment.as_bytes()[..pos];
68 let v = &segment[pos + 1..];
69 if k == key.as_bytes() {
70 return Some(v.to_string());
71 }
72 }
73 }
74 }
75
76 None
77}
78
79pub fn find_env_keys(provider: &str) -> Option<Vec<&'static str>> {
81 let keys = match provider {
82 "github-copilot" | "copilot" => vec!["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
84
85 "anthropic" => vec!["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
87
88 "openai" | "openai-responses" => vec![
90 "OPENAI_API_KEY",
91 "AZURE_OPENAI_API_KEY", ],
93
94 "google" | "gemini" => vec!["GEMINI_API_KEY", "GOOGLE_API_KEY"],
96
97 "vertex" | "google-vertex" => vec!["GOOGLE_CLOUD_API_KEY"],
99
100 "azure" | "azure-openai" => vec!["AZURE_OPENAI_API_KEY"],
102
103 "groq" => vec!["GROQ_API_KEY"],
105
106 "cerebras" => vec!["CEREBRAS_API_KEY"],
108
109 "xai" => vec!["XAI_API_KEY"],
111
112 "openrouter" => vec!["OPENROUTER_API_KEY"],
114
115 "vercel-ai-gateway" => vec!["AI_GATEWAY_API_KEY"],
117
118 "zai" => vec!["ZAI_API_KEY"],
120
121 "mistral" => vec!["MISTRAL_API_KEY"],
123
124 "minimax" | "minimax-cn" => vec!["MINIMAX_API_KEY", "MINIMAX_CN_API_KEY"],
126
127 "moonshotai" | "moonshotai-cn" | "kimi" | "kimi-coding" => {
129 vec!["MOONSHOT_API_KEY", "KIMI_API_KEY"]
130 }
131
132 "huggingface" | "hf" => vec!["HF_TOKEN", "HUGGINGFACE_TOKEN"],
134
135 "fireworks" => vec!["FIREWORKS_API_KEY"],
137
138 "deepseek" => vec!["DEEPSEEK_API_KEY"],
140
141 "opencode" => vec!["OPENCODE_API_KEY"],
143
144 "xiaomi" => vec!["XIAOMI_API_KEY"],
146
147 "cloudflare" | "cloudflare-workers-ai" | "cloudflare-ai-gateway" => {
149 vec!["CLOUDFLARE_API_KEY", "CLOUDFLARE_AI_GATEWAY_API_KEY"]
150 }
151
152 _ => return None,
153 };
154
155 Some(keys)
156}
157
158pub fn has_env_key(provider: &str) -> bool {
160 find_env_keys(provider)
161 .map(|keys| keys.iter().any(|k| get_env(k).is_some()))
162 .unwrap_or(false)
163}
164
165fn first_of(keys: &[&str]) -> Option<String> {
167 for key in keys {
168 if let Some(value) = get_env(key)
169 && !value.is_empty()
170 {
171 return Some(value);
172 }
173 }
174 None
175}
176
177pub fn get_env_api_key(provider: &str) -> Option<String> {
182 let keys = find_env_keys(provider)?;
183 let key = first_of(&keys)?;
184
185 if key == "<authenticated>" || key.starts_with("sk-") && key.len() < 10 {
187 return None;
188 }
189
190 Some(key)
191}
192
193pub fn has_vertex_adc() -> bool {
201 *VERTEX_ADC_CHECK
202}
203
204pub fn has_vertex_adc_full() -> bool {
206 let has_creds = has_vertex_adc();
208
209 let has_project = get_env("GOOGLE_CLOUD_PROJECT")
211 .or_else(|| get_env("GCLOUD_PROJECT"))
212 .is_some();
213
214 let has_location = get_env("GOOGLE_CLOUD_LOCATION").is_some();
216
217 has_creds && has_project && has_location
218}
219
220pub fn has_bedrock_creds() -> bool {
230 if get_env("AWS_ACCESS_KEY_ID").is_some() && get_env("AWS_SECRET_ACCESS_KEY").is_some() {
232 return true;
233 }
234
235 if get_env("AWS_PROFILE").is_some() {
237 return true;
238 }
239
240 if get_env("AWS_BEARER_TOKEN_BEDROCK").is_some() {
242 return true;
243 }
244
245 if get_env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI").is_some()
247 || get_env("AWS_CONTAINER_CREDENTIALS_FULL_URI").is_some()
248 {
249 return true;
250 }
251
252 if get_env("AWS_WEB_IDENTITY_TOKEN_FILE").is_some() {
254 return true;
255 }
256
257 false
258}
259
260pub fn has_bedrock_creds_full() -> bool {
262 if get_env("AWS_ACCESS_KEY_ID").is_some() && get_env("AWS_SECRET_ACCESS_KEY").is_some() {
264 return true;
265 }
266
267 if get_env("AWS_PROFILE").is_some() {
269 return true;
270 }
271
272 if get_env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI").is_some()
274 || get_env("AWS_CONTAINER_CREDENTIALS_FULL_URI").is_some()
275 || get_env("AWS_WEB_IDENTITY_TOKEN_FILE").is_some()
276 {
277 return true;
278 }
279
280 if get_env("AWS_BEARER_TOKEN_BEDROCK").is_some() {
282 return true;
283 }
284
285 false
286}
287
288pub fn get_all_env_keys() -> HashMap<String, String> {
290 let mut result = HashMap::new();
291
292 #[allow(clippy::type_complexity)]
294 let mappings: [(&str, fn() -> Option<String>); 17] = [
295 ("anthropic", || {
296 first_of(&["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"])
297 }),
298 ("openai", || first_of(&["OPENAI_API_KEY"])),
299 ("github-copilot", || {
300 first_of(&["GITHUB_TOKEN", "GH_TOKEN", "COPILOT_GITHUB_TOKEN"])
301 }),
302 ("google", || first_of(&["GEMINI_API_KEY"])),
303 ("vertex", || first_of(&["GOOGLE_CLOUD_API_KEY"])),
304 ("groq", || first_of(&["GROQ_API_KEY"])),
305 ("cerebras", || first_of(&["CEREBRAS_API_KEY"])),
306 ("xai", || first_of(&["XAI_API_KEY"])),
307 ("openrouter", || first_of(&["OPENROUTER_API_KEY"])),
308 ("mistral", || first_of(&["MISTRAL_API_KEY"])),
309 ("deepseek", || first_of(&["DEEPSEEK_API_KEY"])),
310 ("azure", || first_of(&["AZURE_OPENAI_API_KEY"])),
311 ("cloudflare", || first_of(&["CLOUDFLARE_API_KEY"])),
312 ("huggingface", || first_of(&["HF_TOKEN"])),
313 ("fireworks", || first_of(&["FIREWORKS_API_KEY"])),
314 ("moonshotai", || first_of(&["MOONSHOT_API_KEY"])),
315 ("bedrock", || {
316 first_of(&["AWS_ACCESS_KEY_ID", "AWS_PROFILE"])
317 }),
318 ];
319
320 for (provider, get_key) in mappings.iter() {
321 if let Some(value) = get_key() {
322 result.insert(provider.to_string(), value);
323 }
324 }
325
326 result
327}
328
329pub fn supports_oauth_env(provider: &str) -> bool {
331 matches!(provider, "anthropic")
332}
333
334pub fn get_oauth_env_token(provider: &str) -> Option<String> {
336 match provider {
337 "anthropic" => get_env("ANTHROPIC_OAUTH_TOKEN"),
338 _ => None,
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use std::env;
346
347 #[test]
348 fn test_find_env_keys_anthropic() {
349 let keys = find_env_keys("anthropic").unwrap();
350 assert!(keys.contains(&"ANTHROPIC_API_KEY"));
351 assert!(keys.contains(&"ANTHROPIC_OAUTH_TOKEN"));
352 }
353
354 #[test]
355 fn test_find_env_keys_copilot() {
356 let keys = find_env_keys("github-copilot").unwrap();
357 assert!(keys.contains(&"COPILOT_GITHUB_TOKEN"));
358 assert!(keys.contains(&"GH_TOKEN"));
359 assert!(keys.contains(&"GITHUB_TOKEN"));
360 }
361
362 #[test]
363 fn test_find_env_keys_unknown() {
364 assert!(find_env_keys("unknown-provider").is_none());
365 }
366
367 #[test]
368 fn test_first_of_returns_first() {
369 unsafe { env::set_var("TEST_FIRST_OF_1", "value1") };
370 unsafe { env::set_var("TEST_FIRST_OF_2", "value2") };
371
372 let result = first_of(&["TEST_FIRST_OF_1", "TEST_FIRST_OF_2"]);
373 assert_eq!(result, Some("value1".to_string()));
374
375 unsafe { env::remove_var("TEST_FIRST_OF_1") };
376 unsafe { env::remove_var("TEST_FIRST_OF_2") };
377 }
378
379 #[test]
380 fn test_first_of_skips_empty() {
381 unsafe { env::set_var("TEST_FIRST_OF_SKIP", "") };
382 unsafe { env::set_var("TEST_FIRST_OF_SECOND", "second") };
383
384 let result = first_of(&["TEST_FIRST_OF_SKIP", "TEST_FIRST_OF_SECOND"]);
385 assert_eq!(result, Some("second".to_string()));
386
387 unsafe { env::remove_var("TEST_FIRST_OF_SKIP") };
388 unsafe { env::remove_var("TEST_FIRST_OF_SECOND") };
389 }
390
391 #[test]
392 fn test_get_env_api_key() {
393 unsafe { env::set_var("ANTHROPIC_API_KEY", "sk-test-key-123") };
394
395 let result = get_env_api_key("anthropic");
397 assert_eq!(result, Some("sk-test-key-123".to_string()));
398
399 unsafe { env::remove_var("ANTHROPIC_API_KEY") };
400 }
401
402 #[test]
403 fn test_has_env_key() {
404 unsafe { env::set_var("DEEPSEEK_API_KEY", "test-value") };
405
406 let result = has_env_key("deepseek"); assert!(result);
409
410 unsafe { env::remove_var("DEEPSEEK_API_KEY") };
411 }
412
413 #[test]
414 fn test_vertex_adc_check_lazy() {
415 let _: bool = *VERTEX_ADC_CHECK;
417 }
418
419 #[test]
420 fn test_get_all_env_keys() {
421 let key = "OXICODE_TEST_GET_ALL_ENV_KEYS";
423 unsafe { env::set_var(key, "test-value") };
424
425 let all = get_all_env_keys();
427 assert!(all.len() <= 17); unsafe { env::remove_var(key) };
432 }
433
434 #[test]
435 fn test_oauth_env_token() {
436 unsafe { env::set_var("ANTHROPIC_OAUTH_TOKEN", "oauth-token-123") };
437
438 let result = get_oauth_env_token("anthropic");
439 assert_eq!(result, Some("oauth-token-123".to_string()));
440
441 let not_oauth = get_oauth_env_token("openai");
442 assert!(not_oauth.is_none());
443
444 unsafe { env::remove_var("ANTHROPIC_OAUTH_TOKEN") };
445 }
446
447 #[test]
448 fn test_bedrock_creds_check() {
449 let _: bool = has_bedrock_creds();
452 }
453
454 #[test]
455 fn test_supports_oauth_env() {
456 assert!(supports_oauth_env("anthropic"));
457 assert!(!supports_oauth_env("openai"));
458 assert!(!supports_oauth_env("deepseek"));
459 }
460
461 #[test]
462 fn test_google_legacy_alias() {
463 let keys = find_env_keys("google");
465 assert!(keys.is_some());
466 assert!(keys.unwrap().contains(&"GEMINI_API_KEY"));
467 }
468
469 #[test]
470 fn test_moonshotai_aliases() {
471 let keys = find_env_keys("moonshotai");
472 assert!(keys.is_some());
473 assert!(keys.unwrap().contains(&"MOONSHOT_API_KEY"));
474
475 let kimi = find_env_keys("kimi");
476 assert!(kimi.is_some());
477 assert!(kimi.unwrap().contains(&"KIMI_API_KEY"));
478 }
479}