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::auth::CustomApiKeyStorage;
13use crate::models::Provider;
14
15/// API key sources for different providers
16#[derive(Debug, Clone)]
17pub struct ApiKeySources {
18    /// Gemini API key environment variable name
19    pub gemini_env: String,
20    /// Anthropic API key environment variable name
21    pub anthropic_env: String,
22    /// OpenAI API key environment variable name
23    pub openai_env: String,
24    /// OpenRouter API key environment variable name
25    pub openrouter_env: String,
26    /// xAI API key environment variable name
27    pub xai_env: String,
28    /// DeepSeek API key environment variable name
29    pub deepseek_env: String,
30    /// Z.AI API key environment variable name
31    pub zai_env: String,
32    /// Ollama API key environment variable name
33    pub ollama_env: String,
34    /// LM Studio API key environment variable name
35    pub lmstudio_env: String,
36    /// Gemini API key from configuration file
37    pub gemini_config: Option<String>,
38    /// Anthropic API key from configuration file
39    pub anthropic_config: Option<String>,
40    /// OpenAI API key from configuration file
41    pub openai_config: Option<String>,
42    /// OpenRouter API key from configuration file
43    pub openrouter_config: Option<String>,
44    /// xAI API key from configuration file
45    pub xai_config: Option<String>,
46    /// DeepSeek API key from configuration file
47    pub deepseek_config: Option<String>,
48    /// Z.AI API key from configuration file
49    pub zai_config: Option<String>,
50    /// Ollama API key from configuration file
51    pub ollama_config: Option<String>,
52    /// LM Studio API key from configuration file
53    pub lmstudio_config: Option<String>,
54}
55
56impl Default for ApiKeySources {
57    fn default() -> Self {
58        Self {
59            gemini_env: "GEMINI_API_KEY".to_string(),
60            anthropic_env: "ANTHROPIC_API_KEY".to_string(),
61            openai_env: "OPENAI_API_KEY".to_string(),
62            openrouter_env: "OPENROUTER_API_KEY".to_string(),
63            xai_env: "XAI_API_KEY".to_string(),
64            deepseek_env: "DEEPSEEK_API_KEY".to_string(),
65            zai_env: "ZAI_API_KEY".to_string(),
66            ollama_env: "OLLAMA_API_KEY".to_string(),
67            lmstudio_env: "LMSTUDIO_API_KEY".to_string(),
68            gemini_config: None,
69            anthropic_config: None,
70            openai_config: None,
71            openrouter_config: None,
72            xai_config: None,
73            deepseek_config: None,
74            zai_config: None,
75            ollama_config: None,
76            lmstudio_config: None,
77        }
78    }
79}
80
81impl ApiKeySources {
82    /// Create API key sources for a specific provider with automatic environment variable inference
83    pub fn for_provider(_provider: &str) -> Self {
84        Self::default()
85    }
86}
87
88fn inferred_api_key_env(provider: &str) -> &'static str {
89    Provider::from_str(provider)
90        .map(|resolved| resolved.default_api_key_env())
91        .unwrap_or("GEMINI_API_KEY")
92}
93
94/// Load environment variables from .env file
95///
96/// This function attempts to load environment variables from a .env file
97/// in the current directory. It logs a warning if the file exists but cannot
98/// be loaded, but doesn't fail if the file doesn't exist.
99pub fn load_dotenv() -> Result<()> {
100    match dotenvy::dotenv() {
101        Ok(path) => {
102            // Only print in verbose mode to avoid polluting stdout/stderr in scripts
103            if std::env::var("VTCODE_VERBOSE").is_ok() || std::env::var("RUST_LOG").is_ok() {
104                tracing::info!("Loaded environment variables from: {}", path.display());
105            }
106            Ok(())
107        }
108        Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
109            // .env file doesn't exist, which is fine
110            Ok(())
111        }
112        Err(e) => {
113            tracing::warn!("Failed to load .env file: {}", e);
114            Ok(())
115        }
116    }
117}
118
119/// Get API key for a specific provider with secure fallback mechanism
120///
121/// This function implements a secure retrieval mechanism that:
122/// 1. First checks environment variables (highest priority for security)
123/// 2. Then checks .env file values
124/// 3. Falls back to configuration file values if neither above is set
125/// 4. Supports all major providers: Gemini, Anthropic, OpenAI, OpenRouter, and xAI
126/// 5. Automatically infers the correct environment variable based on provider
127///
128/// # Arguments
129///
130/// * `provider` - The provider name ("gemini", "anthropic", or "openai")
131/// * `sources` - Configuration for where to look for API keys
132///
133/// # Returns
134///
135/// * `Ok(String)` - The API key if found
136/// * `Err` - If no API key could be found for the provider
137pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
138    let normalized_provider = provider.to_lowercase();
139    // Automatically infer the correct environment variable based on provider
140    let inferred_env = inferred_api_key_env(&normalized_provider);
141
142    // Try the inferred environment variable first
143    if let Ok(key) = env::var(inferred_env)
144        && !key.is_empty()
145    {
146        return Ok(key);
147    }
148
149    // Try secure storage (keyring) for custom API keys
150    if let Ok(Some(key)) = get_custom_api_key_from_keyring(&normalized_provider) {
151        return Ok(key);
152    }
153
154    // Fall back to the provider-specific sources
155    match normalized_provider.as_str() {
156        "gemini" => get_gemini_api_key(sources),
157        "anthropic" => get_anthropic_api_key(sources),
158        "openai" => get_openai_api_key(sources),
159        "deepseek" => get_deepseek_api_key(sources),
160        "openrouter" => get_openrouter_api_key(sources),
161        "xai" => get_xai_api_key(sources),
162        "zai" => get_zai_api_key(sources),
163        "ollama" => get_ollama_api_key(sources),
164        "lmstudio" => get_lmstudio_api_key(sources),
165        "huggingface" => env::var("HF_TOKEN").map_err(|_| anyhow::anyhow!("HF_TOKEN not set")),
166        _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
167    }
168}
169
170/// Get custom API key from secure storage (keyring).
171///
172/// This function retrieves API keys that were stored securely via the model picker
173/// or interactive configuration flows.
174///
175/// # Arguments
176/// * `provider` - The provider name
177///
178/// # Returns
179/// * `Ok(Some(String))` - The API key if found in keyring
180/// * `Ok(None)` - If no key is stored for this provider
181/// * `Err` - If there was an error accessing the keyring
182fn get_custom_api_key_from_keyring(provider: &str) -> Result<Option<String>> {
183    let storage = CustomApiKeyStorage::new(provider);
184    // Use default storage mode (keyring)
185    let mode = crate::auth::AuthCredentialsStoreMode::default();
186    storage.load(mode)
187}
188
189/// Get API key for a specific environment variable with fallback
190fn get_api_key_with_fallback(
191    env_var: &str,
192    config_value: Option<&String>,
193    provider_name: &str,
194) -> Result<String> {
195    // First try environment variable (most secure)
196    if let Ok(key) = env::var(env_var)
197        && !key.is_empty()
198    {
199        return Ok(key);
200    }
201
202    // Then try configuration file value
203    if let Some(key) = config_value
204        && !key.is_empty()
205    {
206        return Ok(key.clone());
207    }
208
209    // If neither worked, return an error
210    Err(anyhow::anyhow!(
211        "No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
212        provider_name,
213        env_var
214    ))
215}
216
217fn get_optional_api_key_with_fallback(env_var: &str, config_value: Option<&String>) -> String {
218    if let Ok(key) = env::var(env_var)
219        && !key.is_empty()
220    {
221        return key;
222    }
223
224    if let Some(key) = config_value
225        && !key.is_empty()
226    {
227        return key.clone();
228    }
229
230    String::new()
231}
232
233/// Get Gemini API key with secure fallback
234fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
235    // Try primary Gemini environment variable
236    if let Ok(key) = env::var(&sources.gemini_env)
237        && !key.is_empty()
238    {
239        return Ok(key);
240    }
241
242    // Try Google API key as fallback (for backward compatibility)
243    if let Ok(key) = env::var("GOOGLE_API_KEY")
244        && !key.is_empty()
245    {
246        return Ok(key);
247    }
248
249    // Try configuration file value
250    if let Some(key) = &sources.gemini_config
251        && !key.is_empty()
252    {
253        return Ok(key.clone());
254    }
255
256    // If nothing worked, return an error
257    Err(anyhow::anyhow!(
258        "No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
259        sources.gemini_env
260    ))
261}
262
263/// Get Anthropic API key with secure fallback
264fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
265    get_api_key_with_fallback(
266        &sources.anthropic_env,
267        sources.anthropic_config.as_ref(),
268        "Anthropic",
269    )
270}
271
272/// Get OpenAI API key with secure fallback
273fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
274    get_api_key_with_fallback(
275        &sources.openai_env,
276        sources.openai_config.as_ref(),
277        "OpenAI",
278    )
279}
280
281/// Get OpenRouter API key with secure fallback
282///
283/// This function checks for credentials in the following order:
284/// 1. OAuth token from encrypted storage (if OAuth is enabled)
285/// 2. Environment variable (OPENROUTER_API_KEY)
286/// 3. Configuration file value
287fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
288    // First, try to load OAuth token from encrypted storage
289    if let Ok(Some(token)) = crate::auth::load_oauth_token() {
290        tracing::debug!("Using OAuth token for OpenRouter authentication");
291        return Ok(token.api_key);
292    }
293
294    // Fall back to standard API key retrieval
295    get_api_key_with_fallback(
296        &sources.openrouter_env,
297        sources.openrouter_config.as_ref(),
298        "OpenRouter",
299    )
300}
301
302/// Get xAI API key with secure fallback
303fn get_xai_api_key(sources: &ApiKeySources) -> Result<String> {
304    get_api_key_with_fallback(&sources.xai_env, sources.xai_config.as_ref(), "xAI")
305}
306
307/// Get DeepSeek API key with secure fallback
308fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
309    get_api_key_with_fallback(
310        &sources.deepseek_env,
311        sources.deepseek_config.as_ref(),
312        "DeepSeek",
313    )
314}
315
316/// Get Z.AI API key with secure fallback
317fn get_zai_api_key(sources: &ApiKeySources) -> Result<String> {
318    get_api_key_with_fallback(&sources.zai_env, sources.zai_config.as_ref(), "Z.AI")
319}
320
321/// Get Ollama API key with secure fallback
322fn get_ollama_api_key(sources: &ApiKeySources) -> Result<String> {
323    // For Ollama we allow running without credentials when connecting to a local deployment.
324    // Cloud variants still rely on environment/config values when present.
325    Ok(get_optional_api_key_with_fallback(
326        &sources.ollama_env,
327        sources.ollama_config.as_ref(),
328    ))
329}
330
331/// Get LM Studio API key with secure fallback
332fn get_lmstudio_api_key(sources: &ApiKeySources) -> Result<String> {
333    Ok(get_optional_api_key_with_fallback(
334        &sources.lmstudio_env,
335        sources.lmstudio_config.as_ref(),
336    ))
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use std::env;
343
344    #[test]
345    fn test_get_gemini_api_key_from_env() {
346        // Set environment variable
347        unsafe {
348            env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
349        }
350
351        let sources = ApiKeySources {
352            gemini_env: "TEST_GEMINI_KEY".to_string(),
353            ..Default::default()
354        };
355
356        let result = get_gemini_api_key(&sources);
357        assert!(result.is_ok());
358        assert_eq!(result.unwrap(), "test-gemini-key");
359
360        // Clean up
361        unsafe {
362            env::remove_var("TEST_GEMINI_KEY");
363        }
364    }
365
366    #[test]
367    fn test_get_anthropic_api_key_from_env() {
368        // Set environment variable
369        unsafe {
370            env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
371        }
372
373        let sources = ApiKeySources {
374            anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
375            ..Default::default()
376        };
377
378        let result = get_anthropic_api_key(&sources);
379        assert!(result.is_ok());
380        assert_eq!(result.unwrap(), "test-anthropic-key");
381
382        // Clean up
383        unsafe {
384            env::remove_var("TEST_ANTHROPIC_KEY");
385        }
386    }
387
388    #[test]
389    fn test_get_openai_api_key_from_env() {
390        // Set environment variable
391        unsafe {
392            env::set_var("TEST_OPENAI_KEY", "test-openai-key");
393        }
394
395        let sources = ApiKeySources {
396            openai_env: "TEST_OPENAI_KEY".to_string(),
397            ..Default::default()
398        };
399
400        let result = get_openai_api_key(&sources);
401        assert!(result.is_ok());
402        assert_eq!(result.unwrap(), "test-openai-key");
403
404        // Clean up
405        unsafe {
406            env::remove_var("TEST_OPENAI_KEY");
407        }
408    }
409
410    #[test]
411    fn test_get_deepseek_api_key_from_env() {
412        unsafe {
413            env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
414        }
415
416        let sources = ApiKeySources {
417            deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
418            ..Default::default()
419        };
420
421        let result = get_deepseek_api_key(&sources);
422        assert!(result.is_ok());
423        assert_eq!(result.unwrap(), "test-deepseek-key");
424
425        unsafe {
426            env::remove_var("TEST_DEEPSEEK_KEY");
427        }
428    }
429
430    #[test]
431    fn test_get_xai_api_key_from_env() {
432        unsafe {
433            env::set_var("TEST_XAI_KEY", "test-xai-key");
434        }
435
436        let sources = ApiKeySources {
437            xai_env: "TEST_XAI_KEY".to_string(),
438            ..Default::default()
439        };
440
441        let result = get_xai_api_key(&sources);
442        assert!(result.is_ok());
443        assert_eq!(result.unwrap(), "test-xai-key");
444
445        unsafe {
446            env::remove_var("TEST_XAI_KEY");
447        }
448    }
449
450    #[test]
451    fn test_get_gemini_api_key_from_config() {
452        let prior_gemini_key = env::var("TEST_GEMINI_CONFIG_KEY").ok();
453        let prior_google_key = env::var("GOOGLE_API_KEY").ok();
454
455        unsafe {
456            env::remove_var("TEST_GEMINI_CONFIG_KEY");
457            env::remove_var("GOOGLE_API_KEY");
458        }
459
460        let sources = ApiKeySources {
461            gemini_env: "TEST_GEMINI_CONFIG_KEY".to_string(),
462            gemini_config: Some("config-gemini-key".to_string()),
463            ..Default::default()
464        };
465
466        let result = get_gemini_api_key(&sources);
467        assert!(result.is_ok());
468        assert_eq!(result.unwrap(), "config-gemini-key");
469
470        unsafe {
471            if let Some(value) = prior_gemini_key {
472                env::set_var("TEST_GEMINI_CONFIG_KEY", value);
473            } else {
474                env::remove_var("TEST_GEMINI_CONFIG_KEY");
475            }
476            if let Some(value) = prior_google_key {
477                env::set_var("GOOGLE_API_KEY", value);
478            } else {
479                env::remove_var("GOOGLE_API_KEY");
480            }
481        }
482    }
483
484    #[test]
485    fn test_get_api_key_with_fallback_prefers_env() {
486        // Set environment variable
487        unsafe {
488            env::set_var("TEST_FALLBACK_KEY", "env-key");
489        }
490
491        let sources = ApiKeySources {
492            openai_env: "TEST_FALLBACK_KEY".to_string(),
493            openai_config: Some("config-key".to_string()),
494            ..Default::default()
495        };
496
497        let result = get_openai_api_key(&sources);
498        assert!(result.is_ok());
499        assert_eq!(result.unwrap(), "env-key"); // Should prefer env var
500
501        // Clean up
502        unsafe {
503            env::remove_var("TEST_FALLBACK_KEY");
504        }
505    }
506
507    #[test]
508    fn test_get_api_key_fallback_to_config() {
509        let sources = ApiKeySources {
510            openai_env: "NONEXISTENT_ENV_VAR".to_string(),
511            openai_config: Some("config-key".to_string()),
512            ..Default::default()
513        };
514
515        let result = get_openai_api_key(&sources);
516        assert!(result.is_ok());
517        assert_eq!(result.unwrap(), "config-key");
518    }
519
520    #[test]
521    fn test_get_api_key_error_when_not_found() {
522        let sources = ApiKeySources {
523            openai_env: "NONEXISTENT_ENV_VAR".to_string(),
524            ..Default::default()
525        };
526
527        let result = get_openai_api_key(&sources);
528        assert!(result.is_err());
529    }
530
531    #[test]
532    fn test_get_ollama_api_key_missing_sources() {
533        let sources = ApiKeySources {
534            ollama_env: "NONEXISTENT_OLLAMA_ENV".to_string(),
535            ..Default::default()
536        };
537
538        let result = get_ollama_api_key(&sources).expect("Ollama key retrieval should succeed");
539        assert!(result.is_empty());
540    }
541
542    #[test]
543    fn test_get_ollama_api_key_from_env() {
544        // Set environment variable
545        unsafe {
546            env::set_var("TEST_OLLAMA_KEY", "test-ollama-key");
547        }
548
549        let sources = ApiKeySources {
550            ollama_env: "TEST_OLLAMA_KEY".to_string(),
551            ..Default::default()
552        };
553
554        let result = get_ollama_api_key(&sources);
555        assert!(result.is_ok());
556        assert_eq!(result.unwrap(), "test-ollama-key");
557
558        // Clean up
559        unsafe {
560            env::remove_var("TEST_OLLAMA_KEY");
561        }
562    }
563
564    #[test]
565    fn test_get_lmstudio_api_key_missing_sources() {
566        let sources = ApiKeySources {
567            lmstudio_env: "NONEXISTENT_LMSTUDIO_ENV".to_string(),
568            ..Default::default()
569        };
570
571        let result =
572            get_lmstudio_api_key(&sources).expect("LM Studio key retrieval should succeed");
573        assert!(result.is_empty());
574    }
575
576    #[test]
577    fn test_get_lmstudio_api_key_from_env() {
578        unsafe {
579            env::set_var("TEST_LMSTUDIO_KEY", "test-lmstudio-key");
580        }
581
582        let sources = ApiKeySources {
583            lmstudio_env: "TEST_LMSTUDIO_KEY".to_string(),
584            ..Default::default()
585        };
586
587        let result = get_lmstudio_api_key(&sources);
588        assert!(result.is_ok());
589        assert_eq!(result.unwrap(), "test-lmstudio-key");
590
591        unsafe {
592            env::remove_var("TEST_LMSTUDIO_KEY");
593        }
594    }
595
596    #[test]
597    fn test_get_api_key_ollama_provider() {
598        // Set environment variable
599        unsafe {
600            env::set_var("OLLAMA_API_KEY", "test-ollama-env-key");
601        }
602
603        let sources = ApiKeySources::default();
604        let result = get_api_key("ollama", &sources);
605        assert!(result.is_ok());
606        assert_eq!(result.unwrap(), "test-ollama-env-key");
607
608        // Clean up
609        unsafe {
610            env::remove_var("OLLAMA_API_KEY");
611        }
612    }
613
614    #[test]
615    fn test_get_api_key_lmstudio_provider() {
616        unsafe {
617            env::set_var("LMSTUDIO_API_KEY", "test-lmstudio-env-key");
618        }
619
620        let sources = ApiKeySources::default();
621        let result = get_api_key("lmstudio", &sources);
622        assert!(result.is_ok());
623        assert_eq!(result.unwrap(), "test-lmstudio-env-key");
624
625        unsafe {
626            env::remove_var("LMSTUDIO_API_KEY");
627        }
628    }
629}