Hmbown/CodeWhale · error · anyhow::Error

Custom provider '{route_name}' API key not found: the route

Error message

Custom provider '{route_name}' API key not found: the route binds api_key_env = "{env_name}" but that environment variable is not set. Set {env_name} to your key, or remove api_key_env from [providers.{route_name}] to run the endpoint without credentials.

What it means

For ApiProvider::Custom routes that bind api_key_env, key resolution fails hard when the named environment variable is unset or empty. This closes #5104: previously a loopback endpoint silently dispatched unauthenticated while the operator believed credentials were wired. The error names the route and env var and offers the two valid escapes (set the var, or drop api_key_env).

Source

Thrown at crates/tui/src/config.rs:6236

        // `[providers.<name>] api_key_env = "..."`. This remains safe for a
        // custom endpoint because the binding belongs to that route; ambient
        // provider variables below do not.
        //
        // For a custom provider, a binding that names an unset (or empty)
        // variable is a broken credential contract, not a keyless route: fail
        // loudly with the route-scoped fix instead of silently degrading to
        // the self-hosted loopback keyless fallback below (#5104). Without
        // this, an `api_key_env` route on a loopback host dispatched
        // unauthenticated while the operator believed credentials were wired,
        // and the composer-side preflight recovery never saw an error.
        if provider == ApiProvider::Custom
            && let Some(env_name) = bound_provider_api_key_env_name(self, provider)
        {
            return match std::env::var(&env_name) {
                Ok(value) if !value.trim().is_empty() => Ok(value),
                _ => {
                    let route_name = self.provider.as_deref().unwrap_or("<name>");
                    Err(anyhow::anyhow!(
                        "Custom provider '{route_name}' API key not found: the route binds \
                         api_key_env = \"{env_name}\" but that environment variable is not set. \
                         Set {env_name} to your key, or remove api_key_env from \
                         [providers.{route_name}] to run the endpoint without credentials."
                    ))
                }
            };
        }
        if let Some(value) = provider_config_env_api_key(self, provider) {
            return Ok(value);
        }

        // 2. The dispatcher resolves this same provider slot before launching
        // the TUI. Standalone `codewhale-tui` launches must see the identical
        // durable credential. Auto-detection is file-backed and prompt-free by
        // default; the OS keyring is queried only when the user explicitly
        // selects the system backend.
        if !self.should_skip_secret_store_for_provider(provider)

View on GitHub (pinned to 8880682c63)

Solutions

  1. Export the variable in the environment that launches codewhale: export MYROUTE_API_KEY=... (prefer ~/.zshenv on zsh so non-interactive shells see it).
  2. Or put the key inline: [providers.myroute] api_key = "...".
  3. Or remove api_key_env from [providers.<route>] if the endpoint is genuinely keyless (e.g. loopback self-hosted).

Example fix

# before
[providers.myroute]
base_url = "https://api.example.com/v1"
api_key_env = "MYROUTE_API_KEY"   # $MYROUTE_API_KEY unset

# after (option 1, shell)
# export MYROUTE_API_KEY=sk-...

# after (option 2, config)
[providers.myroute]
base_url = "https://api.example.com/v1"
api_key = "sk-..."
Defensive patterns

Strategy: validation

Validate before calling

// before resolving the key, verify the bound env var is present and non-empty
if let Some(env_name) = bound_provider_api_key_env_name(&config, ApiProvider::Custom) {
    let set = std::env::var(&env_name).map(|v| !v.trim().is_empty()).unwrap_or(false);
    anyhow::ensure!(set, "route requires {env_name}; export it or drop api_key_env");
}

Type guard

fn custom_route_key_env_is_set(cfg: &Config) -> bool {
    bound_provider_api_key_env_name(cfg, ApiProvider::Custom)
        .map(|n| std::env::var(&n).map(|v| !v.trim().is_empty()).unwrap_or(false))
        .unwrap_or(true) // no binding → nothing to guard
}

Try / catch

match resolve_key(&config) {
    Err(e) if e.to_string().contains("api_key_env") && e.to_string().contains("is not set") => {
        // prompt to export the var, or fall back to keyless loopback by removing api_key_env
        Err(e)
    }
    other => other,
}

Prevention

When it happens

Trigger: [providers.myroute] api_key_env = "MYROUTE_API_KEY" with the variable not exported in the launching shell; CI/daemon/service units that lack the env var; empty-string value (also rejected, since trim-empty fails).

Common situations: Export defined in ~/.zshrc (interactive shells only) instead of ~/.zshenv; secrets configured in one CI job but not the deployed one; renaming the env var in config without updating the environment.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/180ab91c84a75935. Report an issue: GitHub.