1use anyhow::Result;
9use std::env;
10
11#[derive(Debug, Clone)]
13pub struct ApiKeySources {
14 pub gemini_env: String,
16 pub anthropic_env: String,
18 pub openai_env: String,
20 pub openrouter_env: String,
22 pub xai_env: String,
24 pub deepseek_env: String,
26 pub zai_env: String,
28 pub ollama_env: String,
30 pub lmstudio_env: String,
32 pub gemini_config: Option<String>,
34 pub anthropic_config: Option<String>,
36 pub openai_config: Option<String>,
38 pub openrouter_config: Option<String>,
40 pub xai_config: Option<String>,
42 pub deepseek_config: Option<String>,
44 pub zai_config: Option<String>,
46 pub ollama_config: Option<String>,
48 pub lmstudio_config: Option<String>,
50}
51
52impl Default for ApiKeySources {
53 fn default() -> Self {
54 Self {
55 gemini_env: "GEMINI_API_KEY".to_string(),
56 anthropic_env: "ANTHROPIC_API_KEY".to_string(),
57 openai_env: "OPENAI_API_KEY".to_string(),
58 openrouter_env: "OPENROUTER_API_KEY".to_string(),
59 xai_env: "XAI_API_KEY".to_string(),
60 deepseek_env: "DEEPSEEK_API_KEY".to_string(),
61 zai_env: "ZAI_API_KEY".to_string(),
62 ollama_env: "OLLAMA_API_KEY".to_string(),
63 lmstudio_env: "LMSTUDIO_API_KEY".to_string(),
64 gemini_config: None,
65 anthropic_config: None,
66 openai_config: None,
67 openrouter_config: None,
68 xai_config: None,
69 deepseek_config: None,
70 zai_config: None,
71 ollama_config: None,
72 lmstudio_config: None,
73 }
74 }
75}
76
77impl ApiKeySources {
78 pub fn for_provider(provider: &str) -> Self {
80 let (primary_env, _fallback_envs) = match provider.to_lowercase().as_str() {
81 "gemini" => ("GEMINI_API_KEY", vec!["GOOGLE_API_KEY"]),
82 "anthropic" => ("ANTHROPIC_API_KEY", vec![]),
83 "openai" => ("OPENAI_API_KEY", vec![]),
84 "deepseek" => ("DEEPSEEK_API_KEY", vec![]),
85 "openrouter" => ("OPENROUTER_API_KEY", vec![]),
86 "xai" => ("XAI_API_KEY", vec![]),
87 "zai" => ("ZAI_API_KEY", vec![]),
88 "ollama" => ("OLLAMA_API_KEY", vec![]),
89 "lmstudio" => ("LMSTUDIO_API_KEY", vec![]),
90 _ => ("GEMINI_API_KEY", vec!["GOOGLE_API_KEY"]),
91 };
92
93 Self {
95 gemini_env: if provider == "gemini" {
96 primary_env.to_string()
97 } else {
98 "GEMINI_API_KEY".to_string()
99 },
100 anthropic_env: if provider == "anthropic" {
101 primary_env.to_string()
102 } else {
103 "ANTHROPIC_API_KEY".to_string()
104 },
105 openai_env: if provider == "openai" {
106 primary_env.to_string()
107 } else {
108 "OPENAI_API_KEY".to_string()
109 },
110 openrouter_env: if provider == "openrouter" {
111 primary_env.to_string()
112 } else {
113 "OPENROUTER_API_KEY".to_string()
114 },
115 xai_env: if provider == "xai" {
116 primary_env.to_string()
117 } else {
118 "XAI_API_KEY".to_string()
119 },
120 deepseek_env: if provider == "deepseek" {
121 primary_env.to_string()
122 } else {
123 "DEEPSEEK_API_KEY".to_string()
124 },
125 zai_env: if provider == "zai" {
126 primary_env.to_string()
127 } else {
128 "ZAI_API_KEY".to_string()
129 },
130 ollama_env: if provider == "ollama" {
131 primary_env.to_string()
132 } else {
133 "OLLAMA_API_KEY".to_string()
134 },
135 lmstudio_env: if provider == "lmstudio" {
136 primary_env.to_string()
137 } else {
138 "LMSTUDIO_API_KEY".to_string()
139 },
140 gemini_config: None,
141 anthropic_config: None,
142 openai_config: None,
143 openrouter_config: None,
144 xai_config: None,
145 deepseek_config: None,
146 zai_config: None,
147 ollama_config: None,
148 lmstudio_config: None,
149 }
150 }
151}
152
153pub fn load_dotenv() -> Result<()> {
159 match dotenvy::dotenv() {
160 Ok(path) => {
161 if std::env::var("VTCODE_VERBOSE").is_ok() || std::env::var("RUST_LOG").is_ok() {
163 eprintln!("Loaded environment variables from: {}", path.display());
164 }
165 Ok(())
166 }
167 Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
168 Ok(())
170 }
171 Err(e) => {
172 eprintln!("Warning: Failed to load .env file: {}", e);
173 Ok(())
174 }
175 }
176}
177
178pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
197 let inferred_env = match provider.to_lowercase().as_str() {
199 "gemini" => "GEMINI_API_KEY",
200 "anthropic" => "ANTHROPIC_API_KEY",
201 "openai" => "OPENAI_API_KEY",
202 "deepseek" => "DEEPSEEK_API_KEY",
203 "openrouter" => "OPENROUTER_API_KEY",
204 "xai" => "XAI_API_KEY",
205 "zai" => "ZAI_API_KEY",
206 "ollama" => "OLLAMA_API_KEY",
207 "lmstudio" => "LMSTUDIO_API_KEY",
208 "huggingface" => "HF_TOKEN",
209 _ => "GEMINI_API_KEY",
210 };
211
212 if let Ok(key) = env::var(inferred_env)
214 && !key.is_empty()
215 {
216 return Ok(key);
217 }
218
219 match provider.to_lowercase().as_str() {
221 "gemini" => get_gemini_api_key(sources),
222 "anthropic" => get_anthropic_api_key(sources),
223 "openai" => get_openai_api_key(sources),
224 "deepseek" => get_deepseek_api_key(sources),
225 "openrouter" => get_openrouter_api_key(sources),
226 "xai" => get_xai_api_key(sources),
227 "zai" => get_zai_api_key(sources),
228 "ollama" => get_ollama_api_key(sources),
229 "lmstudio" => get_lmstudio_api_key(sources),
230 "huggingface" => env::var("HF_TOKEN").map_err(|_| anyhow::anyhow!("HF_TOKEN not set")),
231 _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
232 }
233}
234
235fn get_api_key_with_fallback(
237 env_var: &str,
238 config_value: Option<&String>,
239 provider_name: &str,
240) -> Result<String> {
241 if let Ok(key) = env::var(env_var)
243 && !key.is_empty()
244 {
245 return Ok(key);
246 }
247
248 if let Some(key) = config_value
250 && !key.is_empty()
251 {
252 return Ok(key.clone());
253 }
254
255 Err(anyhow::anyhow!(
257 "No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
258 provider_name,
259 env_var
260 ))
261}
262
263fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
265 if let Ok(key) = env::var(&sources.gemini_env)
267 && !key.is_empty()
268 {
269 return Ok(key);
270 }
271
272 if let Ok(key) = env::var("GOOGLE_API_KEY")
274 && !key.is_empty()
275 {
276 return Ok(key);
277 }
278
279 if let Some(key) = &sources.gemini_config
281 && !key.is_empty()
282 {
283 return Ok(key.clone());
284 }
285
286 Err(anyhow::anyhow!(
288 "No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
289 sources.gemini_env
290 ))
291}
292
293fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
295 get_api_key_with_fallback(
296 &sources.anthropic_env,
297 sources.anthropic_config.as_ref(),
298 "Anthropic",
299 )
300}
301
302fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
304 get_api_key_with_fallback(
305 &sources.openai_env,
306 sources.openai_config.as_ref(),
307 "OpenAI",
308 )
309}
310
311fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
318 if let Ok(Some(token)) = crate::auth::load_oauth_token() {
320 tracing::debug!("Using OAuth token for OpenRouter authentication");
321 return Ok(token.api_key);
322 }
323
324 get_api_key_with_fallback(
326 &sources.openrouter_env,
327 sources.openrouter_config.as_ref(),
328 "OpenRouter",
329 )
330}
331
332fn get_xai_api_key(sources: &ApiKeySources) -> Result<String> {
334 get_api_key_with_fallback(&sources.xai_env, sources.xai_config.as_ref(), "xAI")
335}
336
337fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
339 get_api_key_with_fallback(
340 &sources.deepseek_env,
341 sources.deepseek_config.as_ref(),
342 "DeepSeek",
343 )
344}
345
346fn get_zai_api_key(sources: &ApiKeySources) -> Result<String> {
348 get_api_key_with_fallback(&sources.zai_env, sources.zai_config.as_ref(), "Z.AI")
349}
350
351fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
353 if let Ok(key) = env::var(&sources.ollama_env)
357 && !key.is_empty()
358 {
359 return Ok(key);
360 }
361
362 if let Some(key) = sources.ollama_config.as_ref()
363 && !key.is_empty()
364 {
365 return Ok(key.clone());
366 }
367
368 Ok(String::new())
369}
370
371fn get_lmstudio_api_key(sources: &ApiKeySources) -> Result<String> {
373 if let Ok(key) = env::var(&sources.lmstudio_env)
374 && !key.is_empty()
375 {
376 return Ok(key);
377 }
378
379 if let Some(key) = sources.lmstudio_config.as_ref()
380 && !key.is_empty()
381 {
382 return Ok(key.clone());
383 }
384
385 Ok(String::new())
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use std::env;
392
393 #[test]
394 fn test_get_gemini_api_key_from_env() {
395 unsafe {
397 env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
398 }
399
400 let sources = ApiKeySources {
401 gemini_env: "TEST_GEMINI_KEY".to_string(),
402 ..Default::default()
403 };
404
405 let result = get_gemini_api_key(&sources);
406 assert!(result.is_ok());
407 assert_eq!(result.unwrap(), "test-gemini-key");
408
409 unsafe {
411 env::remove_var("TEST_GEMINI_KEY");
412 }
413 }
414
415 #[test]
416 fn test_get_anthropic_api_key_from_env() {
417 unsafe {
419 env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
420 }
421
422 let sources = ApiKeySources {
423 anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
424 ..Default::default()
425 };
426
427 let result = get_anthropic_api_key(&sources);
428 assert!(result.is_ok());
429 assert_eq!(result.unwrap(), "test-anthropic-key");
430
431 unsafe {
433 env::remove_var("TEST_ANTHROPIC_KEY");
434 }
435 }
436
437 #[test]
438 fn test_get_openai_api_key_from_env() {
439 unsafe {
441 env::set_var("TEST_OPENAI_KEY", "test-openai-key");
442 }
443
444 let sources = ApiKeySources {
445 openai_env: "TEST_OPENAI_KEY".to_string(),
446 ..Default::default()
447 };
448
449 let result = get_openai_api_key(&sources);
450 assert!(result.is_ok());
451 assert_eq!(result.unwrap(), "test-openai-key");
452
453 unsafe {
455 env::remove_var("TEST_OPENAI_KEY");
456 }
457 }
458
459 #[test]
460 fn test_get_deepseek_api_key_from_env() {
461 unsafe {
462 env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
463 }
464
465 let sources = ApiKeySources {
466 deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
467 ..Default::default()
468 };
469
470 let result = get_deepseek_api_key(&sources);
471 assert!(result.is_ok());
472 assert_eq!(result.unwrap(), "test-deepseek-key");
473
474 unsafe {
475 env::remove_var("TEST_DEEPSEEK_KEY");
476 }
477 }
478
479 #[test]
480 fn test_get_xai_api_key_from_env() {
481 unsafe {
482 env::set_var("TEST_XAI_KEY", "test-xai-key");
483 }
484
485 let sources = ApiKeySources {
486 xai_env: "TEST_XAI_KEY".to_string(),
487 ..Default::default()
488 };
489
490 let result = get_xai_api_key(&sources);
491 assert!(result.is_ok());
492 assert_eq!(result.unwrap(), "test-xai-key");
493
494 unsafe {
495 env::remove_var("TEST_XAI_KEY");
496 }
497 }
498
499 #[test]
500 fn test_get_gemini_api_key_from_config() {
501 let prior_gemini_key = env::var("TEST_GEMINI_KEY").ok();
502 let prior_google_key = env::var("GOOGLE_API_KEY").ok();
503
504 unsafe {
505 env::remove_var("TEST_GEMINI_KEY");
506 env::remove_var("GOOGLE_API_KEY");
507 }
508
509 let sources = ApiKeySources {
510 gemini_env: "TEST_GEMINI_KEY".to_string(),
511 gemini_config: Some("config-gemini-key".to_string()),
512 ..Default::default()
513 };
514
515 let result = get_gemini_api_key(&sources);
516 assert!(result.is_ok());
517 assert_eq!(result.unwrap(), "config-gemini-key");
518
519 unsafe {
520 if let Some(value) = prior_gemini_key {
521 env::set_var("TEST_GEMINI_KEY", value);
522 } else {
523 env::remove_var("TEST_GEMINI_KEY");
524 }
525 if let Some(value) = prior_google_key {
526 env::set_var("GOOGLE_API_KEY", value);
527 } else {
528 env::remove_var("GOOGLE_API_KEY");
529 }
530 }
531 }
532
533 #[test]
534 fn test_get_api_key_with_fallback_prefers_env() {
535 unsafe {
537 env::set_var("TEST_FALLBACK_KEY", "env-key");
538 }
539
540 let sources = ApiKeySources {
541 openai_env: "TEST_FALLBACK_KEY".to_string(),
542 openai_config: Some("config-key".to_string()),
543 ..Default::default()
544 };
545
546 let result = get_openai_api_key(&sources);
547 assert!(result.is_ok());
548 assert_eq!(result.unwrap(), "env-key"); unsafe {
552 env::remove_var("TEST_FALLBACK_KEY");
553 }
554 }
555
556 #[test]
557 fn test_get_api_key_fallback_to_config() {
558 let sources = ApiKeySources {
559 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
560 openai_config: Some("config-key".to_string()),
561 ..Default::default()
562 };
563
564 let result = get_openai_api_key(&sources);
565 assert!(result.is_ok());
566 assert_eq!(result.unwrap(), "config-key");
567 }
568
569 #[test]
570 fn test_get_api_key_error_when_not_found() {
571 let sources = ApiKeySources {
572 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
573 ..Default::default()
574 };
575
576 let result = get_openai_api_key(&sources);
577 assert!(result.is_err());
578 }
579
580 #[test]
581 fn test_get_ollama_api_key_missing_sources() {
582 let sources = ApiKeySources {
583 ollama_env: "NONEXISTENT_OLLAMA_ENV".to_string(),
584 ..Default::default()
585 };
586
587 let result = get_ollama_api_key(&sources).expect("Ollama key retrieval should succeed");
588 assert!(result.is_empty());
589 }
590
591 #[test]
592 fn test_get_ollama_api_key_from_env() {
593 unsafe {
595 env::set_var("TEST_OLLAMA_KEY", "test-ollama-key");
596 }
597
598 let sources = ApiKeySources {
599 ollama_env: "TEST_OLLAMA_KEY".to_string(),
600 ..Default::default()
601 };
602
603 let result = get_ollama_api_key(&sources);
604 assert!(result.is_ok());
605 assert_eq!(result.unwrap(), "test-ollama-key");
606
607 unsafe {
609 env::remove_var("TEST_OLLAMA_KEY");
610 }
611 }
612
613 #[test]
614 fn test_get_lmstudio_api_key_missing_sources() {
615 let sources = ApiKeySources {
616 lmstudio_env: "NONEXISTENT_LMSTUDIO_ENV".to_string(),
617 ..Default::default()
618 };
619
620 let result =
621 get_lmstudio_api_key(&sources).expect("LM Studio key retrieval should succeed");
622 assert!(result.is_empty());
623 }
624
625 #[test]
626 fn test_get_lmstudio_api_key_from_env() {
627 unsafe {
628 env::set_var("TEST_LMSTUDIO_KEY", "test-lmstudio-key");
629 }
630
631 let sources = ApiKeySources {
632 lmstudio_env: "TEST_LMSTUDIO_KEY".to_string(),
633 ..Default::default()
634 };
635
636 let result = get_lmstudio_api_key(&sources);
637 assert!(result.is_ok());
638 assert_eq!(result.unwrap(), "test-lmstudio-key");
639
640 unsafe {
641 env::remove_var("TEST_LMSTUDIO_KEY");
642 }
643 }
644
645 #[test]
646 fn test_get_api_key_ollama_provider() {
647 unsafe {
649 env::set_var("OLLAMA_API_KEY", "test-ollama-env-key");
650 }
651
652 let sources = ApiKeySources::default();
653 let result = get_api_key("ollama", &sources);
654 assert!(result.is_ok());
655 assert_eq!(result.unwrap(), "test-ollama-env-key");
656
657 unsafe {
659 env::remove_var("OLLAMA_API_KEY");
660 }
661 }
662
663 #[test]
664 fn test_get_api_key_lmstudio_provider() {
665 unsafe {
666 env::set_var("LMSTUDIO_API_KEY", "test-lmstudio-env-key");
667 }
668
669 let sources = ApiKeySources::default();
670 let result = get_api_key("lmstudio", &sources);
671 assert!(result.is_ok());
672 assert_eq!(result.unwrap(), "test-lmstudio-env-key");
673
674 unsafe {
675 env::remove_var("LMSTUDIO_API_KEY");
676 }
677 }
678}