sigoden/aichat · error

{message}

Error message

{message}

What it means

Raised by `catch_error` (src/client/common.rs:530) when a non-2xx API response body has a top-level `message` string field but none of the earlier structured shapes (`error` object, `errors[0]`, `data[0].error`, `detail`+`status`) match. The provider's `message` text is surfaced directly to the caller. This is the last structured fallback before the generic 'Invalid response data' error.

Solutions

  1. Act on the provider's message text contained in the error (invalid key, model not found, rate limited, etc.).
  2. Confirm the API key and environment variable (<CLIENT>_API_KEY) are set correctly.
  3. Check the requested model name exists on the provider (run fetch_models or check /v1/models).
  4. If it's a rate-limit message, wait and retry with backoff.

Example fix

// before
let client = create_openai_compatible_client("http://localhost:8080")?;
// after: ensure server is running and model loaded
// curl http://localhost:8080/v1/models first; then retry with a valid model name
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: confirm model exists on provider
let names = fetch_models(api_base, Some(&api_key)).await?;
assert!(names.contains(&model_name));

Type guard

fn is_message_error(err: &anyhow::Error) -> bool {
    // catch_error surfaces provider `message` verbatim
    !err.to_string().is_empty()
}

Try / catch

match client.embeddings(&req).await {
    Err(e) if e.to_string().to_lowercase().contains("rate limit") => tokio::time::sleep(d).await, retry(),
    Err(e) => return Err(e),
    Ok(r) => Ok(r),
}

Prevention

When it happens

Trigger: chat_completions, embeddings, claude/openai variants, or streaming calls hitting an API that reports errors as `{"message": "..."}` (common for Anthropic-style or custom REST providers) with a non-2xx status, without accompanying `status`/`detail` fields.

Common situations: Anthropic or Claude-compatible gateways returning `{"type":"error","message":...}` variants; custom OpenAI-compatible servers (Ollama, LM Studio, vLLM) reporting quota/auth/model errors via `message`; rate-limit responses from proxied endpoints.

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/0b7468fdc91025c3. Report an issue: GitHub.

Appendix: source

Thrown at src/client/common.rs:530

            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()
            .filter(|v| v.model_type == "chat")
            .map(|v| v.name.clone())

View on GitHub (pinned to 82976d349a)