Skip to main content

vtcode_config/
api_keys.rs

1//! API key management module for secure retrieval from environment variables,
2//! .env files, and configuration files.
3//!
4//! This module provides a unified interface for retrieving API keys for different providers,
5//! prioritizing security by checking environment variables first, then .env files, and finally
6//! falling back to configuration file values.
7
8use anyhow::Result;
9use std::env;
10use std::str::FromStr;
11
12use crate::models::Provider;
13
14/// API key sources for different providers
15#[derive(Debug, Clone)]
16pub struct ApiKeySources {
17    /// Gemini API key environment variable name
18    pub gemini_env: String,
19    /// Anthropic API key environment variable name
20    pub anthropic_env: String,
21    /// OpenAI API key environment variable name
22    pub openai_env: String,
23    /// OpenRouter API key environment variable name
24    pub openrouter_env: String,
25    /// xAI API key environment variable name
26    pub xai_env: String,
27    /// DeepSeek API key environment variable name
28    pub deepseek_env: String,
29    /// Z.AI API key environment variable name
30    pub zai_env: String,
31    /// Ollama API key environment variable name
32    pub ollama_env: String,
33    /// LM Studio API key environment variable name
34    pub lmstudio_env: String,
35    /// Gemini API key from configuration file
36    pub gemini_config: Option<String>,
37    /// Anthropic API key from configuration file
38    pub anthropic_config: Option<String>,
39    /// OpenAI API key from configuration file
40    pub openai_config: Option<String>,
41    /// OpenRouter API key from configuration file
42    pub openrouter_config: Option<String>,
43    /// xAI API key from configuration file
44    pub xai_config: Option<String>,
45    /// DeepSeek API key from configuration file
46    pub deepseek_config: Option<String>,
47    /// Z.AI API key from configuration file
48    pub zai_config: Option<String>,
49    /// Ollama API key from configuration file
50    pub ollama_config: Option<String>,
51    /// LM Studio API key from configuration file
52    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    /// Create API key sources for a specific provider with automatic environment variable inference
82    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
93/// Load environment variables from .env file
94///
95/// This function attempts to load environment variables from a .env file
96/// in the current directory. It logs a warning if the file exists but cannot
97/// be loaded, but doesn't fail if the file doesn't exist.
98pub fn load_dotenv() -> Result<()> {
99    match dotenvy::dotenv() {
100        Ok(path) => {
101            // Only print in verbose mode to avoid polluting stdout/stderr in scripts
102            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            // .env file doesn't exist, which is fine
109            Ok(())
110        }
111        Err(e) => {
112            tracing::warn!("Failed to load .env file: {}", e);
113            Ok(())
114        }
115    }
116}
117
118/// Get API key for a specific provider with secure fallback mechanism
119///
120/// This function implements a secure retrieval mechanism that:
121/// 1. First checks environment variables (highest priority for security)
122/// 2. Then checks .env file values
123/// 3. Falls back to configuration file values if neither above is set
124/// 4. Supports all major providers: Gemini, Anthropic, OpenAI, OpenRouter, and xAI
125/// 5. Automatically infers the correct environment variable based on provider
126///
127/// # Arguments
128///
129/// * `provider` - The provider name ("gemini", "anthropic", or "openai")
130/// * `sources` - Configuration for where to look for API keys
131///
132/// # Returns
133///
134/// * `Ok(String)` - The API key if found
135/// * `Err` - If no API key could be found for the provider
136pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
137    let normalized_provider = provider.to_lowercase();
138    // Automatically infer the correct environment variable based on provider
139    let inferred_env = inferred_api_key_env(&normalized_provider);
140
141    // Try the inferred environment variable first
142    if let Ok(key) = env::var(inferred_env)
143        && !key.is_empty()
144    {
145        return Ok(key);
146    }
147
148    // Fall back to the provider-specific sources
149    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
164/// Get API key for a specific environment variable with fallback
165fn get_api_key_with_fallback(
166    env_var: &str,
167    config_value: Option<&String>,
168    provider_name: &str,
169) -> Result<String> {
170    // First try environment variable (most secure)
171    if let Ok(key) = env::var(env_var)
172        && !key.is_empty()
173    {
174        return Ok(key);
175    }
176
177    // Then try configuration file value
178    if let Some(key) = config_value
179        && !key.is_empty()
180    {
181        return Ok(key.clone());
182    }
183
184    // If neither worked, return an error
185    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
208/// Get Gemini API key with secure fallback
209fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
210    // Try primary Gemini environment variable
211    if let Ok(key) = env::var(&sources.gemini_env)
212        && !key.is_empty()
213    {
214        return Ok(key);
215    }
216
217    // Try Google API key as fallback (for backward compatibility)
218    if let Ok(key) = env::var("GOOGLE_API_KEY")
219        && !key.is_empty()
220    {
221        return Ok(key);
222    }
223
224    // Try configuration file value
225    if let Some(key) = &sources.gemini_config
226        && !key.is_empty()
227    {
228        return Ok(key.clone());
229    }
230
231    // If nothing worked, return an error
232    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
238/// Get Anthropic API key with secure fallback
239fn 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
247/// Get OpenAI API key with secure fallback
248fn 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
256/// Get OpenRouter API key with secure fallback
257///
258/// This function checks for credentials in the following order:
259/// 1. OAuth token from encrypted storage (if OAuth is enabled)
260/// 2. Environment variable (OPENROUTER_API_KEY)
261/// 3. Configuration file value
262fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
263    // First, try to load OAuth token from encrypted storage
264    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    // Fall back to standard API key retrieval
270    get_api_key_with_fallback(
271        &sources.openrouter_env,
272        sources.openrouter_config.as_ref(),
273        "OpenRouter",
274    )
275}
276
277/// Get xAI API key with secure fallback
278fn 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
282/// Get DeepSeek API key with secure fallback
283fn 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
291/// Get Z.AI API key with secure fallback
292fn 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
296/// Get Ollama API key with secure fallback
297fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
298    // For Ollama we allow running without credentials when connecting to a local deployment.
299    // Cloud variants still rely on environment/config values when present.
300    Ok(get_optional_api_key_with_fallback(
301        &sources.ollama_env,
302        sources.ollama_config.as_ref(),
303    ))
304}
305
306/// Get LM Studio API key with secure fallback
307fn 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        // Set environment variable
322        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        // Clean up
336        unsafe {
337            env::remove_var("TEST_GEMINI_KEY");
338        }
339    }
340
341    #[test]
342    fn test_get_anthropic_api_key_from_env() {
343        // Set environment variable
344        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        // Clean up
358        unsafe {
359            env::remove_var("TEST_ANTHROPIC_KEY");
360        }
361    }
362
363    #[test]
364    fn test_get_openai_api_key_from_env() {
365        // Set environment variable
366        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        // Clean up
380        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        // Set environment variable
462        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"); // Should prefer env var
475
476        // Clean up
477        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        // Set environment variable
520        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        // Clean up
534        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        // Set environment variable
574        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        // Clean up
584        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}