Hmbown/CodeWhale · error · anyhow::Error

api_key cannot be empty string

Error message

api_key cannot be empty string

What it means

Config validation (crates/tui/src/config.rs:4380) rejects api_key when it is present but only whitespace. An empty-string key is almost always a redacted file or a broken template substitution; treating it as 'no key' would produce confusing 401s later, so load fails fast.

Source

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

                "Invalid provider '{provider}': expected {}.",
                ApiProvider::names_hint()
            );
        }
        let active_provider = self.api_provider();
        match validate_kimi_code_api_model_id(
            active_provider,
            &self.deepseek_base_url(),
            &self.default_model(),
        ) {
            Err(error) if error == KIMI_CODE_CLAUDE_ALIAS_GUIDANCE => {
                return Err(SafeConfigDiagnostic::KimiCodeClaudeAlias.into());
            }
            result => result.map_err(anyhow::Error::msg)?,
        }
        if let Some(ref key) = self.api_key
            && key.trim().is_empty()
        {
            anyhow::bail!("api_key cannot be empty string");
        }
        if let Some(features) = &self.features {
            for key in features.entries.keys() {
                if !is_known_feature_key(key) {
                    anyhow::bail!("Unknown feature flag: {key}");
                }
            }
        }
        // Validate the model against the *active provider's* name space, not
        // against DeepSeek's. `canonical_model_id_for_provider` is the
        // equal-treatment resolver: it applies each family's own canonical map
        // (GLM via Z.ai, Kimi, MiniMax, …) and passes unknown ids through, so
        // it rejects only what the provider genuinely cannot serve. Validating
        // with the DeepSeek-only `normalize_model_name` bricked every config
        // whose provider owns a non-DeepSeek family — including ones our own
        // setup wizard writes (`provider = "zai"`, `GLM-5.2`). (#4829)
        if let Some(model) = self.default_text_model.as_deref()
            && !model.trim().eq_ignore_ascii_case("auto")

View on GitHub (pinned to 8880682c63)

Solutions

  1. Set the real key value in the config, or delete the api_key line entirely so env-var auth applies
  2. If a template produced the empty value, fix the substitution and regenerate
  3. Never commit the filled-in file; use the environment or a secret manager

Example fix

# config.toml - before
api_key = ""

# config.toml - after
# option 1: remove the line and export the env var instead
# option 2: real value
api_key = "sk-..."
Defensive patterns

Strategy: validation

Validate before calling

if let Some(k) = &cfg.api_key {
    ensure!(!k.trim().is_empty(), "api_key is blank - remove the line or set a real value");
}

Type guard

fn api_key_usable(cfg: &Config) -> bool {
    cfg.api_key.as_deref().is_none_or(|k| !k.trim().is_empty())
}

Try / catch

// In secret-scrubbing pipelines: delete the key, never blank it
scrubbed.api_key = None; // not Some(String::new())

Prevention

When it happens

Trigger: Secret-redaction tooling blanking the value in a shared config; envsubst/template pipelines that leave the literal empty; committing a config with the key removed by leaving quotes.

Common situations: Dotfile repos where keys are scrubbed before commit; CI injecting keys via templates that failed silently; switching from key-in-config to env-var auth halfway.

Related errors


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