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