fix(onboard): honor provider fallback env keys for model discovery

This commit is contained in:
chumyin 2026-03-01 13:31:40 +00:00 committed by Chum Yin
parent 2630486ca8
commit feabd7e488
2 changed files with 109 additions and 25 deletions

View File

@ -882,6 +882,13 @@ async fn run_quick_setup_with_home(
} else {
let env_var = provider_env_var(&provider_name);
println!(" 1. Set your API key: export {env_var}=\"sk-...\"");
let fallback_env_vars = provider_env_var_fallbacks(&provider_name);
if !fallback_env_vars.is_empty() {
println!(
" Alternate accepted env var(s): {}",
fallback_env_vars.join(", ")
);
}
println!(" 2. Or edit: ~/.zeroclaw/config.toml");
println!(" 3. Chat: zeroclaw agent -m \"Hello!\"");
println!(" 4. Gateway: zeroclaw gateway");
@ -1833,20 +1840,7 @@ fn fetch_live_models_for_provider(
if provider_name == "ollama" && !ollama_remote {
None
} else {
std::env::var(provider_env_var(provider_name))
.ok()
.or_else(|| {
// Anthropic also accepts OAuth setup-tokens via ANTHROPIC_OAUTH_TOKEN
if provider_name == "anthropic" {
std::env::var("ANTHROPIC_OAUTH_TOKEN").ok()
} else if provider_name == "minimax" {
std::env::var("MINIMAX_OAUTH_TOKEN").ok()
} else {
None
}
})
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
resolve_provider_api_key_from_env(provider_name)
}
} else {
Some(api_key.trim().to_string())
@ -3020,10 +3014,19 @@ async fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String,
if key.is_empty() {
let env_var = provider_env_var(provider_name);
print_bullet(&format!(
"Skipped. Set {} or edit config.toml later.",
style(env_var).yellow()
));
let fallback_env_vars = provider_env_var_fallbacks(provider_name);
if fallback_env_vars.is_empty() {
print_bullet(&format!(
"Skipped. Set {} or edit config.toml later.",
style(env_var).yellow()
));
} else {
print_bullet(&format!(
"Skipped. Set {} (fallback: {}) or edit config.toml later.",
style(env_var).yellow(),
style(fallback_env_vars.join(", ")).yellow()
));
}
}
key
@ -3043,13 +3046,7 @@ async fn setup_provider(workspace_dir: &Path) -> Result<(String, String, String,
allows_unauthenticated_model_fetch(provider_name) && !ollama_remote;
let has_api_key = !api_key.trim().is_empty()
|| ((canonical_provider != "ollama" || ollama_remote)
&& std::env::var(provider_env_var(provider_name))
.ok()
.is_some_and(|value| !value.trim().is_empty()))
|| (provider_name == "minimax"
&& std::env::var("MINIMAX_OAUTH_TOKEN")
.ok()
.is_some_and(|value| !value.trim().is_empty()));
&& provider_has_env_api_key(provider_name));
if canonical_provider == "ollama" && ollama_remote && !has_api_key {
print_bullet(&format!(
@ -3284,6 +3281,33 @@ fn provider_env_var(name: &str) -> &'static str {
}
}
fn provider_env_var_fallbacks(name: &str) -> &'static [&'static str] {
match canonical_provider_name(name) {
"anthropic" => &["ANTHROPIC_OAUTH_TOKEN"],
"gemini" => &["GOOGLE_API_KEY"],
"minimax" => &["MINIMAX_OAUTH_TOKEN"],
"volcengine" => &["DOUBAO_API_KEY"],
"stepfun" => &["STEPFUN_API_KEY"],
"kimi-code" => &["MOONSHOT_API_KEY"],
_ => &[],
}
}
fn resolve_provider_api_key_from_env(provider_name: &str) -> Option<String> {
std::iter::once(provider_env_var(provider_name))
.chain(provider_env_var_fallbacks(provider_name).iter().copied())
.find_map(|env_var| {
std::env::var(env_var)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
})
}
fn provider_has_env_api_key(provider_name: &str) -> bool {
resolve_provider_api_key_from_env(provider_name).is_some()
}
fn provider_supports_keyless_local_usage(provider_name: &str) -> bool {
matches!(
canonical_provider_name(provider_name),
@ -8580,6 +8604,46 @@ mod tests {
assert_eq!(provider_env_var("tencent"), "HUNYUAN_API_KEY"); // alias
}
#[test]
fn provider_env_var_fallbacks_cover_expected_aliases() {
assert_eq!(provider_env_var_fallbacks("stepfun"), &["STEPFUN_API_KEY"]);
assert_eq!(provider_env_var_fallbacks("step"), &["STEPFUN_API_KEY"]);
assert_eq!(provider_env_var_fallbacks("step-ai"), &["STEPFUN_API_KEY"]);
assert_eq!(provider_env_var_fallbacks("step_ai"), &["STEPFUN_API_KEY"]);
assert_eq!(
provider_env_var_fallbacks("anthropic"),
&["ANTHROPIC_OAUTH_TOKEN"]
);
assert_eq!(provider_env_var_fallbacks("gemini"), &["GOOGLE_API_KEY"]);
assert_eq!(provider_env_var_fallbacks("minimax"), &["MINIMAX_OAUTH_TOKEN"]);
assert_eq!(provider_env_var_fallbacks("volcengine"), &["DOUBAO_API_KEY"]);
}
#[tokio::test]
async fn resolve_provider_api_key_from_env_prefers_primary_over_fallback() {
let _env_guard = env_lock().lock().await;
let _primary = EnvVarGuard::set("STEP_API_KEY", "primary-step-key");
let _fallback = EnvVarGuard::set("STEPFUN_API_KEY", "fallback-step-key");
assert_eq!(
resolve_provider_api_key_from_env("stepfun").as_deref(),
Some("primary-step-key")
);
}
#[tokio::test]
async fn resolve_provider_api_key_from_env_uses_stepfun_fallback_key() {
let _env_guard = env_lock().lock().await;
let _unset_primary = EnvVarGuard::unset("STEP_API_KEY");
let _fallback = EnvVarGuard::set("STEPFUN_API_KEY", "fallback-step-key");
assert_eq!(
resolve_provider_api_key_from_env("step-ai").as_deref(),
Some("fallback-step-key")
);
assert!(provider_has_env_api_key("step_ai"));
}
#[test]
fn provider_supports_keyless_local_usage_for_local_providers() {
assert!(provider_supports_keyless_local_usage("ollama"));

View File

@ -2151,6 +2151,26 @@ mod tests {
assert!(resolve_provider_credential("aws-bedrock", None).is_none());
}
#[test]
fn resolve_provider_credential_prefers_step_primary_env_key() {
let _env_lock = env_lock();
let _primary_guard = EnvGuard::set("STEP_API_KEY", Some("step-primary"));
let _fallback_guard = EnvGuard::set("STEPFUN_API_KEY", Some("step-fallback"));
let resolved = resolve_provider_credential("stepfun", None);
assert_eq!(resolved.as_deref(), Some("step-primary"));
}
#[test]
fn resolve_provider_credential_uses_stepfun_fallback_env_key() {
let _env_lock = env_lock();
let _primary_guard = EnvGuard::set("STEP_API_KEY", None);
let _fallback_guard = EnvGuard::set("STEPFUN_API_KEY", Some("step-fallback"));
let resolved = resolve_provider_credential("step-ai", None);
assert_eq!(resolved.as_deref(), Some("step-fallback"));
}
#[test]
fn resolve_qwen_oauth_context_prefers_explicit_override() {
let _env_lock = env_lock();