Hmbown/CodeWhale · error

custom provider id cannot be empty

Error message

custom provider id cannot be empty

What it means

For the `Custom` provider, the API key is stored under a per-custom-provider id (`identity.key`); this `ensure!` guard rejects an empty id because there is no table key to write the secret under. Non-custom providers use `provider_config_key` instead and never hit this.

Solutions

  1. Supply a non-empty `identity.key` for the custom provider before saving
  2. Validate custom-provider creation so an id is always assigned
  3. Use a named custom provider (select/created with an id) rather than a bare `Custom` identity
Defensive patterns

Strategy: validation

Validate before calling

if provider == ApiProvider::Custom && identity.key.trim().is_empty() {
    return Err(anyhow::anyhow!("custom provider id required"));
}

Try / catch

if let Err(e) = save_api_key_for_identity(&identity, key) {
    if e.to_string().contains("custom provider id cannot be empty") {
        // re-select or recreate the custom provider with an id
    }
}

Prevention

When it happens

Trigger: Saving an API key with `provider == ApiProvider::Custom` and an `identity.key` that is empty or whitespace-only after trim.

Common situations: A custom provider was constructed programmatically without an id; form state lost the custom-provider selection/key pairing; deserialized identity with a blank key field.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

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

        && identity.key.trim() == ApiProvider::Custom.as_str()
        && identity.persisted_id().is_none();
    if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) {
        return save_api_key(api_key);
    }
    if is_legacy_literal_custom {
        return save_root_api_key_for_secret_slot(api_key, "custom", false);
    }

    let api_key = api_key.trim();
    anyhow::ensure!(!api_key.is_empty(), "Refusing to save an empty API key.");

    let config_path =
        credential_config_path().context("Failed to resolve config path for provider API key.")?;
    ensure_parent_dir(&config_path)?;

    let key_inside = if provider == ApiProvider::Custom {
        let key = identity.key.trim();
        anyhow::ensure!(!key.is_empty(), "custom provider id cannot be empty");
        key
    } else {
        provider_config_key(provider).context("provider api key table")?
    };
    // A legacy, manually-selected Kimi CLI import implicitly routed Moonshot
    // traffic to Kimi Code. Once the user replaces that import with the
    // supported API-key route, persist the endpoint before changing auth_mode
    // so the key is not silently sent to the ordinary Moonshot endpoint.
    // Respect an explicit user-owned endpoint.
    let pin_kimi_code_base_url = provider == ApiProvider::Moonshot
        && route_config
            .provider_config_for(provider)
            .is_some_and(|entry| {
                provider_config_uses_kimi_imported_token(entry)
                    && entry
                        .base_url
                        .as_deref()
                        .is_none_or(|base_url| base_url.trim().is_empty())

View on GitHub (pinned to 73e0f67d83)