Hmbown/CodeWhale · error

OAuth bearer credentials are not an API key

Error message

OAuth bearer credentials are not an API key

What it means

resolve_api_key ensures the resolved credential for the provider is an actual API key, not an OAuth bearer token. For non-Cli sources it checks whether the provider authenticates via API keys (xai is exempt from the check) and rejects the handoff when the effective auth mode is OAuth bearer, since bearer tokens cannot be handed off as API keys.

Solutions

  1. Log in to the provider with an API key (set the key via the normal key-entry path) so the runtime slot holds an actual API key.
  2. Select a different provider whose runtime credential is an API key, or override with a CLI-provided key (RuntimeApiKeySource::Cli bypasses this check).
  3. For Moonshot, re-authenticate without the imported Kimi token (auth_mode must not be the kimi imported-token mode).
  4. If you need the bearer token itself, use an auth-mode-aware handoff instead of the API-key handoff.

Example fix

// before
$ codewhale handoff api-key --provider moonshot  # auth_mode = imported kimi token -> refused
// after
$ codewhale auth login --provider moonshot --api-key
$ codewhale handoff api-key --provider moonshot
Defensive patterns

Strategy: validation

Validate before calling

// before requesting an API-key handoff, confirm the provider's auth mode is key-based
let resolved = resolve_provider(provider)?;
if resolved.auth_mode.is_some() {
    eprintln!("provider uses OAuth bearer auth; API-key handoff unavailable");
}

Type guard

fn is_api_key_provider(provider: ProviderKind, store: &SecretStore) -> bool {
    match provider {
        ProviderKind::OpenaiCodex | ProviderKind::Xai => false,
        _ => store.api_key_for(provider).map(|k| !k.trim().is_empty()).unwrap_or(false),
    }
}

Try / catch

match resolve_api_key(provider, store, overrides) {
    Err(e) if e.to_string().contains("are not an API key") => {
        eprintln!("{provider} is authenticated via OAuth; log in with an API key or hand off the bearer token instead");
    }
    Err(e) => return Err(e),
    Ok(k) => Ok(k),
}

Prevention

When it happens

Trigger: Requesting an API key handoff for a provider whose runtime credential came from OAuth login (e.g. OpenAI Codex bearer, or Moonshot with an imported Kimi token), so the store resolves a bearer credential rather than an API key.

Common situations: User authenticated a provider via OAuth flow but then runs the API-key handoff command; a shared secret store holds a bearer token in the provider's slot; xai/non-key providers configured as the runtime provider while an API key is requested.

Understand the failure class

Background: "API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries — this error's family across 16 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/4f3a5abc71864834. Report an issue: GitHub.

Appendix: source

Thrown at crates/cli/src/credential_handoff.rs:44

    secrets: &Secrets,
    provider: ProviderKind,
    runtime_overrides: &CliRuntimeOverrides,
) -> Result<String> {
    let resolved = store.config.resolve_runtime_options_with_secrets(
        &runtime_overrides_for_provider(runtime_overrides, provider),
        secrets,
    );
    if resolved.provider != provider {
        bail!("resolved a different provider");
    }
    let source = resolved.api_key_source;
    if source != Some(RuntimeApiKeySource::Cli) {
        if provider == ProviderKind::OpenaiCodex {
            bail!("bearer credentials are not an API key");
        }
        let uses_api_key = provider != ProviderKind::Xai
            || xai_auth_diagnostics(store, runtime_overrides).evaluates_runtime_api_key();
        ensure!(uses_api_key, "OAuth bearer credentials are not an API key");
        let kimi_bearer = provider == ProviderKind::Moonshot
            && resolved
                .auth_mode
                .as_deref()
                .is_some_and(auth_mode_uses_kimi_imported_token);
        ensure!(!kimi_bearer, "bearer credentials are not an API key");
    }
    ensure!(source.is_some(), "no runtime-effective API key");
    resolved
        .api_key
        .filter(|value| !value.trim().is_empty())
        .context("no usable runtime-effective API key")
}

pub(crate) fn handoff_secret_line(
    writer: &mut impl Write,
    stdout_is_terminal: bool,
    resolve: impl FnOnce() -> Result<String>,

View on GitHub (pinned to 73e0f67d83)