use anyhow::Result;
use std::str::FromStr;
use crate::auth::CredentialIdentity;
use crate::constants::defaults;
use crate::models::Provider;
mod credential_resolution;
pub use credential_resolution::{
CredentialSource, ResolvedCredential, clear_credential_with_mode, get_api_key, get_api_key_with_mode,
load_stored_api_key_with_mode, load_stored_credential_with_mode, resolve_credential, resolve_credential_with_mode,
resolve_openai_api_key_for_auth, store_credential_with_mode,
};
#[derive(Debug, Clone, Default)]
pub struct ApiKeySources {
gemini_env: String,
anthropic_env: String,
openai_env: String,
openrouter_env: String,
deepseek_env: String,
zai_env: String,
ollama_env: String,
lmstudio_env: String,
gemini_config: Option<String>,
anthropic_config: Option<String>,
openai_config: Option<String>,
openrouter_config: Option<String>,
deepseek_config: Option<String>,
zai_config: Option<String>,
ollama_config: Option<String>,
lmstudio_config: Option<String>,
}
pub fn api_key_env_var(provider: &str) -> String {
let trimmed = provider.trim();
if trimmed.is_empty() {
return defaults::DEFAULT_API_KEY_ENV.to_owned();
}
if trimmed.eq_ignore_ascii_case("codex") {
return String::new();
}
if let Ok(resolved) = Provider::from_str(trimmed)
&& resolved.uses_managed_auth()
{
return String::new();
}
Provider::from_str(trimmed)
.map(|resolved| resolved.default_api_key_env().to_owned())
.unwrap_or_else(|_| {
let mut key = String::new();
for ch in trimmed.chars() {
if ch.is_ascii_alphanumeric() {
key.push(ch.to_ascii_uppercase());
} else if !key.ends_with('_') {
key.push('_');
}
}
if key.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
key.insert(0, '_');
}
if !key.ends_with("_API_KEY") {
if !key.ends_with('_') {
key.push('_');
}
key.push_str("API_KEY");
}
key
})
}
pub fn resolve_api_key_env(provider: &str, configured_env: &str) -> String {
let trimmed = configured_env.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case(defaults::DEFAULT_API_KEY_ENV) {
api_key_env_var(provider)
} else {
trimmed.to_owned()
}
}
pub fn credential_identity(provider: &str, key_name: &str) -> Result<Option<CredentialIdentity>> {
let default_key_name = api_key_env_var(provider);
let requested_key_name = if key_name.trim().is_empty() {
default_key_name
} else {
key_name.trim().to_owned()
};
if requested_key_name.is_empty() {
return Ok(None);
}
CredentialIdentity::new(provider, &requested_key_name).map(Some)
}
pub fn credential_metadata_key(provider: &str, key_name: &str) -> Result<Option<String>> {
Ok(credential_identity(provider, key_name)?
.map(|identity| format!("{}/{}", identity.provider(), identity.key_name())))
}
fn read_env_var(key: &str) -> Option<String> {
crate::env_helpers::read_env_var(key)
}
pub fn load_dotenv() -> Result<()> {
match dotenvy::dotenv() {
Ok(path) => {
if read_env_var("VTCODE_VERBOSE").is_some() || read_env_var("RUST_LOG").is_some() {
tracing::info!("Loaded environment variables from: {}", path.display());
}
Ok(())
}
Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
Ok(())
}
Err(e) => {
tracing::warn!("Failed to load .env file: {}", e);
Ok(())
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DiscoveredProvider {
pub provider: Provider,
pub source: CredentialSource,
pub env_var: Option<&'static str>,
}
pub fn provider_credential_detail(provider: Provider) -> Option<DiscoveredProvider> {
provider_credential_detail_with_mode(provider, crate::auth::AuthCredentialsStoreMode::default())
}
pub fn provider_credential_detail_with_mode(
provider: Provider,
storage_mode: crate::auth::AuthCredentialsStoreMode,
) -> Option<DiscoveredProvider> {
if provider.is_local() {
return Some(DiscoveredProvider {
provider,
source: CredentialSource::Local,
env_var: None,
});
}
if provider.uses_managed_auth() {
return Some(DiscoveredProvider {
provider,
source: CredentialSource::ManagedAuth,
env_var: None,
});
}
let resolved =
resolve_credential_with_mode(provider.as_ref(), provider.default_api_key_env(), None, storage_mode).ok()??;
if matches!(resolved.source, CredentialSource::SecureStorage)
|| matches!(resolved.source, CredentialSource::Env | CredentialSource::Workspace | CredentialSource::OAuth)
{
return Some(DiscoveredProvider {
provider,
source: resolved.source,
env_var: resolved.env_var.as_deref().and_then(static_env_var),
});
}
None
}
pub fn provider_credential_source(provider: Provider) -> Option<CredentialSource> {
provider_credential_detail(provider).map(|detail| detail.source)
}
pub fn discover_available_providers() -> Vec<DiscoveredProvider> {
discover_available_providers_with_mode(crate::auth::AuthCredentialsStoreMode::default())
}
pub fn discover_available_providers_with_mode(
storage_mode: crate::auth::AuthCredentialsStoreMode,
) -> Vec<DiscoveredProvider> {
Provider::all_providers()
.into_iter()
.filter_map(|provider| provider_credential_detail_with_mode(provider, storage_mode))
.collect()
}
pub fn find_discovered(discovered: &[DiscoveredProvider], provider: Provider) -> Option<&DiscoveredProvider> {
discovered.iter().find(|entry| entry.provider == provider)
}
pub fn has_oauth_or_managed_auth(discovered: &[DiscoveredProvider]) -> bool {
discovered
.iter()
.any(|entry| matches!(entry.source, CredentialSource::OAuth | CredentialSource::ManagedAuth))
}
fn static_env_var(env_key: &str) -> Option<&'static str> {
Provider::all_providers()
.into_iter()
.find(|provider| provider.default_api_key_env().eq_ignore_ascii_case(env_key))
.map(|provider| provider.default_api_key_env())
.or(match env_key {
"GOOGLE_API_KEY" => Some("GOOGLE_API_KEY"),
"DASHSCOPE_API_KEY" => Some("DASHSCOPE_API_KEY"),
_ => None,
})
}
fn alternate_env_var(provider: Provider) -> Option<&'static str> {
match provider {
Provider::Gemini => Some("GOOGLE_API_KEY"),
Provider::Qwen => Some("DASHSCOPE_API_KEY"),
_ => None,
}
}
#[cfg(test)]
fn test_storage_lookup_is_overridden(key_name: &str) -> bool {
crate::env_helpers::test_env_overrides::is_overridden(key_name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tempfile::tempdir;
static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
struct EnvOverrideGuard {
key: &'static str,
previous: Option<Option<String>>,
}
impl EnvOverrideGuard {
fn set(key: &'static str, value: Option<&str>) -> Self {
let previous = crate::env_helpers::test_env_overrides::get(key);
crate::env_helpers::test_env_overrides::set(key, value);
Self { key, previous }
}
}
impl Drop for EnvOverrideGuard {
fn drop(&mut self) {
crate::env_helpers::test_env_overrides::restore(self.key, self.previous.clone());
}
}
fn with_override<F>(key: &'static str, value: Option<&str>, f: F)
where
F: FnOnce(),
{
let _lock = ENV_TEST_LOCK.lock().expect("env test lock poisoned");
let _guard = EnvOverrideGuard::set(key, value);
f();
}
fn with_overrides<F>(overrides: &[(&'static str, Option<&str>)], f: F)
where
F: FnOnce(),
{
let _lock = ENV_TEST_LOCK.lock().expect("env test lock poisoned");
let _guards: Vec<_> = overrides
.iter()
.map(|(key, value)| EnvOverrideGuard::set(key, *value))
.collect();
f();
}
fn default_sources() -> ApiKeySources {
ApiKeySources::default()
}
#[test]
fn gemini_reads_env_var() {
with_override("GEMINI_API_KEY", Some("test-gemini-key"), || {
let result = get_api_key("gemini", &default_sources());
assert_eq!(result.unwrap(), "test-gemini-key");
});
}
#[test]
fn gemini_falls_back_to_google_api_key() {
with_overrides(
&[
("GEMINI_API_KEY", Some("gemini-primary")),
("GOOGLE_API_KEY", Some("google-fallback")),
],
|| {
let result = get_api_key("gemini", &default_sources());
assert_eq!(result.unwrap(), "gemini-primary");
},
);
with_overrides(&[("GEMINI_API_KEY", None), ("GOOGLE_API_KEY", Some("google-fallback"))], || {
let result = get_api_key("gemini", &default_sources());
assert_eq!(result.unwrap(), "google-fallback");
});
}
#[test]
fn anthropic_reads_env_var() {
with_override("ANTHROPIC_API_KEY", Some("test-anthropic-key"), || {
let result = get_api_key("anthropic", &default_sources());
assert_eq!(result.unwrap(), "test-anthropic-key");
});
}
#[test]
fn openai_reads_env_var() {
with_override("OPENAI_API_KEY", Some("test-openai-key"), || {
let result = get_api_key("openai", &default_sources());
assert_eq!(result.unwrap(), "test-openai-key");
});
}
#[test]
fn deepseek_reads_env_var() {
with_override("DEEPSEEK_API_KEY", Some("test-deepseek-key"), || {
let result = get_api_key("deepseek", &default_sources());
assert_eq!(result.unwrap(), "test-deepseek-key");
});
}
#[test]
fn qwen_falls_back_to_dashscope() {
with_overrides(&[("QWEN_API_KEY", None), ("DASHSCOPE_API_KEY", Some("dashscope-key"))], || {
let result = get_api_key("qwen", &default_sources());
assert_eq!(result.unwrap(), "dashscope-key");
});
}
#[test]
fn ollama_allows_empty_key() {
with_override("OLLAMA_API_KEY", None, || {
let result = get_api_key("ollama", &default_sources());
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
});
}
#[test]
fn lmstudio_allows_empty_key() {
with_override("LMSTUDIO_API_KEY", None, || {
let result = get_api_key("lmstudio", &default_sources());
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
});
}
#[test]
fn ollama_reads_env_var_when_set() {
with_override("OLLAMA_API_KEY", Some("test-ollama-key"), || {
let result = get_api_key("ollama", &default_sources());
assert_eq!(result.unwrap(), "test-ollama-key");
});
}
#[test]
fn copilot_returns_managed_auth_error() {
let result = get_api_key("copilot", &default_sources());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("copilot"));
}
#[test]
fn codex_returns_managed_auth_error() {
let result = get_api_key("codex", &default_sources());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("codex"));
}
#[test]
fn unknown_provider_returns_error_with_env_hint() {
with_override("SOMEUNKNOWN_API_KEY", None, || {
let result = get_api_key("someunknown", &default_sources());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("SOMEUNKNOWN_API_KEY"));
});
}
#[test]
fn poolside_reads_env_var() {
with_override("POOLSIDE_API_KEY", Some("test-poolside-key"), || {
let result = get_api_key("poolside", &default_sources());
assert_eq!(result.unwrap(), "test-poolside-key");
});
}
#[test]
fn poolside_returns_error_when_missing() {
with_override("POOLSIDE_API_KEY", None, || {
let result = get_api_key("poolside", &default_sources());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("POOLSIDE_API_KEY"));
});
}
#[test]
fn api_key_env_var_uses_provider_defaults() {
assert_eq!(api_key_env_var("codex"), "");
assert_eq!(api_key_env_var("minimax"), "MINIMAX_API_KEY");
assert_eq!(api_key_env_var("huggingface"), "HF_TOKEN");
assert_eq!(api_key_env_var("poolside"), "POOLSIDE_API_KEY");
assert_eq!(api_key_env_var("my-corp"), "MY_CORP_API_KEY");
assert_eq!(api_key_env_var("123corp"), "_123CORP_API_KEY");
}
#[test]
fn resolve_api_key_env_uses_provider_default_for_placeholder() {
assert_eq!(resolve_api_key_env("minimax", defaults::DEFAULT_API_KEY_ENV), "MINIMAX_API_KEY");
}
#[test]
fn resolve_api_key_env_preserves_explicit_override() {
assert_eq!(resolve_api_key_env("openai", "CUSTOM_OPENAI_KEY"), "CUSTOM_OPENAI_KEY");
}
#[test]
fn credential_metadata_key_normalizes_provider_and_key() {
assert_eq!(
credential_metadata_key(" MyCorp ", "mycorp_billing_key").expect("metadata key"),
Some("mycorp/MYCORP_BILLING_KEY".to_string())
);
}
#[test]
fn resolver_prefers_process_environment_over_workspace_dotenv() {
let workspace = tempdir().expect("workspace");
std::fs::write(workspace.path().join(".env"), "MYCORP_API_KEY=workspace-key\n").expect("write dotenv");
with_override("MYCORP_API_KEY", Some("process-key"), || {
let resolved = resolve_credential_with_mode(
"mycorp",
"MYCORP_API_KEY",
Some(workspace.path()),
crate::auth::AuthCredentialsStoreMode::File,
)
.expect("resolve credential")
.expect("credential");
assert_eq!(resolved.secret.as_deref(), Some("process-key"));
assert_eq!(resolved.source, CredentialSource::Env);
assert_eq!(resolved.identity.provider(), "mycorp");
assert_eq!(resolved.identity.key_name(), "MYCORP_API_KEY");
});
}
#[test]
fn resolver_prefers_alternate_process_environment_over_primary_workspace_dotenv() {
let workspace = tempdir().expect("workspace");
std::fs::write(workspace.path().join(".env"), "GEMINI_API_KEY=workspace-key\n").expect("write dotenv");
with_overrides(&[("GEMINI_API_KEY", None), ("GOOGLE_API_KEY", Some("process-key"))], || {
let resolved = resolve_credential_with_mode(
"gemini",
"GEMINI_API_KEY",
Some(workspace.path()),
crate::auth::AuthCredentialsStoreMode::File,
)
.expect("resolve credential")
.expect("credential");
assert_eq!(resolved.secret.as_deref(), Some("process-key"));
assert_eq!(resolved.source, CredentialSource::Env);
assert_eq!(resolved.env_var.as_deref(), Some("GOOGLE_API_KEY"));
});
}
#[test]
fn resolver_reads_workspace_dotenv_for_custom_provider_key() {
let workspace = tempdir().expect("workspace");
std::fs::write(workspace.path().join(".env"), "MYCORP_BILLING_KEY=workspace-key\n").expect("write dotenv");
with_override("MYCORP_BILLING_KEY", None, || {
let resolved = resolve_credential_with_mode(
"mycorp",
"mycorp_billing_key",
Some(workspace.path()),
crate::auth::AuthCredentialsStoreMode::File,
)
.expect("resolve credential")
.expect("credential");
assert_eq!(resolved.secret.as_deref(), Some("workspace-key"));
assert_eq!(resolved.source, CredentialSource::Workspace);
assert_eq!(resolved.env_var.as_deref(), Some("MYCORP_BILLING_KEY"));
});
}
#[test]
fn resolver_does_not_reuse_legacy_storage_for_non_default_key() {
with_override("MIMO_TOKEN_PLAN_KEY", None, || {
let resolved = resolve_credential_with_mode(
"mimo",
"MIMO_TOKEN_PLAN_KEY",
None,
crate::auth::AuthCredentialsStoreMode::File,
)
.expect("resolve credential");
assert!(resolved.is_none());
});
}
#[test]
fn local_providers_are_always_discovered() {
with_overrides(
&[
("OLLAMA_API_KEY", None),
("LMSTUDIO_API_KEY", None),
("LLAMACPP_API_KEY", None),
],
|| {
assert_eq!(provider_credential_source(Provider::Ollama), Some(CredentialSource::Local));
assert_eq!(provider_credential_source(Provider::LmStudio), Some(CredentialSource::Local));
assert_eq!(provider_credential_source(Provider::LlamaCpp), Some(CredentialSource::Local));
},
);
}
#[test]
fn copilot_is_managed_auth_discovered() {
assert_eq!(provider_credential_source(Provider::Copilot), Some(CredentialSource::ManagedAuth));
}
#[test]
fn env_var_makes_provider_discovered() {
with_override("OPENROUTER_API_KEY", Some("or-test-key"), || {
assert_eq!(provider_credential_source(Provider::OpenRouter), Some(CredentialSource::Env));
});
}
#[test]
fn missing_env_var_leaves_provider_undiscovered() {
with_override("OPENROUTER_API_KEY", None, || {
assert_eq!(provider_credential_source(Provider::OpenRouter), None);
});
}
#[test]
fn gemini_alt_env_var_is_discovered() {
with_overrides(&[("GEMINI_API_KEY", None), ("GOOGLE_API_KEY", Some("g-key"))], || {
assert_eq!(provider_credential_source(Provider::Gemini), Some(CredentialSource::Env));
});
}
#[test]
fn qwen_alt_env_var_is_discovered() {
with_overrides(&[("QWEN_API_KEY", None), ("DASHSCOPE_API_KEY", Some("ds-key"))], || {
assert_eq!(provider_credential_source(Provider::Qwen), Some(CredentialSource::Env));
});
}
#[test]
fn credential_detail_surfaces_primary_env_var_name() {
with_override("OPENROUTER_API_KEY", Some("or-key"), || {
let detail = provider_credential_detail(Provider::OpenRouter).expect("OpenRouter discovered");
assert_eq!(detail.source, CredentialSource::Env);
assert_eq!(detail.env_var, Some("OPENROUTER_API_KEY"));
});
}
#[test]
fn credential_detail_surfaces_alternate_env_var_name() {
with_overrides(&[("GEMINI_API_KEY", None), ("GOOGLE_API_KEY", Some("g-key"))], || {
let detail = provider_credential_detail(Provider::Gemini).expect("Gemini discovered");
assert_eq!(detail.source, CredentialSource::Env);
assert_eq!(detail.env_var, Some("GOOGLE_API_KEY"));
});
}
#[test]
fn credential_detail_env_var_is_none_for_non_env_sources() {
assert_eq!(
provider_credential_detail(Provider::Ollama).map(|d| d.env_var),
Some(None),
"local providers must report env_var = None"
);
assert_eq!(
provider_credential_detail(Provider::Copilot).map(|d| d.env_var),
Some(None),
"managed-auth providers must report env_var = None"
);
}
#[test]
fn credential_detail_returns_none_when_no_credential() {
with_overrides(
&[
("OPENROUTER_API_KEY", None),
("OPENAI_API_KEY", None),
("ANTHROPIC_API_KEY", None),
],
|| {
assert!(provider_credential_detail(Provider::OpenRouter).is_none());
},
);
}
#[test]
fn discover_available_providers_carries_env_var_detail() {
with_overrides(
&[
("OPENROUTER_API_KEY", Some("or-key")),
("GEMINI_API_KEY", None),
("GOOGLE_API_KEY", Some("g-key")),
("OPENAI_API_KEY", None),
("ANTHROPIC_API_KEY", None),
],
|| {
let discovered = discover_available_providers();
let or = find_discovered(&discovered, Provider::OpenRouter).unwrap();
assert_eq!(or.source, CredentialSource::Env);
assert_eq!(or.env_var, Some("OPENROUTER_API_KEY"));
let gemini = find_discovered(&discovered, Provider::Gemini).unwrap();
assert_eq!(gemini.source, CredentialSource::Env);
assert_eq!(gemini.env_var, Some("GOOGLE_API_KEY"));
},
);
}
#[test]
fn discover_available_providers_includes_ready_providers() {
with_overrides(
&[
("OPENROUTER_API_KEY", Some("or-key")),
("OPENAI_API_KEY", None),
("ANTHROPIC_API_KEY", None),
("GEMINI_API_KEY", None),
],
|| {
let discovered = discover_available_providers();
let providers: Vec<Provider> = discovered.iter().map(|d| d.provider).collect();
assert!(providers.contains(&Provider::OpenRouter), "OpenRouter should be discovered");
assert!(providers.contains(&Provider::Ollama), "Ollama should be discovered (local)");
assert!(providers.contains(&Provider::Copilot), "Copilot should be discovered (managed auth)");
assert!(
!providers.contains(&Provider::OpenAI),
"OpenAI should NOT be discovered when OPENAI_API_KEY is unset"
);
let or = find_discovered(&discovered, Provider::OpenRouter).unwrap();
assert_eq!(or.source, CredentialSource::Env);
},
);
}
#[test]
fn credential_source_describes_origin() {
assert_eq!(CredentialSource::Env.describe(Provider::OpenRouter), "found in environment");
assert_eq!(CredentialSource::Local.describe(Provider::Ollama), "local — no key required");
}
#[test]
fn get_api_key_trims_non_empty_environment_values() {
with_override("STEPFUN_API_KEY", Some(" test-stepfun-key "), || {
let result =
get_api_key_with_mode("stepfun", &default_sources(), crate::auth::AuthCredentialsStoreMode::File);
assert_eq!(result.unwrap(), "test-stepfun-key");
});
}
}