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 zai_env: String,
28 pub gemini_config: Option<String>,
30 pub anthropic_config: Option<String>,
32 pub openai_config: Option<String>,
34 pub openrouter_config: Option<String>,
36 pub xai_config: Option<String>,
38 pub deepseek_config: Option<String>,
40 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 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 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
127pub 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 Ok(())
141 }
142 Err(e) => {
143 eprintln!("Warning: Failed to load .env file: {}", e);
144 Ok(())
145 }
146 }
147}
148
149pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
168 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 if let Ok(key) = env::var(inferred_env) {
182 if !key.is_empty() {
183 return Ok(key);
184 }
185 }
186
187 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
200fn get_api_key_with_fallback(
202 env_var: &str,
203 config_value: Option<&String>,
204 provider_name: &str,
205) -> Result<String> {
206 if let Ok(key) = env::var(env_var) {
208 if !key.is_empty() {
209 return Ok(key);
210 }
211 }
212
213 if let Some(key) = config_value {
215 if !key.is_empty() {
216 return Ok(key.clone());
217 }
218 }
219
220 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
228fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
230 if let Ok(key) = env::var(&sources.gemini_env) {
232 if !key.is_empty() {
233 return Ok(key);
234 }
235 }
236
237 if let Ok(key) = env::var("GOOGLE_API_KEY") {
239 if !key.is_empty() {
240 return Ok(key);
241 }
242 }
243
244 if let Some(key) = &sources.gemini_config {
246 if !key.is_empty() {
247 return Ok(key.clone());
248 }
249 }
250
251 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
258fn 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
267fn 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
276fn 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
285fn 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
290fn 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
299fn 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 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 unsafe {
327 env::remove_var("TEST_GEMINI_KEY");
328 }
329 }
330
331 #[test]
332 fn test_get_anthropic_api_key_from_env() {
333 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 unsafe {
349 env::remove_var("TEST_ANTHROPIC_KEY");
350 }
351 }
352
353 #[test]
354 fn test_get_openai_api_key_from_env() {
355 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 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 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"); 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}