vtcode_core/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;
10
11/// API key sources for different providers
12#[derive(Debug, Clone)]
13pub struct ApiKeySources {
14    /// Gemini API key environment variable name
15    pub gemini_env: String,
16    /// Anthropic API key environment variable name
17    pub anthropic_env: String,
18    /// OpenAI API key environment variable name
19    pub openai_env: String,
20    /// OpenRouter API key environment variable name
21    pub openrouter_env: String,
22    /// xAI API key environment variable name
23    pub xai_env: String,
24    /// DeepSeek API key environment variable name
25    pub deepseek_env: String,
26    /// Gemini API key from configuration file
27    pub gemini_config: Option<String>,
28    /// Anthropic API key from configuration file
29    pub anthropic_config: Option<String>,
30    /// OpenAI API key from configuration file
31    pub openai_config: Option<String>,
32    /// OpenRouter API key from configuration file
33    pub openrouter_config: Option<String>,
34    /// xAI API key from configuration file
35    pub xai_config: Option<String>,
36    /// DeepSeek API key from configuration file
37    pub deepseek_config: Option<String>,
38}
39
40impl Default for ApiKeySources {
41    fn default() -> Self {
42        Self {
43            gemini_env: "GEMINI_API_KEY".to_string(),
44            anthropic_env: "ANTHROPIC_API_KEY".to_string(),
45            openai_env: "OPENAI_API_KEY".to_string(),
46            openrouter_env: "OPENROUTER_API_KEY".to_string(),
47            xai_env: "XAI_API_KEY".to_string(),
48            deepseek_env: "DEEPSEEK_API_KEY".to_string(),
49            gemini_config: None,
50            anthropic_config: None,
51            openai_config: None,
52            openrouter_config: None,
53            xai_config: None,
54            deepseek_config: None,
55        }
56    }
57}
58
59impl ApiKeySources {
60    /// Create API key sources for a specific provider with automatic environment variable inference
61    pub fn for_provider(provider: &str) -> Self {
62        let (primary_env, _fallback_envs) = match provider.to_lowercase().as_str() {
63            "gemini" => ("GEMINI_API_KEY", vec!["GOOGLE_API_KEY"]),
64            "anthropic" => ("ANTHROPIC_API_KEY", vec![]),
65            "openai" => ("OPENAI_API_KEY", vec![]),
66            "deepseek" => ("DEEPSEEK_API_KEY", vec![]),
67            "openrouter" => ("OPENROUTER_API_KEY", vec![]),
68            "xai" => ("XAI_API_KEY", vec![]),
69            _ => ("GEMINI_API_KEY", vec!["GOOGLE_API_KEY"]),
70        };
71
72        // For backward compatibility, we still set all env vars but prioritize the primary one
73        Self {
74            gemini_env: if provider == "gemini" {
75                primary_env.to_string()
76            } else {
77                "GEMINI_API_KEY".to_string()
78            },
79            anthropic_env: if provider == "anthropic" {
80                primary_env.to_string()
81            } else {
82                "ANTHROPIC_API_KEY".to_string()
83            },
84            openai_env: if provider == "openai" {
85                primary_env.to_string()
86            } else {
87                "OPENAI_API_KEY".to_string()
88            },
89            openrouter_env: if provider == "openrouter" {
90                primary_env.to_string()
91            } else {
92                "OPENROUTER_API_KEY".to_string()
93            },
94            xai_env: if provider == "xai" {
95                primary_env.to_string()
96            } else {
97                "XAI_API_KEY".to_string()
98            },
99            deepseek_env: if provider == "deepseek" {
100                primary_env.to_string()
101            } else {
102                "DEEPSEEK_API_KEY".to_string()
103            },
104            gemini_config: None,
105            anthropic_config: None,
106            openai_config: None,
107            openrouter_config: None,
108            xai_config: None,
109            deepseek_config: None,
110        }
111    }
112}
113
114/// Load environment variables from .env file
115///
116/// This function attempts to load environment variables from a .env file
117/// in the current directory. It logs a warning if the file exists but cannot
118/// be loaded, but doesn't fail if the file doesn't exist.
119pub fn load_dotenv() -> Result<()> {
120    match dotenvy::dotenv() {
121        Ok(path) => {
122            eprintln!("Loaded environment variables from: {}", path.display());
123            Ok(())
124        }
125        Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
126            // .env file doesn't exist, which is fine
127            Ok(())
128        }
129        Err(e) => {
130            eprintln!("Warning: Failed to load .env file: {}", e);
131            Ok(())
132        }
133    }
134}
135
136/// Get API key for a specific provider with secure fallback mechanism
137///
138/// This function implements a secure retrieval mechanism that:
139/// 1. First checks environment variables (highest priority for security)
140/// 2. Then checks .env file values
141/// 3. Falls back to configuration file values if neither above is set
142/// 4. Supports all major providers: Gemini, Anthropic, OpenAI, OpenRouter, and xAI
143/// 5. Automatically infers the correct environment variable based on provider
144///
145/// # Arguments
146///
147/// * `provider` - The provider name ("gemini", "anthropic", or "openai")
148/// * `sources` - Configuration for where to look for API keys
149///
150/// # Returns
151///
152/// * `Ok(String)` - The API key if found
153/// * `Err` - If no API key could be found for the provider
154pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
155    // Automatically infer the correct environment variable based on provider
156    let inferred_env = match provider.to_lowercase().as_str() {
157        "gemini" => "GEMINI_API_KEY",
158        "anthropic" => "ANTHROPIC_API_KEY",
159        "openai" => "OPENAI_API_KEY",
160        "deepseek" => "DEEPSEEK_API_KEY",
161        "openrouter" => "OPENROUTER_API_KEY",
162        "xai" => "XAI_API_KEY",
163        _ => "GEMINI_API_KEY",
164    };
165
166    // Try the inferred environment variable first
167    if let Ok(key) = env::var(inferred_env) {
168        if !key.is_empty() {
169            return Ok(key);
170        }
171    }
172
173    // Fall back to the provider-specific sources
174    match provider.to_lowercase().as_str() {
175        "gemini" => get_gemini_api_key(sources),
176        "anthropic" => get_anthropic_api_key(sources),
177        "openai" => get_openai_api_key(sources),
178        "deepseek" => get_deepseek_api_key(sources),
179        "openrouter" => get_openrouter_api_key(sources),
180        "xai" => get_xai_api_key(sources),
181        _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
182    }
183}
184
185/// Get API key for a specific environment variable with fallback
186fn get_api_key_with_fallback(
187    env_var: &str,
188    config_value: Option<&String>,
189    provider_name: &str,
190) -> Result<String> {
191    // First try environment variable (most secure)
192    if let Ok(key) = env::var(env_var) {
193        if !key.is_empty() {
194            return Ok(key);
195        }
196    }
197
198    // Then try configuration file value
199    if let Some(key) = config_value {
200        if !key.is_empty() {
201            return Ok(key.clone());
202        }
203    }
204
205    // If neither worked, return an error
206    Err(anyhow::anyhow!(
207        "No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
208        provider_name,
209        env_var
210    ))
211}
212
213/// Get Gemini API key with secure fallback
214fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
215    // Try primary Gemini environment variable
216    if let Ok(key) = env::var(&sources.gemini_env) {
217        if !key.is_empty() {
218            return Ok(key);
219        }
220    }
221
222    // Try Google API key as fallback (for backward compatibility)
223    if let Ok(key) = env::var("GOOGLE_API_KEY") {
224        if !key.is_empty() {
225            return Ok(key);
226        }
227    }
228
229    // Try configuration file value
230    if let Some(key) = &sources.gemini_config {
231        if !key.is_empty() {
232            return Ok(key.clone());
233        }
234    }
235
236    // If nothing worked, return an error
237    Err(anyhow::anyhow!(
238        "No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
239        sources.gemini_env
240    ))
241}
242
243/// Get Anthropic API key with secure fallback
244fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
245    get_api_key_with_fallback(
246        &sources.anthropic_env,
247        sources.anthropic_config.as_ref(),
248        "Anthropic",
249    )
250}
251
252/// Get OpenAI API key with secure fallback
253fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
254    get_api_key_with_fallback(
255        &sources.openai_env,
256        sources.openai_config.as_ref(),
257        "OpenAI",
258    )
259}
260
261/// Get OpenRouter API key with secure fallback
262fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
263    get_api_key_with_fallback(
264        &sources.openrouter_env,
265        sources.openrouter_config.as_ref(),
266        "OpenRouter",
267    )
268}
269
270/// Get xAI API key with secure fallback
271fn get_xai_api_key(sources: &ApiKeySources) -> Result<String> {
272    get_api_key_with_fallback(&sources.xai_env, sources.xai_config.as_ref(), "xAI")
273}
274
275/// Get DeepSeek API key with secure fallback
276fn get_deepseek_api_key(sources: &ApiKeySources) -> Result<String> {
277    get_api_key_with_fallback(
278        &sources.deepseek_env,
279        sources.deepseek_config.as_ref(),
280        "DeepSeek",
281    )
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use std::env;
288
289    #[test]
290    fn test_get_gemini_api_key_from_env() {
291        // Set environment variable
292        unsafe {
293            env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
294        }
295
296        let sources = ApiKeySources {
297            gemini_env: "TEST_GEMINI_KEY".to_string(),
298            ..Default::default()
299        };
300
301        let result = get_gemini_api_key(&sources);
302        assert!(result.is_ok());
303        assert_eq!(result.unwrap(), "test-gemini-key");
304
305        // Clean up
306        unsafe {
307            env::remove_var("TEST_GEMINI_KEY");
308        }
309    }
310
311    #[test]
312    fn test_get_anthropic_api_key_from_env() {
313        // Set environment variable
314        unsafe {
315            env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
316        }
317
318        let sources = ApiKeySources {
319            anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
320            ..Default::default()
321        };
322
323        let result = get_anthropic_api_key(&sources);
324        assert!(result.is_ok());
325        assert_eq!(result.unwrap(), "test-anthropic-key");
326
327        // Clean up
328        unsafe {
329            env::remove_var("TEST_ANTHROPIC_KEY");
330        }
331    }
332
333    #[test]
334    fn test_get_openai_api_key_from_env() {
335        // Set environment variable
336        unsafe {
337            env::set_var("TEST_OPENAI_KEY", "test-openai-key");
338        }
339
340        let sources = ApiKeySources {
341            openai_env: "TEST_OPENAI_KEY".to_string(),
342            ..Default::default()
343        };
344
345        let result = get_openai_api_key(&sources);
346        assert!(result.is_ok());
347        assert_eq!(result.unwrap(), "test-openai-key");
348
349        // Clean up
350        unsafe {
351            env::remove_var("TEST_OPENAI_KEY");
352        }
353    }
354
355    #[test]
356    fn test_get_deepseek_api_key_from_env() {
357        unsafe {
358            env::set_var("TEST_DEEPSEEK_KEY", "test-deepseek-key");
359        }
360
361        let sources = ApiKeySources {
362            deepseek_env: "TEST_DEEPSEEK_KEY".to_string(),
363            ..Default::default()
364        };
365
366        let result = get_deepseek_api_key(&sources);
367        assert!(result.is_ok());
368        assert_eq!(result.unwrap(), "test-deepseek-key");
369
370        unsafe {
371            env::remove_var("TEST_DEEPSEEK_KEY");
372        }
373    }
374
375    #[test]
376    fn test_get_xai_api_key_from_env() {
377        unsafe {
378            env::set_var("TEST_XAI_KEY", "test-xai-key");
379        }
380
381        let sources = ApiKeySources {
382            xai_env: "TEST_XAI_KEY".to_string(),
383            ..Default::default()
384        };
385
386        let result = get_xai_api_key(&sources);
387        assert!(result.is_ok());
388        assert_eq!(result.unwrap(), "test-xai-key");
389
390        unsafe {
391            env::remove_var("TEST_XAI_KEY");
392        }
393    }
394
395    #[test]
396    fn test_get_gemini_api_key_from_config() {
397        let sources = ApiKeySources {
398            gemini_config: Some("config-gemini-key".to_string()),
399            ..Default::default()
400        };
401
402        let result = get_gemini_api_key(&sources);
403        assert!(result.is_ok());
404        assert_eq!(result.unwrap(), "config-gemini-key");
405    }
406
407    #[test]
408    fn test_get_api_key_with_fallback_prefers_env() {
409        // Set environment variable
410        unsafe {
411            env::set_var("TEST_FALLBACK_KEY", "env-key");
412        }
413
414        let sources = ApiKeySources {
415            openai_env: "TEST_FALLBACK_KEY".to_string(),
416            openai_config: Some("config-key".to_string()),
417            ..Default::default()
418        };
419
420        let result = get_openai_api_key(&sources);
421        assert!(result.is_ok());
422        assert_eq!(result.unwrap(), "env-key"); // Should prefer env var
423
424        // Clean up
425        unsafe {
426            env::remove_var("TEST_FALLBACK_KEY");
427        }
428    }
429
430    #[test]
431    fn test_get_api_key_fallback_to_config() {
432        let sources = ApiKeySources {
433            openai_env: "NONEXISTENT_ENV_VAR".to_string(),
434            openai_config: Some("config-key".to_string()),
435            ..Default::default()
436        };
437
438        let result = get_openai_api_key(&sources);
439        assert!(result.is_ok());
440        assert_eq!(result.unwrap(), "config-key");
441    }
442
443    #[test]
444    fn test_get_api_key_error_when_not_found() {
445        let sources = ApiKeySources {
446            openai_env: "NONEXISTENT_ENV_VAR".to_string(),
447            ..Default::default()
448        };
449
450        let result = get_openai_api_key(&sources);
451        assert!(result.is_err());
452    }
453}