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 deepseek_env: String,
28 pub zai_env: String,
30 pub ollama_env: String,
32 pub lmstudio_env: String,
34 pub gemini_config: Option<String>,
36 pub anthropic_config: Option<String>,
38 pub openai_config: Option<String>,
40 pub openrouter_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 deepseek_env: "DEEPSEEK_API_KEY".to_string(),
60 zai_env: "ZAI_API_KEY".to_string(),
61 ollama_env: "OLLAMA_API_KEY".to_string(),
62 lmstudio_env: "LMSTUDIO_API_KEY".to_string(),
63 gemini_config: None,
64 anthropic_config: None,
65 openai_config: None,
66 openrouter_config: None,
67 deepseek_config: None,
68 zai_config: None,
69 ollama_config: None,
70 lmstudio_config: None,
71 }
72 }
73}
74
75impl ApiKeySources {
76 pub fn for_provider(_provider: &str) -> Self {
78 Self::default()
79 }
80}
81
82fn inferred_api_key_env(provider: &str) -> &'static str {
83 Provider::from_str(provider)
84 .map(|resolved| resolved.default_api_key_env())
85 .unwrap_or("GEMINI_API_KEY")
86}
87
88pub fn load_dotenv() -> Result<()> {
94 match dotenvy::dotenv() {
95 Ok(path) => {
96 if std::env::var("VTCODE_VERBOSE").is_ok() || std::env::var("RUST_LOG").is_ok() {
98 tracing::info!("Loaded environment variables from: {}", path.display());
99 }
100 Ok(())
101 }
102 Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
103 Ok(())
105 }
106 Err(e) => {
107 tracing::warn!("Failed to load .env file: {}", e);
108 Ok(())
109 }
110 }
111}
112
113pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
132 let normalized_provider = provider.to_lowercase();
133 let inferred_env = inferred_api_key_env(&normalized_provider);
135
136 if let Ok(key) = env::var(inferred_env)
138 && !key.is_empty()
139 {
140 return Ok(key);
141 }
142
143 if let Ok(Some(key)) = get_custom_api_key_from_keyring(&normalized_provider) {
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 "zai" => get_zai_api_key(sources),
156 "ollama" => get_ollama_api_key(sources),
157 "lmstudio" => get_lmstudio_api_key(sources),
158 "huggingface" => env::var("HF_TOKEN").map_err(|_| anyhow::anyhow!("HF_TOKEN not set")),
159 _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
160 }
161}
162
163fn get_custom_api_key_from_keyring(provider: &str) -> Result<Option<String>> {
176 let storage = CustomApiKeyStorage::new(provider);
177 let mode = crate::auth::AuthCredentialsStoreMode::default();
179 storage.load(mode)
180}
181
182fn get_api_key_with_fallback(
184 env_var: &str,
185 config_value: Option<&String>,
186 provider_name: &str,
187) -> Result<String> {
188 if let Ok(key) = env::var(env_var)
190 && !key.is_empty()
191 {
192 return Ok(key);
193 }
194
195 if let Some(key) = config_value
197 && !key.is_empty()
198 {
199 return Ok(key.clone());
200 }
201
202 Err(anyhow::anyhow!(
204 "No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
205 provider_name,
206 env_var
207 ))
208}
209
210fn get_optional_api_key_with_fallback(env_var: &str, config_value: Option<&String>) -> String {
211 if let Ok(key) = env::var(env_var)
212 && !key.is_empty()
213 {
214 return key;
215 }
216
217 if let Some(key) = config_value
218 && !key.is_empty()
219 {
220 return key.clone();
221 }
222
223 String::new()
224}
225
226fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
228 if let Ok(key) = env::var(&sources.gemini_env)
230 && !key.is_empty()
231 {
232 return Ok(key);
233 }
234
235 if let Ok(key) = env::var("GOOGLE_API_KEY")
237 && !key.is_empty()
238 {
239 return Ok(key);
240 }
241
242 if let Some(key) = &sources.gemini_config
244 && !key.is_empty()
245 {
246 return Ok(key.clone());
247 }
248
249 Err(anyhow::anyhow!(
251 "No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
252 sources.gemini_env
253 ))
254}
255
256fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
258 get_api_key_with_fallback(
259 &sources.anthropic_env,
260 sources.anthropic_config.as_ref(),
261 "Anthropic",
262 )
263}
264
265fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
267 get_api_key_with_fallback(
268 &sources.openai_env,
269 sources.openai_config.as_ref(),
270 "OpenAI",
271 )
272}
273
274fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
281 if let Ok(Some(token)) = crate::auth::load_oauth_token() {
283 tracing::debug!("Using OAuth token for OpenRouter authentication");
284 return Ok(token.api_key);
285 }
286
287 get_api_key_with_fallback(
289 &sources.openrouter_env,
290 sources.openrouter_config.as_ref(),
291 "OpenRouter",
292 )
293}
294
295fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
297 get_api_key_with_fallback(
298 &sources.deepseek_env,
299 sources.deepseek_config.as_ref(),
300 "DeepSeek",
301 )
302}
303
304fn get_zai_api_key(sources: &ApiKeySources) -> Result<String> {
306 get_api_key_with_fallback(&sources.zai_env, sources.zai_config.as_ref(), "Z.AI")
307}
308
309fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
311 Ok(get_optional_api_key_with_fallback(
314 &sources.ollama_env,
315 sources.ollama_config.as_ref(),
316 ))
317}
318
319fn get_lmstudio_api_key(sources: &ApiKeySources) -> Result<String> {
321 Ok(get_optional_api_key_with_fallback(
322 &sources.lmstudio_env,
323 sources.lmstudio_config.as_ref(),
324 ))
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use std::env;
331
332 #[test]
333 fn test_get_gemini_api_key_from_env() {
334 unsafe {
336 env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
337 }
338
339 let sources = ApiKeySources {
340 gemini_env: "TEST_GEMINI_KEY".to_string(),
341 ..Default::default()
342 };
343
344 let result = get_gemini_api_key(&sources);
345 assert!(result.is_ok());
346 assert_eq!(result.unwrap(), "test-gemini-key");
347
348 unsafe {
350 env::remove_var("TEST_GEMINI_KEY");
351 }
352 }
353
354 #[test]
355 fn test_get_anthropic_api_key_from_env() {
356 unsafe {
358 env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
359 }
360
361 let sources = ApiKeySources {
362 anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
363 ..Default::default()
364 };
365
366 let result = get_anthropic_api_key(&sources);
367 assert!(result.is_ok());
368 assert_eq!(result.unwrap(), "test-anthropic-key");
369
370 unsafe {
372 env::remove_var("TEST_ANTHROPIC_KEY");
373 }
374 }
375
376 #[test]
377 fn test_get_openai_api_key_from_env() {
378 unsafe {
380 env::set_var("TEST_OPENAI_KEY", "test-openai-key");
381 }
382
383 let sources = ApiKeySources {
384 openai_env: "TEST_OPENAI_KEY".to_string(),
385 ..Default::default()
386 };
387
388 let result = get_openai_api_key(&sources);
389 assert!(result.is_ok());
390 assert_eq!(result.unwrap(), "test-openai-key");
391
392 unsafe {
394 env::remove_var("TEST_OPENAI_KEY");
395 }
396 }
397
398 #[test]
399 fn test_get_deepseek_api_key_from_env() {
400 unsafe {
401 env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
402 }
403
404 let sources = ApiKeySources {
405 deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
406 ..Default::default()
407 };
408
409 let result = get_deepseek_api_key(&sources);
410 assert!(result.is_ok());
411 assert_eq!(result.unwrap(), "test-deepseek-key");
412
413 unsafe {
414 env::remove_var("TEST_DEEPSEEK_KEY");
415 }
416 }
417
418 #[test]
419 fn test_get_gemini_api_key_from_config() {
420 let prior_gemini_key = env::var("TEST_GEMINI_CONFIG_KEY").ok();
421 let prior_google_key = env::var("GOOGLE_API_KEY").ok();
422
423 unsafe {
424 env::remove_var("TEST_GEMINI_CONFIG_KEY");
425 env::remove_var("GOOGLE_API_KEY");
426 }
427
428 let sources = ApiKeySources {
429 gemini_env: "TEST_GEMINI_CONFIG_KEY".to_string(),
430 gemini_config: Some("config-gemini-key".to_string()),
431 ..Default::default()
432 };
433
434 let result = get_gemini_api_key(&sources);
435 assert!(result.is_ok());
436 assert_eq!(result.unwrap(), "config-gemini-key");
437
438 unsafe {
439 if let Some(value) = prior_gemini_key {
440 env::set_var("TEST_GEMINI_CONFIG_KEY", value);
441 } else {
442 env::remove_var("TEST_GEMINI_CONFIG_KEY");
443 }
444 if let Some(value) = prior_google_key {
445 env::set_var("GOOGLE_API_KEY", value);
446 } else {
447 env::remove_var("GOOGLE_API_KEY");
448 }
449 }
450 }
451
452 #[test]
453 fn test_get_api_key_with_fallback_prefers_env() {
454 unsafe {
456 env::set_var("TEST_FALLBACK_KEY", "env-key");
457 }
458
459 let sources = ApiKeySources {
460 openai_env: "TEST_FALLBACK_KEY".to_string(),
461 openai_config: Some("config-key".to_string()),
462 ..Default::default()
463 };
464
465 let result = get_openai_api_key(&sources);
466 assert!(result.is_ok());
467 assert_eq!(result.unwrap(), "env-key"); unsafe {
471 env::remove_var("TEST_FALLBACK_KEY");
472 }
473 }
474
475 #[test]
476 fn test_get_api_key_fallback_to_config() {
477 let sources = ApiKeySources {
478 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
479 openai_config: Some("config-key".to_string()),
480 ..Default::default()
481 };
482
483 let result = get_openai_api_key(&sources);
484 assert!(result.is_ok());
485 assert_eq!(result.unwrap(), "config-key");
486 }
487
488 #[test]
489 fn test_get_api_key_error_when_not_found() {
490 let sources = ApiKeySources {
491 openai_env: "NONEXISTENT_ENV_VAR".to_string(),
492 ..Default::default()
493 };
494
495 let result = get_openai_api_key(&sources);
496 assert!(result.is_err());
497 }
498
499 #[test]
500 fn test_get_ollama_api_key_missing_sources() {
501 let sources = ApiKeySources {
502 ollama_env: "NONEXISTENT_OLLAMA_ENV".to_string(),
503 ..Default::default()
504 };
505
506 let result = get_ollama_api_key(&sources).expect("Ollama key retrieval should succeed");
507 assert!(result.is_empty());
508 }
509
510 #[test]
511 fn test_get_ollama_api_key_from_env() {
512 unsafe {
514 env::set_var("TEST_OLLAMA_KEY", "test-ollama-key");
515 }
516
517 let sources = ApiKeySources {
518 ollama_env: "TEST_OLLAMA_KEY".to_string(),
519 ..Default::default()
520 };
521
522 let result = get_ollama_api_key(&sources);
523 assert!(result.is_ok());
524 assert_eq!(result.unwrap(), "test-ollama-key");
525
526 unsafe {
528 env::remove_var("TEST_OLLAMA_KEY");
529 }
530 }
531
532 #[test]
533 fn test_get_lmstudio_api_key_missing_sources() {
534 let sources = ApiKeySources {
535 lmstudio_env: "NONEXISTENT_LMSTUDIO_ENV".to_string(),
536 ..Default::default()
537 };
538
539 let result =
540 get_lmstudio_api_key(&sources).expect("LM Studio key retrieval should succeed");
541 assert!(result.is_empty());
542 }
543
544 #[test]
545 fn test_get_lmstudio_api_key_from_env() {
546 unsafe {
547 env::set_var("TEST_LMSTUDIO_KEY", "test-lmstudio-key");
548 }
549
550 let sources = ApiKeySources {
551 lmstudio_env: "TEST_LMSTUDIO_KEY".to_string(),
552 ..Default::default()
553 };
554
555 let result = get_lmstudio_api_key(&sources);
556 assert!(result.is_ok());
557 assert_eq!(result.unwrap(), "test-lmstudio-key");
558
559 unsafe {
560 env::remove_var("TEST_LMSTUDIO_KEY");
561 }
562 }
563
564 #[test]
565 fn test_get_api_key_ollama_provider() {
566 unsafe {
568 env::set_var("OLLAMA_API_KEY", "test-ollama-env-key");
569 }
570
571 let sources = ApiKeySources::default();
572 let result = get_api_key("ollama", &sources);
573 assert!(result.is_ok());
574 assert_eq!(result.unwrap(), "test-ollama-env-key");
575
576 unsafe {
578 env::remove_var("OLLAMA_API_KEY");
579 }
580 }
581
582 #[test]
583 fn test_get_api_key_lmstudio_provider() {
584 unsafe {
585 env::set_var("LMSTUDIO_API_KEY", "test-lmstudio-env-key");
586 }
587
588 let sources = ApiKeySources::default();
589 let result = get_api_key("lmstudio", &sources);
590 assert!(result.is_ok());
591 assert_eq!(result.unwrap(), "test-lmstudio-env-key");
592
593 unsafe {
594 env::remove_var("LMSTUDIO_API_KEY");
595 }
596 }
597}