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
- Read the embedded upstream body - it usually names the exact cause (model not found, quota exceeded, invalid field)
- 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
- Retry transient 429/5xx with exponential backoff
- 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
- Log the full sanitized body - upstream usually names the cause
- Retry only 429/5xx with exponential backoff and jitter; never retry 4xx auth errors
- Keep model ids configurable so deprecations are a config change
- Run a health/chat smoke test on aliases at startup to catch config drift early
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
- {} model list failed at {url}: HTTP {status}
- Gemini API error ({status}): {error_text}
- ACP returned unknown optionId: {option_id}
- WhatsApp API error: {status}
- Embedding API error {status}: {text}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/1673f64aef97c490.
Report an issue: GitHub.