Chat completions
POST /v1/chat/completions creates a model response for a chat conversation in the OpenAI Chat Completions format. Point any OpenAI-compatible client at https://api.zdr.sh/v1 and it works; the answer comes back in the same shape.
Request#
The body is JSON. The fields below are the ones the service reads itself; other OpenAI fields, frequency_penalty for instance, are passed on when the model supports them and ignored when it does not.
| Field | Type | Notes |
|---|---|---|
model | string | Required. A model id from GET /v1/models. There is no variant suffix |
messages | array | Required. system, user, assistant and tool turns |
models | array | Fallback model ids, tried in order when the requested model cannot serve the request. At most 4, including model |
stream | boolean | Default false. With true the answer is server-sent events |
stream_options | object | { "include_usage": true } adds a final chunk with usage before data: [DONE] |
max_tokens | integer | Output cap. max_completion_tokens is the newer name and wins when both are set. The model's own maximum applies when omitted |
temperature, top_p | number | Sampling controls, passed to models that honour them |
stop | string or array | Stop sequences |
response_format | object | { "type": "json_object" } or a JSON schema, on models that support it |
tools | array | Function definitions: { "type": "function", "function": { "name", "description", "parameters" } } |
tool_choice | string or object | none, auto, required or a named function |
reasoning_effort | string | low, medium, high or max on reasoning models. Models lists the values each one takes |
prompt_cache_key | string | A stable key for the prompt cache and for keeping a session on one cached prefix |
user | string | Your end-user id, for attribution. safety_identifier is an alias |
metadata | object | Flat tags that say what the request belongs to, for attribution |
A messages entry has a role and content. User content is a string or an array of parts, {"type": "text"} and {"type": "image_url"} on models that accept images. An assistant turn may carry tool_calls, and a tool turn answers one with tool_call_id.
Reasoning models return their thinking as reasoning_content on the message and on each delta. Send a previous assistant turn back with its reasoning_content when your flow replays it; an empty value is valid. The service replays it where the model needs it, so a tool loop keeps working.
Attribution#
user and metadata tie spend to your own product: an end user, a feature, an environment, your own customer id. Neither is sent to a model. Both are kept on the request's usage record, shown in the dashboard and exported in the CSV.
Keep the values to identifiers and tags. An end-user id is at most 128 characters. A metadata object holds at most 16 entries, with keys of letters, digits, _, - or . up to 64 characters, and string, number or boolean values up to 256 characters. Anything beyond that is dropped.
Idempotency#
Send an Idempotency-Key header with a POST to /v1/chat/completions, /v1/messages or /v1/responses and a retry of the same request is billed once:
A repeat with the same key and body returns the first answer, with Idempotency-Replayed: true and the first x-request-id. Nothing is charged again and nothing is sent to a model again. Keys live for 24 hours from settlement.
- Same key, different body:
400 idempotency_key_reused. - Same key while the first request is still running:
409 idempotency_in_progress. - A response too large to replay, or a stream that did not finish:
409 idempotency_unreplayable. Use a new key. - A request that charged nothing releases its key.
- The key is scoped to the API key that sent it.
Without the header, a retry is a new request with its own charge.
Fallbacks#
When the requested model cannot serve a request, the service tries the fallback models in order and the first one that answers serves it. The list comes from models in the body, from an X-Token-Fallback: a, b header, or from the key's own fallback setting. X-Token-Fallback: none or "models": [] turns the list off for one request.
The answer names the model that served it. You pay the price of that model, and a failed-over answer is never served from the response cache.
Response#
| Field | Type | Notes |
|---|---|---|
id | string | The completion id, chatcmpl- followed by random characters. It is not the request id: x-request-id is the value support searches for |
object | string | Always chat.completion |
created | integer | Unix seconds |
model | string | The model that served the request |
choices[].message | object | content, reasoning_content when the model thought, tool_calls when it calls tools |
choices[].finish_reason | string | stop, length, tool_calls or content_filter |
usage | object | The token counts below |
ai_generated | boolean | Always true. AI transparency |
ai_notice | string | [AI-generated content] |
usage reports what the request was billed for:
| Field | Meaning |
|---|---|
prompt_tokens | The whole input, cached and written tokens included |
completion_tokens | Generated tokens, reasoning included |
total_tokens | The sum |
prompt_tokens_details.cached_tokens | Input tokens that hit the prompt cache |
prompt_tokens_details.cache_write_tokens | Tokens written to the cache, when the model reports them. They are free |
completion_tokens_details.reasoning_tokens | Tokens spent thinking, inside completion_tokens |
When a model does not report usage, the counts are estimated and the request is marked as estimated in the dashboard. The estimate is what is charged.
Streaming#
With stream: true the answer is server-sent events: the role first, then content and reasoning_content deltas, then a final chunk with finish_reason, usage when asked for, and data: [DONE]. Tool calls stream as tool_calls deltas with an index.
While the model is quiet the service writes SSE keepalive comments, invisible to a conforming parser: the first after eight seconds, then every ten seconds until the first token and every fifteen seconds of silence after that. An error after the stream opened arrives as its last event, carrying status, retry_after_ms and x_should_retry.
x-token-cost arrives as an HTTP trailer on a stream, because the price is known at the end. curl -sSfN -D - prints it after the body.
Headers#
| Header | On | Meaning |
|---|---|---|
x-request-id | every response | The request id; quote it in a support message |
x-token-cost | a request the balance paid for | The cost in EUR |
x-ratelimit-* | the key has limits | limit, remaining and reset for requests and for tokens |
x-token-cache | a cache hit | hit: the stored answer, billed at 10 percent of its price |
x-token-notice | the service changed the request | A comma-separated list: a tool left out, a fallback model, a shortened request, a lowered output cap |
x-ai-generated, x-ai-model | every generated answer | The answer is AI-generated, and the model that wrote it |
retry-after | 429 and 503 | Whole seconds to wait |
No other headers are forwarded. Errors has the shape of a non-200 answer and Billing the pricing.