zeroclaw-labs/zeroclaw · error · anyhow::Error

API key prefix mismatch: key "{visible}..." looks like a {li

Error message

API key prefix mismatch: key "{visible}..." looks like a {likely_model_provider} key, but model_provider "{provider_kind}" is selected. Set the correct provider-specific env var or use `-p {likely_model_provider}`.

What it means

The provider factory runs a pre-flight key sniff: when the resolved API key's prefix matches a known provider fingerprint (check_api_key_prefix) that differs from the selected provider_kind, and the provider is neither a custom/anthropic-custom name nor has a custom api_url, construction fails instead of sending a guaranteed-to-be-rejected request. The message shows the first 8 characters of the key and the likely intended provider.

Source

Thrown at crates/zeroclaw-providers/src/lib.rs:1326

            options.vision,
        ));
    }
    let resolved_credential = resolve_model_provider_credential(provider_kind, api_key)
        .map(|v| String::from_utf8(v.into_bytes()).unwrap_or_default());
    #[allow(clippy::option_as_ref_deref)]
    let key = resolved_credential.as_ref().map(String::as_str);

    // Pre-flight: catch obvious API-key / model_provider mismatches early.
    if let Some(key_value) = key {
        let is_custom =
            provider_kind.starts_with("custom:") || provider_kind.starts_with("anthropic-custom:");
        let has_custom_url = api_url.map(str::trim).filter(|u| !u.is_empty()).is_some();
        if !is_custom
            && !has_custom_url
            && let Some(likely_model_provider) = check_api_key_prefix(provider_kind, key_value)
        {
            let visible = &key_value[..key_value.len().min(8)];
            anyhow::bail!(
                "API key prefix mismatch: key \"{visible}...\" looks like a \
                     {likely_model_provider} key, but model_provider \"{provider_kind}\" is selected. \
                     Set the correct provider-specific env var or use `-p {likely_model_provider}`."
            );
        }
    }

    // Resolve the effective endpoint URL for the dispatch arms below.
    // Precedence: `api_url` parameter (operator-set base.uri), then
    // `options.provider_api_url` (pre-resolved family endpoint URI from the
    // typed alias's `*Endpoint::uri()` for multi-endpoint families).
    let resolved_url: Option<&str> =
        api_url
            .map(str::trim)
            .filter(|v| !v.is_empty())
            .or_else(|| {
                options
                    .provider_api_url

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Put the key in the matching provider's env var (e.g. ANTHROPIC_API_KEY for sk-ant- keys) or select the provider with `-p <likely provider>`
  2. If the key genuinely fronts a gateway whose prefix mimics another provider, set `uri`/api_url on the alias so the pre-flight is skipped
  3. Re-copy the key: truncated pastes sometimes produce a misleading prefix

Example fix

# before
export OPENAI_API_KEY=sk-ant-api03-xxxx
zeroclaw -p openai ...

# after
export ANTHROPIC_API_KEY=sk-ant-api03-xxxx
zeroclaw -p anthropic ...
Defensive patterns

Strategy: validation

Validate before calling

fn key_matches_provider(provider_kind: &str, key: &str) -> bool {
    let likely = if key.starts_with("sk-ant-") { "anthropic" }
        else if key.starts_with("sk-or-") { "openrouter" }
        else if key.starts_with("gsk_") { "groq" }
        else { return true };
    provider_kind == likely || provider_kind.contains("custom") || provider_kind.contains(':')
}

Type guard

fn sniff_key_provider(key: &str) -> Option<&'static str> {
    match key {
        k if k.starts_with("sk-ant-") => Some("anthropic"),
        k if k.starts_with("sk-or-") => Some("openrouter"),
        k if k.starts_with("gsk_") => Some("groq"),
        _ => None,
    }
}

Try / catch

if let Some(likely) = sniff_key_provider(&key) {
    if likely != provider_kind {
        return Err(format!("key looks like {likely}; set {likely}_API_KEY or pass -p {likely}"));
    }
}
create_resilient_model_provider(name, Some(&key), None, &reliability).await

Prevention

When it happens

Trigger: provider_kind = openai while the key starts with sk-ant- (Anthropic); `-p openai` combined with an Anthropic key in the generic env var; a groq-style gsk_ key resolved for a different family. Skipped entirely for custom:<url> names or when api_url/uri is set.

Common situations: Copy-pasting the wrong key into the generic or per-provider env var; switching providers with -p while keeping the old env var; CI secret matrices where one job binds key A to provider B.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/16427a1ab1f64bb6. Report an issue: GitHub.