Error Format
When something goes wrong, the JSON body you get back depends on which endpoint you called. Bodhi keeps each compat layer's errors in the shape that layer's clients expect, so an Anthropic SDK gets Anthropic-shaped errors and an OpenAI SDK gets OpenAI-shaped errors. There are four envelopes in practice. This page tells you which one you'll see and how to read each.
Quick decision table
| Endpoint prefix | Local-error envelope | Upstream-error envelope (when proxied to a provider) |
|---|---|---|
/v1/chat/completions, /v1/embeddings, /v1/responses |
OpenAI-style | Pass-through (the upstream's own OpenAI-shaped envelope) |
/v1/models, /api/* (Ollama) |
OpenAI-style | n/a |
/anthropic/v1/messages, /v1/messages, /anthropic/v1/models |
Anthropic-style | Pass-through (Anthropic's native envelope) |
/v1beta/* (Gemini) |
Gemini-style | Pass-through (Google's native envelope) |
/bodhi/v1/* |
Bodhi-native | n/a |
/bodhi/v1/apps/mcps/{id}/mcp |
Bodhi-native (gateway-side) or upstream MCP shape | Pass-through (the upstream MCP server's response) |
"Local" errors are produced inside Bodhi — token rejection, alias not found, validation failure, instance disabled. "Upstream" errors come from the remote provider Bodhi proxied your call to. Bodhi never rewraps an upstream error envelope, so SDK-side error handling continues to work.
The Bodhi-native error envelope
Used by every endpoint under /bodhi/v1/* (UI APIs, MCP CRUD, app access, settings, tokens) and by the MCP proxy when the proxy itself rejects you (instance not found, access not granted, body too large).
{
"error": {
"message": "Validation failed: name is required",
"type": "invalid_request_error",
"code": "validation_error",
"params": { "field": "name" },
"param": "{\"field\":\"name\"}"
}
}
The HTTP status code carries the high-level category (4xx client error, 5xx server error). Inside the body:
message— human-readable, safe to surface to end users for 4xx; 5xx messages are sanitized so internal details don't leak.type— error category. The values you can encounter are listed below.code— a stable, machine-readable code for programmatic branching (e.g.alias_not_found,token_invalid). Codes follow the pattern<domain>-<reason>.params— structured key/value context for validation errors (which field failed, what the invalid value was).param— a JSON-encoded form ofparams. This is a superset field so clients that only know the OpenAI shape (whereparamis a single string) can still read it.
This envelope is a strict superset of the OpenAI shape — an OpenAI SDK pointed at a Bodhi-native endpoint will still parse error.message, error.type, error.code, and error.param, ignoring params.
The OpenAI-style error envelope
Used by /v1/chat/completions, /v1/embeddings, /v1/responses, /v1/models, and /api/* (Ollama).
{
"error": {
"message": "Model 'foo' not found.",
"type": "invalid_request_error",
"param": "model=foo",
"code": "alias_not_found"
}
}
This is the same wire format the OpenAI API uses. The official OpenAI SDK and any tool that follows OpenAI's spec will parse it without modification. Differences from the Bodhi-native envelope:
- No
paramsmap —paramis a flat string ofkey=valuepairs. - No 5xx-message sanitization here either — but the messages are kept generic by design.
The Anthropic-style envelope
Used by /anthropic/v1/messages, /v1/messages, and /anthropic/v1/models for local errors. Upstream errors pass through.
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "Field 'model' is required and must be a string."
}
}
Bodhi maps its internal error categories onto Anthropic's: forbidden_error becomes permission_error, internal_server_error becomes api_error, service_unavailable becomes overloaded_error, etc. The mapping is one-way and lossy by design — Anthropic SDKs only know the Anthropic types.
5xx messages are replaced with "internal server error" so internal service details never leak.
The Gemini-style envelope
Used by /v1beta/* for local errors. Upstream errors pass through.
{
"error": {
"code": 400,
"message": "Model 'foo' not found.",
"status": "NOT_FOUND"
}
}
status is the gRPC-style string Google uses (INVALID_ARGUMENT, UNAUTHENTICATED, PERMISSION_DENIED, NOT_FOUND, INTERNAL, UNAVAILABLE). code mirrors the HTTP status. As with the Anthropic envelope, 5xx messages are sanitized.
Common error categories
Across all envelopes, these are the categories you'll see most often. The exact type string differs by envelope (see the per-page docs for the mapping); the underlying meaning is the same.
| Category | When | Typical HTTP status |
|---|---|---|
invalid_request |
Body validation, wrong content type, missing field, malformed model id | 400, 422 |
authentication_error |
No token, expired token, bad token | 401 |
permission_error |
Token is valid but lacks the scope or role for this endpoint | 403 |
not_found_error |
Model alias, MCP instance, user, or other entity doesn't exist | 404 |
rate_limit |
Upstream rate-limited (passed through verbatim from the provider) | 429 |
internal_error |
Bug, DB failure, llama.cpp crash, upstream 5xx | 500 |
overloaded_error / service_unavailable |
Server temporarily can't handle the request | 503 |
Specific stable code values exist for the most common reasons (alias_not_found, token_invalid, validation_error, instance_disabled, etc.) and are documented in the error code reference.
How SDKs surface these
- OpenAI SDK — raises
openai.BadRequestError,openai.AuthenticationError,openai.PermissionDeniedError, etc. Thecodeandtypeare accessible on the exception. - Anthropic SDK — raises
anthropic.BadRequestError,anthropic.APIError, etc. witherror.typeanderror.message. - Google
google-genaiSDK — raises agoogle.genai.errors.APIErrorwithcode,message, andstatus. - Raw
fetch/requests— read the JSON body and branch onerror.type(orerror.error.typefor the Anthropic envelope).
Telling them apart at a glance
If body.error is an object with a message field — it's OpenAI-style or Bodhi-native.
If body.error is an object with code (integer), message, and status (string) — it's Gemini.
If body.type === "error" and body.error is nested — it's Anthropic.
If body.error.params is present — it's specifically the Bodhi-native envelope (the others don't have that field).
Source of truth
Every endpoint's exact response shape — including the schemas of all four envelopes — is in Swagger UI. The local default is http://localhost:1135/swagger-ui.