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