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

{} API error ({status}): {sanitized}

Error message

{} API error ({status}): {sanitized}

What it means

chat_with_system on an OpenAI-compatible provider received a non-2xx from the chat completions endpoint. The message includes the provider display name, HTTP status, and the upstream error body after sanitize_api_error scrubs secrets, so the root cause is usually readable in the message itself.

Source

Thrown at crates/zeroclaw-providers/src/compatible.rs:2875

        let response = match self
            .apply_auth_header(
                self.http_client().post(&url).json(&request),
                credential.as_deref(),
            )
            .send()
            .await
        {
            Ok(response) => response,
            Err(chat_error) => {
                return Err(chat_error.into());
            }
        };

        if !response.status().is_success() {
            let status = response.status();
            let error = response.text().await?;
            let sanitized = super::sanitize_api_error(&error);
            anyhow::bail!("{} API error ({status}): {sanitized}", self.name);
        }

        let body = response.text().await?;
        let chat_response = parse_chat_response_body(&self.name, &body)?;

        chat_response
            .choices
            .into_iter()
            .next()
            .map(|c| {
                if c.message.tool_calls.is_some()
                    && c.message
                        .tool_calls
                        .as_ref()
                        .is_some_and(|t: &Vec<_>| !t.is_empty())
                {
                    serde_json::to_string(&c.message)
                        .unwrap_or_else(|_| c.message.effective_content())

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the embedded upstream body - it usually names the exact cause (model not found, quota exceeded, invalid field)
  2. 401/403: fix the API key; 404: fix the model id or base_url; 429: back off or raise limits; 400: drop suspect extra_body fields and retry
  3. Retry transient 429/5xx with exponential backoff
  4. If the provider changed its error shape, upgrade zeroclaw-providers
Defensive patterns

Strategy: retry

Validate before calling

// Validate the cheap stuff before spending a request
if model.trim().is_empty() {
    anyhow::bail!("model id must not be empty");
}
let url = reqwest::Url::parse(&chat_url)?; // catches malformed base_url early

Try / catch

let mut attempt = 0;
loop {
    match provider.chat_with_system(None, prompt, model, None).await {
        Ok(text) => break Ok(text),
        Err(e) => {
            let msg = e.to_string();
            let retryable = msg.contains("HTTP 429") || msg.contains("HTTP 5");
            if retryable && attempt < 3 {
                attempt += 1;
                tokio::time::sleep(std::time::Duration::from_millis(
                    250u64 * (1 << attempt),
                )).await;
                continue;
            }
            break Err(e); // 401/403/400 are terminal: fix config or payload
        }
    }
}

Prevention

When it happens

Trigger: 400 malformed or oversized request or unsupported parameter (extra_body fields); 401/403 invalid key; 404 wrong model id or base_url; 429 quota exhausted; 5xx upstream outage - any non-success on the non-streaming chat path.

Common situations: Model id typo'd or not enabled for the account; rate limits hit during batch jobs; provider deprecating a model; reverse proxies returning HTML error pages that get sanitized into the message.

Related errors


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