1use anyhow::Result;
9use std::env;
10use std::str::FromStr;
11
12use crate::auth::CustomApiKeyStorage;
13use crate::models::Provider;
14
15#[derive(Debug, Clone)]
17pub struct ApiKeySources {
18 pub gemini_env: String,
20 pub anthropic_env: String,
22 pub openai_env: String,
24 pub openrouter_env: String,
26 pub xai_env: String,
28 pub deepseek_env: String,
30 pub zai_env: String,
32 pub ollama_env: String,
34 pub lmstudio_env: String,
36 pub gemini_config: Option<String>,
38 pub anthropic_config: Option<String>,
40 pub openai_config: Option<String>,
42 pub openrouter_config: Option<String>,
44 pub xai_config: Option<String>,
46 pub deepseek_config: Option<String>,
48 pub zai_config: Option<String>,
50 pub ollama_config: Option<String>,
52 pub lmstudio_config: Option<String>,
54}
55
56impl Default for ApiKeySources {
57 fn default() -> Self {
58 Self {
59 gemini_env: "GEMINI_API_KEY".to_string(),
60 anthropic_env: "ANTHROPIC_API_KEY".to_string(),
61 openai_env: "OPENAI_API_KEY".to_string(),
62 openrouter_env: "OPENROUTER_API_KEY".to_string(),
63 xai_env: "XAI_API_KEY".to_string(),
64 deepseek_env: "DEEPSEEK_API_KEY".to_string(),
65 zai_env: "ZAI_API_KEY".to_string(),
66 ollama_env: "OLLAMA_API_KEY".to_string(),
67 lmstudio_env: "LMSTUDIO_API_KEY".to_string(),
68 gemini_config: None,
69 anthropic_config: None,
70 openai_config: None,
71 openrouter_config: None,
72 xai_config: None,
73 deepseek_config: None,
74 zai_config: None,
75 ollama_config: None,
76 lmstudio_config: None,
77 }
78 }
79}
80
81impl ApiKeySources {
82 pub fn for_provider(_provider: &str) -> Self {
84 Self::default()
85 }
86}
87
88fn inferred_api_key_env(provider: &str) -> &'static str {
89 Provider::from_str(provider)
90 .map(|resolved| resolved.default_api_key_env())
91 .unwrap_or("GEMINI_API_KEY")
92}
93
94pub fn load_dotenv() -> Result<()> {
100 match dotenvy::dotenv() {
101 Ok(path) => {
102 if std::env::var("VTCODE_VERBOSE").is_ok() || std::env::var("RUST_LOG").is_ok() {
104 tracing::info!("Loaded environment variables from: {}", path.display());
105 }
106 Ok(())
107 }
108 Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
109 Ok(())
111 }
112 Err(e) => {
113 tracing::warn!("Failed to load .env file: {}", e);
114 Ok(())
115 }
116 }
117}
118
119pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
138 let normalized_provider = provider.to_lowercase();
139 let inferred_env = inferred_api_key_env(&normalized_provider);
141
142 if let Ok(key) = env::var(inferred_env)
144 && !key.is_empty()
145 {
146 return Ok(key);
147 }
148
149 if let Ok(Some(key)) = get_custom_api_key_from_keyring(&normalized_provider) {
151 return Ok(key);
152 }
153
154 match normalized_provider.as_str() {
156 "gemini" => get_gemini_api_key(sources),
157 "anthropic" => get_anthropic_api_key(sources),
158 "openai" => get_openai_api_key(sources),
159 "deepseek" => get_deepseek_api_key(sources),
160 "openrouter" => get_openrouter_api_key(sources),
161 "xai" => get_xai_api_key(sources),
162 "zai" => get_zai_api_key(sources),
163 "ollama" => get_ollama_api_key(sources),
164 "lmstudio" => get_lmstudio_api_key(sources),
165 "huggingface" => env::var("HF_TOKEN").map_err(|_| anyhow::anyhow!("HF_TOKEN not set")),
166 _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
167 }
168}
169
170fn get_custom_api_key_from_keyring(provider: &str) -> Result<Option<String>> {
183 let storage = CustomApiKeyStorage::new(provider);
184 let mode = crate::auth::AuthCredentialsStoreMode::default();
186 storage.load(mode)
187}
188
189fn get_api_key_with_fallback(
191 env_var: &str,
192 config_value: Option<&String>,
193 provider_name: &str,
194) -> Result<String> {
195 if let Ok(key) = env::var(env_var)
197 && !key.is_empty()
198 {
199 return Ok(key);
200 }
201
202 if let Some(key) = config_value
204 && !key.is_empty()
205 {
206 return Ok(key.clone());
207 }
208
209 Err(anyhow::anyhow!(
211 "No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
212 provider_name,
213 env_var
214 ))
215}
216
217fn get_optional_api_key_with_fallback(env_var: &str, config_value: Option<&String>) -> String {
218 if let Ok(key) = env::var(env_var)
219 && !key.is_empty()
220 {
221 return key;
222 }
223
224 if let Some(key) = config_value
225 && !key.is_empty()
226 {
227 return key.clone();
228 }
229
230 String::new()
231}
232
233fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
235 if let Ok(key) = env::var(&sources.gemini_env)
237 && !key.is_empty()
238 {
239 return Ok(key);
240 }
241
242 if let Ok(key) = env::var("GOOGLE_API_KEY")
244 && !key.is_empty()
245 {
246 return Ok(key);
247 }
248
249 if let Some(key) = &sources.gemini_config
251 && !key.is_empty()
252 {
253 return Ok(key.clone());
254 }
255
256 Err(anyhow::anyhow!(
258 "No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
259 sources.gemini_env
260 ))
261}
262
263fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
265 get_api_key_with_fallback(
266 &sources.anthropic_env,
267 sources.anthropic_config.as_ref(),
268 "Anthropic",
269 )
270}
271
272fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
274 get_api_key_with_fallback(
275 &sources.openai_env,
276 sources.openai_config.as_ref(),
277 "OpenAI",
278 )
279}
280
281fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
288 if let Ok(Some(token)) = crate::auth::load_oauth_token() {
290 tracing::debug!("Using OAuth token for OpenRouter authentication");
291 return Ok(token.api_key);
292 }
293
294 get_api_key_with_fallback(
296 &sources.openrouter_env,
297 sources.openrouter_config.as_ref(),
298 "OpenRouter",
299 )
300}
301
302fn get_xai_api_key(sources: &ApiKeySources) -> Result<String> {
304 get_api_key_with_fallback(&sources.xai_env, sources.xai_config.as_ref(), "xAI")
305}
306
307fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
309 get_api_key_with_fallback(
310 &sources.deepseek_env,
311 sources.deepseek_config.as_ref(),
312 "DeepSeek",
313 )
314}
315
316fn get_zai_api_key(sources: &ApiKeySources) -> Result<String> {
318 get_api_key_with_fallback(&sources.zai_env, sources.zai_config.as_ref(), "Z.AI")
319}
320
321fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
323 Ok(get_optional_api_key_with_fallback(
326 &sources.ollama_env,
327 sources.ollama_config.as_ref(),
328 ))
329}
330
331fn get_lmstudio_api_key(sources: &ApiKeySources) -> Result<String> {
333 Ok(get_optional_api_key_with_fallback(
334 &sources.lmstudio_env,
335 sources.lmstudio_config.as_ref(),
336 ))
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use std::env;
343
344 #[test]
345 fn test_get_gemini_api_key_from_env() {
346 unsafe {
348 env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
349 }
350
351 let sources = ApiKeySources {
352 gemini_env: "TEST_GEMINI_KEY".to_string(),
353 ..Default::default()
354 };
355
356 let result = get_gemini_api_key(&sources);
357 assert!(result.is_ok());
358 assert_eq!(result.unwrap(), "test-gemini-key");
359
360 unsafe {
362 env::remove_var("TEST_GEMINI_KEY");
363 }
364 }
365
366 #[test]
367 fn test_get_anthropic_api_key_from_env() {
368 unsafe {
370 env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
371 }
372
373 let sources = ApiKeySources {
374 anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
375 ..Default::default()
376 };
377
378 let result = get_anthropic_api_key(&sources);
379 assert!(result.is_ok());
380 assert_eq!(result.unwrap(), "test-anthropic-key");
381
382 unsafe {
384 env::remove_var("TEST_ANTHROPIC_KEY");
385 }
386 }
387
388 #[test]
389 fn test_get_openai_api_key_from_env() {
390 unsafe {
392 env::set_var("TEST_OPENAI_KEY", "test-openai-key");
393 }
394
395 let sources = ApiKeySources {
396 openai_env: "TEST_OPENAI_KEY".to_string(),
397 ..Default::default()
398 };
399
400 let result = get_openai_api_key(&sources);
401 assert!(result.is_ok());
402 assert_eq!(result.unwrap(), "test-openai-key");
403
404 unsafe {
406 env::remove_var("TEST_OPENAI_KEY");
407 }
408 }
409
410 #[test]
411 fn test_get_deepseek_api_key_from_env() {
412 unsafe {
413 env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
414 }
415
416 let sources = ApiKeySources {
417 deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
418 ..Default::default()
419 };
420
421 let result = get_deepseek_api_key(&sources);
422 assert!(result.is_ok());
423 assert_eq!(result.unwrap(), "test-deepseek-key");
424
425 unsafe {
426 env::remove_var("TEST_DEEPSEEK_KEY");
427 }
428 }
429
430 #[test]
431 fn test_get_xai_api_key_from_env() {
432 unsafe {
433 env::set_var("TEST_XAI_KEY", "test-xai-key");
434 }
435
436 let sources = ApiKeySources {
437 xai_env: "TEST_XAI_KEY".to_string(),
438 ..Default::default()
439 };
440
441 let result = get_xai_api_key(&sources);
442 assert!(result.is_ok());
443 assert_eq!(result.unwrap(), "test-xai-key");
444
445 unsafe {
446 env::remove_var("TEST_XAI_KEY");
447 }
448 }
449
450 #[test]
451 fn test_get_gemini_api_key_from_config() {
452 let prior_gemini_key = env::var("TEST_GEMINI_CONFIG_KEY").ok();
453 let prior_google_key = env::var("GOOGLE_API_KEY").ok();
454
455 unsafe {
456 env::remove_var("TEST_GEMINI_CONFIG_KEY");
457 env::remove_var("GOOGLE_API_KEY");
458 }
459
460 let sources = ApiKeySources {
461 gemini_env: "TEST_GEMINI_CONFIG_KEY".to_string(),
462 gemini_config: Some("config-gemini-key".to_string()),
463 ..Default::default()
464 };
465
466 let result = get_gemini_api_key(&sources);
467 assert!(result.is_ok());
468 assert_eq!(result.unwrap(), "config-gemini-key");
469
470 unsafe {
471 if let Some(value) = prior_gemini_key {
472 env::set_var("TEST_GEMINI_CONFIG_KEY", value);
473 } else {
474 env::remove_var("TEST_GEMINI_CONFIG_KEY");
475 }
476 if let Some(value) = prior_google_key {
477 env::set_var("GOOGLE_API_KEY", value);
478 } else {
479 env::remove_var("GOOGLE_API_KEY");
480 }
481 }
482 }
483
484 #[test]
485 fn test_get_api_key_with_fallback_prefers_env() {
486 unsafe {
488 env::set_var("TEST_FALLBACK_KEY", "env-key");
489 }
490
491 let sources = ApiKeySources {
492 openai_env: "TEST_FALLBACK_KEY".to_string(),
493 openai_config: Some("config-key".to_string()),
494 ..Default::default()
495 };
496
497 let result = get_openai_api_key(&sources);
498 assert!(result.is_ok());
499 assert_eq!(result.unwrap(), "env-key"); unsafe {
503 env::remove_var("TEST_FALLBACK_KEY");
504 }
505 }
506
507 #[test]
508 fn test_get_api_key_fallback_to_config() {
509 let sources = ApiKeySources {
510 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
511 openai_config: Some("config-key".to_string()),
512 ..Default::default()
513 };
514
515 let result = get_openai_api_key(&sources);
516 assert!(result.is_ok());
517 assert_eq!(result.unwrap(), "config-key");
518 }
519
520 #[test]
521 fn test_get_api_key_error_when_not_found() {
522 let sources = ApiKeySources {
523 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
524 ..Default::default()
525 };
526
527 let result = get_openai_api_key(&sources);
528 assert!(result.is_err());
529 }
530
531 #[test]
532 fn test_get_ollama_api_key_missing_sources() {
533 let sources = ApiKeySources {
534 ollama_env: "NONEXISTENT_OLLAMA_ENV".to_string(),
535 ..Default::default()
536 };
537
538 let result = get_ollama_api_key(&sources).expect("Ollama key retrieval should succeed");
539 assert!(result.is_empty());
540 }
541
542 #[test]
543 fn test_get_ollama_api_key_from_env() {
544 unsafe {
546 env::set_var("TEST_OLLAMA_KEY", "test-ollama-key");
547 }
548
549 let sources = ApiKeySources {
550 ollama_env: "TEST_OLLAMA_KEY".to_string(),
551 ..Default::default()
552 };
553
554 let result = get_ollama_api_key(&sources);
555 assert!(result.is_ok());
556 assert_eq!(result.unwrap(), "test-ollama-key");
557
558 unsafe {
560 env::remove_var("TEST_OLLAMA_KEY");
561 }
562 }
563
564 #[test]
565 fn test_get_lmstudio_api_key_missing_sources() {
566 let sources = ApiKeySources {
567 lmstudio_env: "NONEXISTENT_LMSTUDIO_ENV".to_string(),
568 ..Default::default()
569 };
570
571 let result =
572 get_lmstudio_api_key(&sources).expect("LM Studio key retrieval should succeed");
573 assert!(result.is_empty());
574 }
575
576 #[test]
577 fn test_get_lmstudio_api_key_from_env() {
578 unsafe {
579 env::set_var("TEST_LMSTUDIO_KEY", "test-lmstudio-key");
580 }
581
582 let sources = ApiKeySources {
583 lmstudio_env: "TEST_LMSTUDIO_KEY".to_string(),
584 ..Default::default()
585 };
586
587 let result = get_lmstudio_api_key(&sources);
588 assert!(result.is_ok());
589 assert_eq!(result.unwrap(), "test-lmstudio-key");
590
591 unsafe {
592 env::remove_var("TEST_LMSTUDIO_KEY");
593 }
594 }
595
596 #[test]
597 fn test_get_api_key_ollama_provider() {
598 unsafe {
600 env::set_var("OLLAMA_API_KEY", "test-ollama-env-key");
601 }
602
603 let sources = ApiKeySources::default();
604 let result = get_api_key("ollama", &sources);
605 assert!(result.is_ok());
606 assert_eq!(result.unwrap(), "test-ollama-env-key");
607
608 unsafe {
610 env::remove_var("OLLAMA_API_KEY");
611 }
612 }
613
614 #[test]
615 fn test_get_api_key_lmstudio_provider() {
616 unsafe {
617 env::set_var("LMSTUDIO_API_KEY", "test-lmstudio-env-key");
618 }
619
620 let sources = ApiKeySources::default();
621 let result = get_api_key("lmstudio", &sources);
622 assert!(result.is_ok());
623 assert_eq!(result.unwrap(), "test-lmstudio-env-key");
624
625 unsafe {
626 env::remove_var("LMSTUDIO_API_KEY");
627 }
628 }
629}