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