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