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 gemini_config: Option<String>,
24 pub anthropic_config: Option<String>,
26 pub openai_config: Option<String>,
28 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 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 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
89pub 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 Ok(())
103 }
104 Err(e) => {
105 eprintln!("Warning: Failed to load .env file: {}", e);
106 Ok(())
107 }
108 }
109}
110
111pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
130 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 if let Ok(key) = env::var(inferred_env) {
142 if !key.is_empty() {
143 return Ok(key);
144 }
145 }
146
147 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
157fn get_api_key_with_fallback(
159 env_var: &str,
160 config_value: Option<&String>,
161 provider_name: &str,
162) -> Result<String> {
163 if let Ok(key) = env::var(env_var) {
165 if !key.is_empty() {
166 return Ok(key);
167 }
168 }
169
170 if let Some(key) = config_value {
172 if !key.is_empty() {
173 return Ok(key.clone());
174 }
175 }
176
177 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
185fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
187 if let Ok(key) = env::var(&sources.gemini_env) {
189 if !key.is_empty() {
190 return Ok(key);
191 }
192 }
193
194 if let Ok(key) = env::var("GOOGLE_API_KEY") {
196 if !key.is_empty() {
197 return Ok(key);
198 }
199 }
200
201 if let Some(key) = &sources.gemini_config {
203 if !key.is_empty() {
204 return Ok(key.clone());
205 }
206 }
207
208 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
215fn 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
224fn 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
233fn 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 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 unsafe {
265 env::remove_var("TEST_GEMINI_KEY");
266 }
267 }
268
269 #[test]
270 fn test_get_anthropic_api_key_from_env() {
271 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 unsafe {
287 env::remove_var("TEST_ANTHROPIC_KEY");
288 }
289 }
290
291 #[test]
292 fn test_get_openai_api_key_from_env() {
293 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 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 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"); 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}