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 _ => "GEMINI_API_KEY",
209 };
210
211 if let Ok(key) = env::var(inferred_env)
213 && !key.is_empty()
214 {
215 return Ok(key);
216 }
217
218 match provider.to_lowercase().as_str() {
220 "gemini" => get_gemini_api_key(sources),
221 "anthropic" => get_anthropic_api_key(sources),
222 "openai" => get_openai_api_key(sources),
223 "deepseek" => get_deepseek_api_key(sources),
224 "openrouter" => get_openrouter_api_key(sources),
225 "xai" => get_xai_api_key(sources),
226 "zai" => get_zai_api_key(sources),
227 "ollama" => get_ollama_api_key(sources),
228 "lmstudio" => get_lmstudio_api_key(sources),
229 _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
230 }
231}
232
233fn get_api_key_with_fallback(
235 env_var: &str,
236 config_value: Option<&String>,
237 provider_name: &str,
238) -> Result<String> {
239 if let Ok(key) = env::var(env_var)
241 && !key.is_empty()
242 {
243 return Ok(key);
244 }
245
246 if let Some(key) = config_value
248 && !key.is_empty()
249 {
250 return Ok(key.clone());
251 }
252
253 Err(anyhow::anyhow!(
255 "No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
256 provider_name,
257 env_var
258 ))
259}
260
261fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
263 if let Ok(key) = env::var(&sources.gemini_env)
265 && !key.is_empty()
266 {
267 return Ok(key);
268 }
269
270 if let Ok(key) = env::var("GOOGLE_API_KEY")
272 && !key.is_empty()
273 {
274 return Ok(key);
275 }
276
277 if let Some(key) = &sources.gemini_config
279 && !key.is_empty()
280 {
281 return Ok(key.clone());
282 }
283
284 Err(anyhow::anyhow!(
286 "No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
287 sources.gemini_env
288 ))
289}
290
291fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
293 get_api_key_with_fallback(
294 &sources.anthropic_env,
295 sources.anthropic_config.as_ref(),
296 "Anthropic",
297 )
298}
299
300fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
302 get_api_key_with_fallback(
303 &sources.openai_env,
304 sources.openai_config.as_ref(),
305 "OpenAI",
306 )
307}
308
309fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
311 get_api_key_with_fallback(
312 &sources.openrouter_env,
313 sources.openrouter_config.as_ref(),
314 "OpenRouter",
315 )
316}
317
318fn get_xai_api_key(sources: &ApiKeySources) -> Result<String> {
320 get_api_key_with_fallback(&sources.xai_env, sources.xai_config.as_ref(), "xAI")
321}
322
323fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
325 get_api_key_with_fallback(
326 &sources.deepseek_env,
327 sources.deepseek_config.as_ref(),
328 "DeepSeek",
329 )
330}
331
332fn get_zai_api_key(sources: &ApiKeySources) -> Result<String> {
334 get_api_key_with_fallback(&sources.zai_env, sources.zai_config.as_ref(), "Z.AI")
335}
336
337fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
339 if let Ok(key) = env::var(&sources.ollama_env)
343 && !key.is_empty()
344 {
345 return Ok(key);
346 }
347
348 if let Some(key) = sources.ollama_config.as_ref()
349 && !key.is_empty()
350 {
351 return Ok(key.clone());
352 }
353
354 Ok(String::new())
355}
356
357fn get_lmstudio_api_key(sources: &ApiKeySources) -> Result<String> {
359 if let Ok(key) = env::var(&sources.lmstudio_env)
360 && !key.is_empty()
361 {
362 return Ok(key);
363 }
364
365 if let Some(key) = sources.lmstudio_config.as_ref()
366 && !key.is_empty()
367 {
368 return Ok(key.clone());
369 }
370
371 Ok(String::new())
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use std::env;
378
379 #[test]
380 fn test_get_gemini_api_key_from_env() {
381 unsafe {
383 env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
384 }
385
386 let sources = ApiKeySources {
387 gemini_env: "TEST_GEMINI_KEY".to_string(),
388 ..Default::default()
389 };
390
391 let result = get_gemini_api_key(&sources);
392 assert!(result.is_ok());
393 assert_eq!(result.unwrap(), "test-gemini-key");
394
395 unsafe {
397 env::remove_var("TEST_GEMINI_KEY");
398 }
399 }
400
401 #[test]
402 fn test_get_anthropic_api_key_from_env() {
403 unsafe {
405 env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
406 }
407
408 let sources = ApiKeySources {
409 anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
410 ..Default::default()
411 };
412
413 let result = get_anthropic_api_key(&sources);
414 assert!(result.is_ok());
415 assert_eq!(result.unwrap(), "test-anthropic-key");
416
417 unsafe {
419 env::remove_var("TEST_ANTHROPIC_KEY");
420 }
421 }
422
423 #[test]
424 fn test_get_openai_api_key_from_env() {
425 unsafe {
427 env::set_var("TEST_OPENAI_KEY", "test-openai-key");
428 }
429
430 let sources = ApiKeySources {
431 openai_env: "TEST_OPENAI_KEY".to_string(),
432 ..Default::default()
433 };
434
435 let result = get_openai_api_key(&sources);
436 assert!(result.is_ok());
437 assert_eq!(result.unwrap(), "test-openai-key");
438
439 unsafe {
441 env::remove_var("TEST_OPENAI_KEY");
442 }
443 }
444
445 #[test]
446 fn test_get_deepseek_api_key_from_env() {
447 unsafe {
448 env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
449 }
450
451 let sources = ApiKeySources {
452 deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
453 ..Default::default()
454 };
455
456 let result = get_deepseek_api_key(&sources);
457 assert!(result.is_ok());
458 assert_eq!(result.unwrap(), "test-deepseek-key");
459
460 unsafe {
461 env::remove_var("TEST_DEEPSEEK_KEY");
462 }
463 }
464
465 #[test]
466 fn test_get_xai_api_key_from_env() {
467 unsafe {
468 env::set_var("TEST_XAI_KEY", "test-xai-key");
469 }
470
471 let sources = ApiKeySources {
472 xai_env: "TEST_XAI_KEY".to_string(),
473 ..Default::default()
474 };
475
476 let result = get_xai_api_key(&sources);
477 assert!(result.is_ok());
478 assert_eq!(result.unwrap(), "test-xai-key");
479
480 unsafe {
481 env::remove_var("TEST_XAI_KEY");
482 }
483 }
484
485 #[test]
486 fn test_get_gemini_api_key_from_config() {
487 let sources = ApiKeySources {
488 gemini_config: Some("config-gemini-key".to_string()),
489 ..Default::default()
490 };
491
492 let result = get_gemini_api_key(&sources);
493 assert!(result.is_ok());
494 assert_eq!(result.unwrap(), "config-gemini-key");
495 }
496
497 #[test]
498 fn test_get_api_key_with_fallback_prefers_env() {
499 unsafe {
501 env::set_var("TEST_FALLBACK_KEY", "env-key");
502 }
503
504 let sources = ApiKeySources {
505 openai_env: "TEST_FALLBACK_KEY".to_string(),
506 openai_config: Some("config-key".to_string()),
507 ..Default::default()
508 };
509
510 let result = get_openai_api_key(&sources);
511 assert!(result.is_ok());
512 assert_eq!(result.unwrap(), "env-key"); unsafe {
516 env::remove_var("TEST_FALLBACK_KEY");
517 }
518 }
519
520 #[test]
521 fn test_get_api_key_fallback_to_config() {
522 let sources = ApiKeySources {
523 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
524 openai_config: Some("config-key".to_string()),
525 ..Default::default()
526 };
527
528 let result = get_openai_api_key(&sources);
529 assert!(result.is_ok());
530 assert_eq!(result.unwrap(), "config-key");
531 }
532
533 #[test]
534 fn test_get_api_key_error_when_not_found() {
535 let sources = ApiKeySources {
536 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
537 ..Default::default()
538 };
539
540 let result = get_openai_api_key(&sources);
541 assert!(result.is_err());
542 }
543
544 #[test]
545 fn test_get_ollama_api_key_missing_sources() {
546 let sources = ApiKeySources {
547 ollama_env: "NONEXISTENT_OLLAMA_ENV".to_string(),
548 ..Default::default()
549 };
550
551 let result = get_ollama_api_key(&sources).expect("Ollama key retrieval should succeed");
552 assert!(result.is_empty());
553 }
554
555 #[test]
556 fn test_get_ollama_api_key_from_env() {
557 unsafe {
559 env::set_var("TEST_OLLAMA_KEY", "test-ollama-key");
560 }
561
562 let sources = ApiKeySources {
563 ollama_env: "TEST_OLLAMA_KEY".to_string(),
564 ..Default::default()
565 };
566
567 let result = get_ollama_api_key(&sources);
568 assert!(result.is_ok());
569 assert_eq!(result.unwrap(), "test-ollama-key");
570
571 unsafe {
573 env::remove_var("TEST_OLLAMA_KEY");
574 }
575 }
576
577 #[test]
578 fn test_get_lmstudio_api_key_missing_sources() {
579 let sources = ApiKeySources {
580 lmstudio_env: "NONEXISTENT_LMSTUDIO_ENV".to_string(),
581 ..Default::default()
582 };
583
584 let result =
585 get_lmstudio_api_key(&sources).expect("LM Studio key retrieval should succeed");
586 assert!(result.is_empty());
587 }
588
589 #[test]
590 fn test_get_lmstudio_api_key_from_env() {
591 unsafe {
592 env::set_var("TEST_LMSTUDIO_KEY", "test-lmstudio-key");
593 }
594
595 let sources = ApiKeySources {
596 lmstudio_env: "TEST_LMSTUDIO_KEY".to_string(),
597 ..Default::default()
598 };
599
600 let result = get_lmstudio_api_key(&sources);
601 assert!(result.is_ok());
602 assert_eq!(result.unwrap(), "test-lmstudio-key");
603
604 unsafe {
605 env::remove_var("TEST_LMSTUDIO_KEY");
606 }
607 }
608
609 #[test]
610 fn test_get_api_key_ollama_provider() {
611 unsafe {
613 env::set_var("OLLAMA_API_KEY", "test-ollama-env-key");
614 }
615
616 let sources = ApiKeySources::default();
617 let result = get_api_key("ollama", &sources);
618 assert!(result.is_ok());
619 assert_eq!(result.unwrap(), "test-ollama-env-key");
620
621 unsafe {
623 env::remove_var("OLLAMA_API_KEY");
624 }
625 }
626
627 #[test]
628 fn test_get_api_key_lmstudio_provider() {
629 unsafe {
630 env::set_var("LMSTUDIO_API_KEY", "test-lmstudio-env-key");
631 }
632
633 let sources = ApiKeySources::default();
634 let result = get_api_key("lmstudio", &sources);
635 assert!(result.is_ok());
636 assert_eq!(result.unwrap(), "test-lmstudio-env-key");
637
638 unsafe {
639 env::remove_var("LMSTUDIO_API_KEY");
640 }
641 }
642}