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 gemini_config: Option<String>,
26 pub anthropic_config: Option<String>,
28 pub openai_config: Option<String>,
30 pub openrouter_config: Option<String>,
32 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 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 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
102pub 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 Ok(())
116 }
117 Err(e) => {
118 eprintln!("Warning: Failed to load .env file: {}", e);
119 Ok(())
120 }
121 }
122}
123
124pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
143 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 if let Ok(key) = env::var(inferred_env) {
156 if !key.is_empty() {
157 return Ok(key);
158 }
159 }
160
161 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
172fn get_api_key_with_fallback(
174 env_var: &str,
175 config_value: Option<&String>,
176 provider_name: &str,
177) -> Result<String> {
178 if let Ok(key) = env::var(env_var) {
180 if !key.is_empty() {
181 return Ok(key);
182 }
183 }
184
185 if let Some(key) = config_value {
187 if !key.is_empty() {
188 return Ok(key.clone());
189 }
190 }
191
192 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
200fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
202 if let Ok(key) = env::var(&sources.gemini_env) {
204 if !key.is_empty() {
205 return Ok(key);
206 }
207 }
208
209 if let Ok(key) = env::var("GOOGLE_API_KEY") {
211 if !key.is_empty() {
212 return Ok(key);
213 }
214 }
215
216 if let Some(key) = &sources.gemini_config {
218 if !key.is_empty() {
219 return Ok(key.clone());
220 }
221 }
222
223 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
230fn 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
239fn 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
248fn 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
257fn 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 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 unsafe {
285 env::remove_var("TEST_GEMINI_KEY");
286 }
287 }
288
289 #[test]
290 fn test_get_anthropic_api_key_from_env() {
291 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 unsafe {
307 env::remove_var("TEST_ANTHROPIC_KEY");
308 }
309 }
310
311 #[test]
312 fn test_get_openai_api_key_from_env() {
313 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 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 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"); 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}