Validate API keys immediately during arc install

Instead of deferring key validation to the optional `arc doctor` step,
each API key is now validated right after entry with a retry loop if
invalid.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-10 17:12:08 -04:00
parent 2ddb8fdae3
commit 92f40e59fb
2 changed files with 71 additions and 16 deletions

View file

@ -926,7 +926,7 @@ async fn probe_docker() -> Option<Result<(), String>> {
Some(docker.ping().await.map(|_| ()).map_err(|e| e.to_string()))
}
fn cheapest_model(provider: Provider) -> String {
pub(crate) fn cheapest_model(provider: Provider) -> String {
let models = arc_llm::catalog::list_models(Some(provider.as_str()));
models
.iter()

View file

@ -597,13 +597,8 @@ pub async fn run_install() -> Result<()> {
let first_provider = primary_providers[primary_idx];
{
let env_var = first_provider.api_key_env_vars()[0];
let url = provider_key_url(first_provider);
eprintln!(" Get your API key at: {url}");
let prompt = env_var.to_string();
let key: String = tokio::task::spawn_blocking(move || prompt_password(&prompt)).await??;
env_pairs.push((env_var.to_string(), key));
let (env_var, key) = prompt_and_validate_key(first_provider).await?;
env_pairs.push((env_var, key));
configured_providers.push(first_provider);
}
@ -635,14 +630,8 @@ pub async fn run_install() -> Result<()> {
for idx in selected_indices {
let provider = remaining_providers[idx];
let env_var = provider.api_key_env_vars()[0];
let url = provider_key_url(provider);
eprintln!(" Get your API key at: {url}");
let prompt = env_var.to_string();
let key: String =
tokio::task::spawn_blocking(move || prompt_password(&prompt)).await??;
env_pairs.push((env_var.to_string(), key));
let (env_var, key) = prompt_and_validate_key(provider).await?;
env_pairs.push((env_var, key));
}
}
eprintln!();
@ -799,6 +788,64 @@ mod hex {
// Tests
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// API key validation
// ---------------------------------------------------------------------------
async fn validate_api_key(provider: Provider, api_key: &str) -> Result<(), String> {
// Temporarily set the env var so Client::from_env() picks it up
let env_var = provider.api_key_env_vars()[0];
std::env::set_var(env_var, api_key);
let client = arc_llm::client::Client::from_env()
.await
.map_err(|e| e.to_string())?;
let params = arc_llm::generate::GenerateParams::new(doctor::cheapest_model(provider))
.provider(provider.as_str())
.prompt("Say OK")
.max_tokens(16)
.client(std::sync::Arc::new(client));
tokio::time::timeout(
std::time::Duration::from_secs(30),
arc_llm::generate::generate(params),
)
.await
.map_err(|_| "timeout (30s)".to_string())?
.map(|_| ())
.map_err(|e| e.to_string())
}
async fn prompt_and_validate_key(provider: Provider) -> Result<(String, String)> {
let env_var = provider.api_key_env_vars()[0];
let url = provider_key_url(provider);
eprintln!(" Get your API key at: {url}");
loop {
let prompt = env_var.to_string();
let key: String = tokio::task::spawn_blocking(move || prompt_password(&prompt)).await??;
eprintln!(" Validating API key...");
match validate_api_key(provider, &key).await {
Ok(()) => {
eprintln!(" [ok] API key is valid");
return Ok((env_var.to_string(), key));
}
Err(e) => {
eprintln!(" [error] API key validation failed: {e}");
let retry = tokio::task::spawn_blocking(|| {
prompt_confirm("Try again with a different key?", true)
})
.await??;
if !retry {
return Ok((env_var.to_string(), key));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -1000,4 +1047,12 @@ mod tests {
assert!(url.starts_with("https://"), "{provider:?} URL: {url}");
}
}
// -- API key validation --
#[tokio::test]
async fn validate_api_key_rejects_invalid_key() {
let result = validate_api_key(Provider::Anthropic, "sk-invalid-key-12345").await;
assert!(result.is_err(), "expected invalid key to be rejected");
}
}