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> {
313 get_api_key_with_fallback(
314 &sources.openrouter_env,
315 sources.openrouter_config.as_ref(),
316 "OpenRouter",
317 )
318}
319
320fn get_xai_api_key(sources: &ApiKeySources) -> Result<String> {
322 get_api_key_with_fallback(&sources.xai_env, sources.xai_config.as_ref(), "xAI")
323}
324
325fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
327 get_api_key_with_fallback(
328 &sources.deepseek_env,
329 sources.deepseek_config.as_ref(),
330 "DeepSeek",
331 )
332}
333
334fn get_zai_api_key(sources: &ApiKeySources) -> Result<String> {
336 get_api_key_with_fallback(&sources.zai_env, sources.zai_config.as_ref(), "Z.AI")
337}
338
339fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
341 if let Ok(key) = env::var(&sources.ollama_env)
345 && !key.is_empty()
346 {
347 return Ok(key);
348 }
349
350 if let Some(key) = sources.ollama_config.as_ref()
351 && !key.is_empty()
352 {
353 return Ok(key.clone());
354 }
355
356 Ok(String::new())
357}
358
359fn get_lmstudio_api_key(sources: &ApiKeySources) -> Result<String> {
361 if let Ok(key) = env::var(&sources.lmstudio_env)
362 && !key.is_empty()
363 {
364 return Ok(key);
365 }
366
367 if let Some(key) = sources.lmstudio_config.as_ref()
368 && !key.is_empty()
369 {
370 return Ok(key.clone());
371 }
372
373 Ok(String::new())
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use std::env;
380
381 #[test]
382 fn test_get_gemini_api_key_from_env() {
383 unsafe {
385 env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
386 }
387
388 let sources = ApiKeySources {
389 gemini_env: "TEST_GEMINI_KEY".to_string(),
390 ..Default::default()
391 };
392
393 let result = get_gemini_api_key(&sources);
394 assert!(result.is_ok());
395 assert_eq!(result.unwrap(), "test-gemini-key");
396
397 unsafe {
399 env::remove_var("TEST_GEMINI_KEY");
400 }
401 }
402
403 #[test]
404 fn test_get_anthropic_api_key_from_env() {
405 unsafe {
407 env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
408 }
409
410 let sources = ApiKeySources {
411 anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
412 ..Default::default()
413 };
414
415 let result = get_anthropic_api_key(&sources);
416 assert!(result.is_ok());
417 assert_eq!(result.unwrap(), "test-anthropic-key");
418
419 unsafe {
421 env::remove_var("TEST_ANTHROPIC_KEY");
422 }
423 }
424
425 #[test]
426 fn test_get_openai_api_key_from_env() {
427 unsafe {
429 env::set_var("TEST_OPENAI_KEY", "test-openai-key");
430 }
431
432 let sources = ApiKeySources {
433 openai_env: "TEST_OPENAI_KEY".to_string(),
434 ..Default::default()
435 };
436
437 let result = get_openai_api_key(&sources);
438 assert!(result.is_ok());
439 assert_eq!(result.unwrap(), "test-openai-key");
440
441 unsafe {
443 env::remove_var("TEST_OPENAI_KEY");
444 }
445 }
446
447 #[test]
448 fn test_get_deepseek_api_key_from_env() {
449 unsafe {
450 env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
451 }
452
453 let sources = ApiKeySources {
454 deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
455 ..Default::default()
456 };
457
458 let result = get_deepseek_api_key(&sources);
459 assert!(result.is_ok());
460 assert_eq!(result.unwrap(), "test-deepseek-key");
461
462 unsafe {
463 env::remove_var("TEST_DEEPSEEK_KEY");
464 }
465 }
466
467 #[test]
468 fn test_get_xai_api_key_from_env() {
469 unsafe {
470 env::set_var("TEST_XAI_KEY", "test-xai-key");
471 }
472
473 let sources = ApiKeySources {
474 xai_env: "TEST_XAI_KEY".to_string(),
475 ..Default::default()
476 };
477
478 let result = get_xai_api_key(&sources);
479 assert!(result.is_ok());
480 assert_eq!(result.unwrap(), "test-xai-key");
481
482 unsafe {
483 env::remove_var("TEST_XAI_KEY");
484 }
485 }
486
487 #[test]
488 fn test_get_gemini_api_key_from_config() {
489 let sources = ApiKeySources {
490 gemini_config: Some("config-gemini-key".to_string()),
491 ..Default::default()
492 };
493
494 let result = get_gemini_api_key(&sources);
495 assert!(result.is_ok());
496 assert_eq!(result.unwrap(), "config-gemini-key");
497 }
498
499 #[test]
500 fn test_get_api_key_with_fallback_prefers_env() {
501 unsafe {
503 env::set_var("TEST_FALLBACK_KEY", "env-key");
504 }
505
506 let sources = ApiKeySources {
507 openai_env: "TEST_FALLBACK_KEY".to_string(),
508 openai_config: Some("config-key".to_string()),
509 ..Default::default()
510 };
511
512 let result = get_openai_api_key(&sources);
513 assert!(result.is_ok());
514 assert_eq!(result.unwrap(), "env-key"); unsafe {
518 env::remove_var("TEST_FALLBACK_KEY");
519 }
520 }
521
522 #[test]
523 fn test_get_api_key_fallback_to_config() {
524 let sources = ApiKeySources {
525 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
526 openai_config: Some("config-key".to_string()),
527 ..Default::default()
528 };
529
530 let result = get_openai_api_key(&sources);
531 assert!(result.is_ok());
532 assert_eq!(result.unwrap(), "config-key");
533 }
534
535 #[test]
536 fn test_get_api_key_error_when_not_found() {
537 let sources = ApiKeySources {
538 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
539 ..Default::default()
540 };
541
542 let result = get_openai_api_key(&sources);
543 assert!(result.is_err());
544 }
545
546 #[test]
547 fn test_get_ollama_api_key_missing_sources() {
548 let sources = ApiKeySources {
549 ollama_env: "NONEXISTENT_OLLAMA_ENV".to_string(),
550 ..Default::default()
551 };
552
553 let result = get_ollama_api_key(&sources).expect("Ollama key retrieval should succeed");
554 assert!(result.is_empty());
555 }
556
557 #[test]
558 fn test_get_ollama_api_key_from_env() {
559 unsafe {
561 env::set_var("TEST_OLLAMA_KEY", "test-ollama-key");
562 }
563
564 let sources = ApiKeySources {
565 ollama_env: "TEST_OLLAMA_KEY".to_string(),
566 ..Default::default()
567 };
568
569 let result = get_ollama_api_key(&sources);
570 assert!(result.is_ok());
571 assert_eq!(result.unwrap(), "test-ollama-key");
572
573 unsafe {
575 env::remove_var("TEST_OLLAMA_KEY");
576 }
577 }
578
579 #[test]
580 fn test_get_lmstudio_api_key_missing_sources() {
581 let sources = ApiKeySources {
582 lmstudio_env: "NONEXISTENT_LMSTUDIO_ENV".to_string(),
583 ..Default::default()
584 };
585
586 let result =
587 get_lmstudio_api_key(&sources).expect("LM Studio key retrieval should succeed");
588 assert!(result.is_empty());
589 }
590
591 #[test]
592 fn test_get_lmstudio_api_key_from_env() {
593 unsafe {
594 env::set_var("TEST_LMSTUDIO_KEY", "test-lmstudio-key");
595 }
596
597 let sources = ApiKeySources {
598 lmstudio_env: "TEST_LMSTUDIO_KEY".to_string(),
599 ..Default::default()
600 };
601
602 let result = get_lmstudio_api_key(&sources);
603 assert!(result.is_ok());
604 assert_eq!(result.unwrap(), "test-lmstudio-key");
605
606 unsafe {
607 env::remove_var("TEST_LMSTUDIO_KEY");
608 }
609 }
610
611 #[test]
612 fn test_get_api_key_ollama_provider() {
613 unsafe {
615 env::set_var("OLLAMA_API_KEY", "test-ollama-env-key");
616 }
617
618 let sources = ApiKeySources::default();
619 let result = get_api_key("ollama", &sources);
620 assert!(result.is_ok());
621 assert_eq!(result.unwrap(), "test-ollama-env-key");
622
623 unsafe {
625 env::remove_var("OLLAMA_API_KEY");
626 }
627 }
628
629 #[test]
630 fn test_get_api_key_lmstudio_provider() {
631 unsafe {
632 env::set_var("LMSTUDIO_API_KEY", "test-lmstudio-env-key");
633 }
634
635 let sources = ApiKeySources::default();
636 let result = get_api_key("lmstudio", &sources);
637 assert!(result.is_ok());
638 assert_eq!(result.unwrap(), "test-lmstudio-env-key");
639
640 unsafe {
641 env::remove_var("LMSTUDIO_API_KEY");
642 }
643 }
644}