sigoden/aichat · error
{error}
Error message
{error} What it means
This error is raised by `catch_error` (src/client/common.rs:528) when an upstream LLM API returns a non-2xx HTTP status whose JSON body contains an `error` field that is a plain string (not an object with type/message). The library surfaces that raw string verbatim as the error message. It is the fallback branch for APIs whose error payloads don't match the structured OpenAI/Anthropic shapes checked earlier.
Solutions
- Read the message text included in the error — it is the provider's own error string; fix what it names first (usually auth or model name).
- Verify the API key is valid and set (config or <CLIENT>_API_KEY env var) with a direct curl to the endpoint.
- Check that api_base points at a real OpenAI-compatible endpoint that returns the expected error shapes.
- If the provider returns structured errors, ensure the client type matches the provider so the structured branches of catch_error parse them instead.
Example fix
// before: hitting wrong endpoint
let client = Client::new("openai", api_base: "https://example.com/v1");
// after: correct endpoint and key
let client = Client::new("openai", api_base: "https://api.openai.com/v1", api_key: Some(key)); Defensive patterns
Strategy: try-catch
Validate before calling
// preflight: verify key/endpoint before calling let models = fetch_models(api_base, Some(&api_key)).await?;
Type guard
fn is_upstream_error_str(err: &anyhow::Error) -> bool {
err.to_string().contains("status:") || !err.to_string().is_empty()
} Try / catch
match client.chat_completions(&req).await {
Err(e) if e.to_string().contains("status: 401") => refresh_api_key_and_retry(),
Err(e) => eprintln!("request failed: {e}"),
Ok(r) => handle(r),
} Prevention
- Validate the API key with a cheap /v1/models call before heavy requests.
- Point api_base at a real OpenAI-compatible endpoint, not a proxy UI page.
- Log the failing response body at debug level to see the provider's error string.
- Use backoff retry only for 429/5xx messages.
When it happens
Trigger: Any call to chat_completions, chat_completions_streaming, embeddings, claude_chat_completions, openai_chat_completions, or openai_embeddings receiving a non-2xx response whose body parses as JSON with `data["error"]` being a JSON string, e.g. `{"error": "Invalid API key"}` with status 400/401/404/429/500.
Common situations: Wrong or expired API key; hitting a non-OpenAI-compatible endpoint; proxies/gateways (e.g. LiteLLM, nginx) returning simple `{"error": "..."}` bodies; deprecated model names rejected server-side; quota exhausted on a custom provider.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09).
Data as JSON: /api/errors/211293869153f2e7.
Report an issue: GitHub.
Appendix: source
Thrown at src/client/common.rs:528
} else if let Some(error) = data["errors"][0].as_object() {
if let (Some(code), Some(message)) = (
error.get("code").and_then(|v| v.as_u64()),
json_str_from_map(error, "message"),
) {
bail!("{message} (status: {code})")
}
} else if let Some(error) = data[0]["error"].as_object() {
if let (Some(status), Some(message)) = (
json_str_from_map(error, "status"),
json_str_from_map(error, "message"),
) {
bail!("{message} (status: {status})")
}
} else if let (Some(detail), Some(status)) = (data["detail"].as_str(), data["status"].as_i64())
{
bail!("{detail} (status: {status})");
} else if let Some(error) = data["error"].as_str() {
bail!("{error}");
} else if let Some(message) = data["message"].as_str() {
bail!("{message}");
}
bail!("Invalid response data: {data} (status: {status})");
}
pub fn json_str_from_map<'a>(
map: &'a serde_json::Map<String, Value>,
field_name: &str,
) -> Option<&'a str> {
map.get(field_name).and_then(|v| v.as_str())
}
async fn set_client_models_config(client_config: &mut Value, client: &str) -> Result<String> {
if let Some(provider) = ALL_PROVIDER_MODELS.iter().find(|v| v.provider == client) {
let models: Vec<String> = provider
.models
.iter()View on GitHub (pinned to 82976d349a)