Data-plane routes
Every route the data plane serves, with its authentication, body limits, errors and header handling.
13 min readUpdated for v2.4.0Edit on GitHub
The data plane listens on OBLETH_PROXY_LISTEN (default 0.0.0.0:8080).
Every route except /health needs a credential.
Routes
| Method | Path | Auth | What it does |
|---|---|---|---|
GET | /health | None | Liveness check; returns ok |
POST | /v1/chat/completions | Credential | Chat completions, streamed or not |
POST | /v1/completions | Credential | Legacy text completions |
POST | /v1/embeddings | Credential | Embeddings |
POST | /v1/audio/transcriptions | Credential | Speech to text, multipart |
POST | /v1/audio/translations | Credential | Speech translation, multipart |
POST | /v1/audio/speech | Credential | Text to speech |
GET | /v1/audio/voices | Credential | The voices a text-to-speech model's backend offers (?model=) |
POST | /v1/images/generations | Credential | Image generation |
POST | /v1/images/edits | Credential | Image edits, multipart |
POST | /v1/images/variations | Credential | Image variations, multipart |
POST | /v1/videos | Credential | Creates a video job; JSON or multipart |
GET | /v1/videos | Credential | Lists the caller's video jobs |
GET | /v1/videos/{id} | Credential | Video job status |
GET | /v1/videos/{id}/content | Credential | Downloads the video, streamed |
DELETE | /v1/videos/{id} | Credential | Deletes a video job |
POST | /v1/meshes | Credential | Creates a 3D mesh job; multipart or JSON |
GET | /v1/meshes | Credential | Lists the caller's mesh jobs |
GET | /v1/meshes/{id} | Credential | Mesh job status |
GET | /v1/meshes/{id}/content | Credential | Downloads the model (binary glTF), streamed |
DELETE | /v1/meshes/{id} | Credential | Deletes a mesh job |
POST | /v1/rerank, /v1/reranking | Credential | Forwarded to the named model |
POST | /v1/moderations | Credential | Forwarded to the named model |
POST | /v1/responses | Credential | Responses API, translated to chat |
POST | /v1/messages | Credential | Anthropic Messages API, translated to chat |
POST | /v1/messages/count_tokens | Credential | Gateway token estimate for a Messages body |
POST | /v1/verdicts | Credential | Typed verdicts from a chat model |
POST | /v1/search, /v1/search/{tool} | Credential | Web search through a search model; also at /search |
GET | /v1/search/tools | Credential | The search tools the caller may use; also at /search/tools |
GET | /v1/models, /v1/models/ | Credential | Lists the registry, filtered by the tenant's allowlist |
GET | /v1/models/{id} | Credential | One registry entry; an unclaimed id is forwarded upstream |
GET | /model/info, /v1/model/info | Credential | Registry detail: prices, capabilities, health |
| any | /mcp/{server}, /mcp/{server}/{*rest} | Credential | MCP gateway |
| any | any other path | Credential | Forwarded when the body names a registered model, else 404 |
Path handling
- POST spellings. Repeated slashes collapse, a trailing slash is dropped,
and the bare
/responses,/messages,/messages/count_tokensand/verdictsmap to their/v1forms. The request is then served like the canonical path, with translation, boons and guardrails. - Path traversal. A
..segment, or%2e,%2for%5canywhere in the path, returns400before authentication. - Unknown endpoints. A path that is not a known endpoint and whose body
names no registered model returns
404unknown endpoint. It never reaches an upstream or the usage ledger. - Registered-model paths. Chat, completions, embeddings, responses, the
three audio paths, the three image paths and
POST /v1/videosmust name a registered model inmodel(the form field for multipart). An alias is replaced by the canonical name before admission, budgets, caching, the allowlist and the ledger. A variant is too, with its boons added, its own response-cache entries and its name kept in the ledger'smodel_variant. Only chat takes part inautorouting. - Upstream URL. The path is appended to the endpoint's (or model's)
api_base. Setapi_baseto the base ending in/v1; a doubled/v1is dropped, and a base that already ends with the request path is used as is.
Authentication
The credential comes from Authorization: Bearer <credential>, or from
x-api-key when there is no bearer token. It is an API key or, with identity
tokens configured, a JWT. See Authentication.
| Condition | Status | Message |
|---|---|---|
| No credential | 401 | missing bearer token (missing api key on /v1/messages) |
| Unknown API key | 401 | invalid api key |
| JWT rejected | 401 | invalid token |
| Redis or identity backend unreachable | 503 | authentication backend unavailable |
| Verified JWT that cannot be mapped to a key | 503 | identity provisioning unavailable |
| Key disabled | 403 | api key disabled |
| Tenant not active | 403 | tenant is not active |
| Before the tenant's activation time | 403 | tenant is not active yet |
| After the tenant's expiry | 403 | tenant access has expired |
| Outside the tenant's weekly schedule | 403 | tenant is outside its scheduled access window |
Route behaviour
- Model listing.
GET /v1/modelsanswers from the registry without calling an upstream: every enabled model once, sorted by id, withid,object: "model",owned_by: "obleth",model_type,mode,quantization,tags,aliases, the model's ownsupports_*flags,boonsandboon_details(whether each boon runs, and the helper model that does its work), and each variant as its own entry withvariant_ofanddescription. Staged and retired models andsearchmodels are left out; a deprecated entry addsstatusand its lifecycle fields. A request body is ignored.GET /v1/models/{id}returns the same entry for a name, alias or variant, including a retired model, and404for a staged model or one outside the tenant's allowlist./model/infonever returnsapi_baseorapi_key. See Model registry and Model lifecycle. - Multipart uploads. Audio, image edits and variations, and a video create
with a reference frame are parsed:
modelis swapped for the upstream name, text fields are scanned and billed like JSON (imagen), and file parts are forwarded unscanned. A multipart request gets one attempt, never a retry. See Images, audio and speech. - Video jobs. A create is billed
cost_per_videoonce the job is recorded. Follow-ups go to the endpoint that holds the job, take no fairshare slot or budget, write no ledger row, and pass the upstream status through. By default a key sees only its own jobs. See Video generation. - Mesh jobs. The same as video jobs on
/v1/meshes, billed the model's per-job price as request typemesh. A job is found only through the endpoint that created it. See 3D mesh generation. - Responses API. Body up to 32 MiB.
store: true,background: trueor aprevious_response_idreturns400. See OpenAI-compatible API. - Messages API. Body up to 32 MiB; errors use the Anthropic envelope. An
unknown model is served by the default model for Anthropic clients, else
404.count_tokensreturns{"input_tokens": <n>}and is not admitted, budgeted or recorded. See Claude Code and Anthropic SDKs. - Verdicts. Needs a registered
chatmodel orauto; one fairshare permit and one ledger row cover every question. See Verdicts. - Search. Body up to 64 KiB. Takes a slot in the search tool's pool with no
token estimate, writes one free
searchledger row, and answers in the Perplexity Search API shape. Any other route that names a search tool gets400. See Search API. - Response cache. Checked before admission for a model with caching on and
cache_ttl_secsabove 0; never for video creates, tool loops or output guardrails. See Response cache.
Request pipeline
- Authenticate and gate the key and tenant.
- Read the body (up to 64 MiB), resolve the model, and check its
lifecycle: a retired model is refused with
410or answered by its replacement. - Refuse endpoints the tenant's guardrails
cannot enforce, then run boons and the input guardrails. Guardrails also run
under
x-obleth-boons: off. - Check the response cache.
- Take a slot in the model's pool, waiting up to
OBLETH_ADMISSION_TIMEOUT_SECS(default 60). - Reserve the per-minute and term budgets in Redis.
- Forward with the model's upstream headers and the endpoint's
api_key, retrying and failing over as configured. - Stream the response back, reconcile the budgets and write the usage row.
A request that fails on every attempt is not billed. An upstream 4xx or 5xx
without a usage block records zero tokens. Images bill n × cost_per_image,
speech bills input characters × cost_per_character, and embeddings bill
prompt tokens.
Error responses
Gateway errors on the OpenAI-shaped routes use this body:
{ "error": { "message": "token budget exceeded", "type": "obleth_gateway_error" } }
| Status | Message | When |
|---|---|---|
400 | invalid request path | Path traversal |
400 | invalid JSON body | Unparseable body on Responses, Messages or verdicts |
400 | invalid multipart body, invalid multipart boundary | Malformed form |
400 | model is required | Registered-model path without model |
400 | request blocked by content policy | Input guardrails blocked the request |
400 | token-id prompts cannot be scanned by this tenant's guardrails; send text | Token-id prompt under a block or redact input policy |
400 | this tenant's input guardrails cannot scan requests to this path; use /v1/chat/completions, /v1/responses, or /v1/messages | Unrecognised POST path under a block or redact input policy |
400 | this tenant's output guardrails read chat responses only; use /v1/chat/completions, /v1/responses, or /v1/messages | Completions, transcription, translation or an unrecognised path under a block or redact output policy |
400 | verdicts requires a chat model; … | Verdicts against a non-chat model |
400 | '<name>' is a search tool: call POST /v1/search | A non-search route named a search model |
400 | `<field>` needs server-side response storage, … | Responses request with store, background or previous_response_id |
400 | request body too large or unreadable | Responses body over 32 MiB |
403 | model is disabled | The model is off |
403 | model not permitted for tenant | The tenant's allowlist excludes the model |
403 | api key term budget exhausted, tenant term budget exhausted | Term budget spent |
404 | model '<name>' is not registered | Unknown model name |
404 | unknown endpoint | Unknown path, no registered model |
404 | model '<id>' not found | GET /v1/models/{id} for a staged model or one outside the tenant's allowlist |
410 | The model `<name>` was retired on <date>. Use `<replacement>` instead. | The model is retired; code is model_retired, with replacement and retired_at |
404 | no such video job | Unknown id, or another tenant's or key's job |
413 | request body too large | Body over 64 MiB (32 MiB on Messages) |
429 | token budget exceeded | Per-minute token budget spent |
502 | upstream request failed | Every endpoint and attempt failed |
502 | upstream response too large | A translated response over 32 MiB |
502 | model has no usable upstream endpoint | Verdicts found no endpoint to call |
502 | the upstream did not return a video job id | Create answered without a usable id |
502 | the endpoint that created this video job is no longer configured | Follow-up to a removed endpoint |
502 | could not list video jobs upstream | A backend failed during GET /v1/videos |
503 | timed out waiting for model capacity | No slot within the admission timeout; sends Retry-After: 5 |
503 | no model is available to satisfy the auto request | auto found no eligible model |
503 | scheduler unavailable | Fairshare could not admit |
503 | budget check failed, key budget check failed | Redis failed during budget reservation |
503 | content policy scanner unavailable | A scanner failed and the policy is not fail-open |
503 | the model that created this video job is not available | That model is disabled or gone |
503 | video job lookup failed | The job record could not be read |
503 | could not record the video job; it was cancelled, retry the request | Job created but not recorded |
504 | upstream request timed out | The last attempt hit the upstream timeout |
Upstream errors pass through with the upstream's status and body. For a stream that stops early, see When a stream is cut short.
On /v1/messages, errors use {"type": "error", "error": {"type", "message"}}
with this error.type:
| Status | error.type |
|---|---|
400 | invalid_request_error |
401 | authentication_error |
403 | permission_error |
404 | not_found_error |
413 | request_too_large |
429 | rate_limit_error |
502, 503, 504 | overloaded_error |
| other | api_error |
When a stream is cut short
A stream is cut when, before its last event, the upstream's connection breaks,
the upstream goes silent past OBLETH_UPSTREAM_READ_TIMEOUT_SECS, or a chat or
text completion stream closes with neither [DONE] nor a finish reason. The
client then gets one error event, and the response is aborted, with no finish
chunk, usage chunk or [DONE]:
data: {"error":{"message":"upstream stream ended before the response was complete (after 47 s)","type":"obleth_gateway_error","code":502}}
The seconds count from the start of the upstream call. OpenAI's SDKs raise the
event as an APIError. Every stream ends a cut this way: one passed through
from the upstream, each turn of the tool loop
(including a follow-up or final turn that can't be dispatched), and the
speculation boon's continuation. The translated
Responses stream ends with response.failed instead, and the Messages stream
with an error event, each carrying the message.
A cut is recorded as 502 and billed for what reached the client: the usage
the upstream reported, else the streamed text at about 4 characters per token,
else nothing. It is never billed the max_tokens estimate. A connection the
upstream drops after [DONE] is not a cut.
An error event the upstream sends itself is forwarded once, and the gateway
adds no event of its own. The request is recorded under the error's status
(its code when that is an HTTP status, else 502) and billed the same way.
When the tool loop or the speculation boon runs the stream, [DONE] follows
the error event; otherwise the client gets the rest of the stream as the
upstream sends it.
Headers
Client headers are forwarded upstream except host, content-length,
connection, accept-encoding, authorization, x-api-key,
x-obleth-boons and x-obleth-end-user. The model's
upstream headers are added after
them, so the operator's value wins. On /v1/messages, anthropic-beta and the
beta query parameter are dropped.
| Header | Direction | Purpose |
|---|---|---|
x-obleth-boons | Request | Per-request boon directive: off, lossy |
x-obleth-session-id, x-session-id | Request | Conversation id for the ledger; see Conversations and sessions |
x-obleth-effort | Request | Effort hint for auto routing |
x-obleth-end-user | Request | The end user, for a key with per-user fairshare |
x-obleth-request-id | Response | Request id for request logs |
x-obleth-auto-model | Response | The model an auto request was served by |
x-obleth-auto-reason | Response | Why, when conversations keep their model |
x-obleth-cache: hit | Response | Served from the response cache |
x-obleth-compression | Response | before=<n>;after=<n>;saved=<n> from the compression boon |
x-obleth-boons-warning | Response | A boon hit a non-fatal problem |
x-accel-buffering: no | Response | Tells a buffering reverse proxy to pass tokens through |
x-obleth-model-variant | Response | The variant or model family the request named |
x-obleth-reasoning-effort | Response | The reasoning effort level served, or unsupported when it was removed |
x-obleth-reasoning-effort-requested | Response | The level asked for, when a fallback served another |
x-obleth-drafted-by | Response | The drafter whose answer speculation shipped |
x-obleth-model-status, x-obleth-model-replacement | Response | A deprecated or retired model's status and replacement |
Deprecation, Sunset, Link | Response | RFC 9745 and RFC 8594 dates and the successor-version link of a deprecated or retired model |
x-obleth-redirected-from | Response | The retired model a replacement answered for |
MCP gateway
/mcp/{server} and its sub-paths take the data-plane credential and tenant
gate, replace Authorization with the server's stored auth_header, and
reverse-proxy the bodies unchanged, so streamable HTTP and SSE both work.
Outcomes are counted in
obleth_mcp_requests_total. See
MCP gateway.
| Condition | Status | Message |
|---|---|---|
| Path traversal in the sub-path | 400 | invalid request path |
| Bad credential, key or tenant | 401, 403, 503 | As in Authentication |
| Server disabled | 403 | mcp server is disabled |
| Server not registered | 404 | mcp server '<name>' is not registered |
| Body over 16 MiB | 413 | request body too large |
| Upstream unreachable | 502 | mcp upstream request failed |
