use anyhow::Result;
use std::env;
#[derive(Debug, Clone)]
pub struct ApiKeySources {
pub gemini_env: String,
pub anthropic_env: String,
pub openai_env: String,
pub openrouter_env: String,
pub gemini_config: Option<String>,
pub anthropic_config: Option<String>,
pub openai_config: Option<String>,
pub openrouter_config: Option<String>,
}
impl Default for ApiKeySources {
fn default() -> Self {
Self {
gemini_env: "GEMINI_API_KEY".to_string(),
anthropic_env: "ANTHROPIC_API_KEY".to_string(),
openai_env: "OPENAI_API_KEY".to_string(),
openrouter_env: "OPENROUTER_API_KEY".to_string(),
gemini_config: None,
anthropic_config: None,
openai_config: None,
openrouter_config: None,
}
}
}
impl ApiKeySources {
pub fn for_provider(provider: &str) -> Self {
let (primary_env, _fallback_envs) = match provider.to_lowercase().as_str() {
"gemini" => ("GEMINI_API_KEY", vec!["GOOGLE_API_KEY"]),
"anthropic" => ("ANTHROPIC_API_KEY", vec![]),
"openai" => ("OPENAI_API_KEY", vec![]),
"deepseek" => ("DEEPSEEK_API_KEY", vec![]),
"openrouter" => ("OPENROUTER_API_KEY", vec![]),
_ => ("GEMINI_API_KEY", vec!["GOOGLE_API_KEY"]),
};
Self {
gemini_env: if provider == "gemini" {
primary_env.to_string()
} else {
"GEMINI_API_KEY".to_string()
},
anthropic_env: if provider == "anthropic" {
primary_env.to_string()
} else {
"ANTHROPIC_API_KEY".to_string()
},
openai_env: if provider == "openai" {
primary_env.to_string()
} else {
"OPENAI_API_KEY".to_string()
},
openrouter_env: if provider == "openrouter" {
primary_env.to_string()
} else {
"OPENROUTER_API_KEY".to_string()
},
gemini_config: None,
anthropic_config: None,
openai_config: None,
openrouter_config: None,
}
}
}
pub fn load_dotenv() -> Result<()> {
match dotenvy::dotenv() {
Ok(path) => {
eprintln!("Loaded environment variables from: {}", path.display());
Ok(())
}
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(())
}
Err(e) => {
eprintln!("Warning: Failed to load .env file: {}", e);
Ok(())
}
}
}
pub fn get_api_key(provider: &str, sources: &ApiKeySources) -> Result<String> {
let inferred_env = match provider.to_lowercase().as_str() {
"gemini" => "GEMINI_API_KEY",
"anthropic" => "ANTHROPIC_API_KEY",
"openai" => "OPENAI_API_KEY",
"deepseek" => "DEEPSEEK_API_KEY",
"openrouter" => "OPENROUTER_API_KEY",
_ => "GEMINI_API_KEY",
};
if let Ok(key) = env::var(inferred_env) {
if !key.is_empty() {
return Ok(key);
}
}
match provider.to_lowercase().as_str() {
"gemini" => get_gemini_api_key(sources),
"anthropic" => get_anthropic_api_key(sources),
"openai" => get_openai_api_key(sources),
"openrouter" => get_openrouter_api_key(sources),
_ => Err(anyhow::anyhow!("Unsupported provider: {}", provider)),
}
}
fn get_api_key_with_fallback(
env_var: &str,
config_value: Option<&String>,
provider_name: &str,
) -> Result<String> {
if let Ok(key) = env::var(env_var) {
if !key.is_empty() {
return Ok(key);
}
}
if let Some(key) = config_value {
if !key.is_empty() {
return Ok(key.clone());
}
}
Err(anyhow::anyhow!(
"No API key found for {} provider. Set {} environment variable (or add to .env file) or configure in vtcode.toml",
provider_name,
env_var
))
}
fn get_gemini_api_key(sources: &ApiKeySources) -> Result<String> {
if let Ok(key) = env::var(&sources.gemini_env) {
if !key.is_empty() {
return Ok(key);
}
}
if let Ok(key) = env::var("GOOGLE_API_KEY") {
if !key.is_empty() {
return Ok(key);
}
}
if let Some(key) = &sources.gemini_config {
if !key.is_empty() {
return Ok(key.clone());
}
}
Err(anyhow::anyhow!(
"No API key found for Gemini provider. Set {} or GOOGLE_API_KEY environment variable (or add to .env file) or configure in vtcode.toml",
sources.gemini_env
))
}
fn get_anthropic_api_key(sources: &ApiKeySources) -> Result<String> {
get_api_key_with_fallback(
&sources.anthropic_env,
sources.anthropic_config.as_ref(),
"Anthropic",
)
}
fn get_openai_api_key(sources: &ApiKeySources) -> Result<String> {
get_api_key_with_fallback(
&sources.openai_env,
sources.openai_config.as_ref(),
"OpenAI",
)
}
fn get_openrouter_api_key(sources: &ApiKeySources) -> Result<String> {
get_api_key_with_fallback(
&sources.openrouter_env,
sources.openrouter_config.as_ref(),
"OpenRouter",
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_get_gemini_api_key_from_env() {
unsafe {
env::set_var("TEST_GEMINI_KEY", "test-gemini-key");
}
let sources = ApiKeySources {
gemini_env: "TEST_GEMINI_KEY".to_string(),
..Default::default()
};
let result = get_gemini_api_key(&sources);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "test-gemini-key");
unsafe {
env::remove_var("TEST_GEMINI_KEY");
}
}
#[test]
fn test_get_anthropic_api_key_from_env() {
unsafe {
env::set_var("TEST_ANTHROPIC_KEY", "test-anthropic-key");
}
let sources = ApiKeySources {
anthropic_env: "TEST_ANTHROPIC_KEY".to_string(),
..Default::default()
};
let result = get_anthropic_api_key(&sources);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "test-anthropic-key");
unsafe {
env::remove_var("TEST_ANTHROPIC_KEY");
}
}
#[test]
fn test_get_openai_api_key_from_env() {
unsafe {
env::set_var("TEST_OPENAI_KEY", "test-openai-key");
}
let sources = ApiKeySources {
openai_env: "TEST_OPENAI_KEY".to_string(),
..Default::default()
};
let result = get_openai_api_key(&sources);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "test-openai-key");
unsafe {
env::remove_var("TEST_OPENAI_KEY");
}
}
#[test]
fn test_get_gemini_api_key_from_config() {
let sources = ApiKeySources {
gemini_config: Some("config-gemini-key".to_string()),
..Default::default()
};
let result = get_gemini_api_key(&sources);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "config-gemini-key");
}
#[test]
fn test_get_api_key_with_fallback_prefers_env() {
unsafe {
env::set_var("TEST_FALLBACK_KEY", "env-key");
}
let sources = ApiKeySources {
openai_env: "TEST_FALLBACK_KEY".to_string(),
openai_config: Some("config-key".to_string()),
..Default::default()
};
let result = get_openai_api_key(&sources);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "env-key");
unsafe {
env::remove_var("TEST_FALLBACK_KEY");
}
}
#[test]
fn test_get_api_key_fallback_to_config() {
let sources = ApiKeySources {
openai_env: "NONEXISTENT_ENV_VAR".to_string(),
openai_config: Some("config-key".to_string()),
..Default::default()
};
let result = get_openai_api_key(&sources);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "config-key");
}
#[test]
fn test_get_api_key_error_when_not_found() {
let sources = ApiKeySources {
openai_env: "NONEXISTENT_ENV_VAR".to_string(),
..Default::default()
};
let result = get_openai_api_key(&sources);
assert!(result.is_err());
}
}