1use anyhow::Result;
9use std::env;
10
11#[derive(Debug, Clone)]
13pub struct ApiKeySources {
14 pub gemini_env: String,
16 pub anthropic_env: String,
18 pub openai_env: String,
20 pub openrouter_env: String,
22 pub xai_env: String,
24 pub deepseek_env: String,
26 pub gemini_config: Option<String>,
28 pub anthropic_config: Option<String>,
30 pub openai_config: Option<String>,
32 pub openrouter_config: Option<String>,
34 pub xai_config: Option<String>,
36 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 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 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
114pub 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 Ok(())
128 }
129 Err(e) => {
130 eprintln!("Warning: Failed to load .env file: {}", e);
131 Ok(())
132 }
133 }
134}
135
136pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
155 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 if let Ok(key) = env::var(inferred_env) {
168 if !key.is_empty() {
169 return Ok(key);
170 }
171 }
172
173 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
185fn get_api_key_with_fallback(
187 env_var: &str,
188 config_value: Option<&String>,
189 provider_name: &str,
190) -> Result<String> {
191 if let Ok(key) = env::var(env_var) {
193 if !key.is_empty() {
194 return Ok(key);
195 }
196 }
197
198 if let Some(key) = config_value {
200 if !key.is_empty() {
201 return Ok(key.clone());
202 }
203 }
204
205 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
213fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
215 if let Ok(key) = env::var(&sources.gemini_env) {
217 if !key.is_empty() {
218 return Ok(key);
219 }
220 }
221
222 if let Ok(key) = env::var("GOOGLE_API_KEY") {
224 if !key.is_empty() {
225 return Ok(key);
226 }
227 }
228
229 if let Some(key) = &sources.gemini_config {
231 if !key.is_empty() {
232 return Ok(key.clone());
233 }
234 }
235
236 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
243fn 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
252fn 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
261fn 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
270fn 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
275fn 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 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 unsafe {
307 env::remove_var("TEST_GEMINI_KEY");
308 }
309 }
310
311 #[test]
312 fn test_get_anthropic_api_key_from_env() {
313 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 unsafe {
329 env::remove_var("TEST_ANTHROPIC_KEY");
330 }
331 }
332
333 #[test]
334 fn test_get_openai_api_key_from_env() {
335 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 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 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"); 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}