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.

FieldTypeNotes
modelstringRequired. A model id from GET /v1/models. There is no variant suffix
messagesarrayRequired. system, user, assistant and tool turns
modelsarrayFallback model ids, tried in order when the requested model cannot serve the request. At most 4, including model
streambooleanDefault false. With true the answer is server-sent events
stream_optionsobject{ "include_usage": true } adds a final chunk with usage before data: [DONE]
max_tokensintegerOutput cap. max_completion_tokens is the newer name and wins when both are set. The model's own maximum applies when omitted
temperature, top_pnumberSampling controls, passed to models that honour them
stopstring or arrayStop sequences
response_formatobject{ "type": "json_object" } or a JSON schema, on models that support it
toolsarrayFunction definitions: { "type": "function", "function": { "name", "description", "parameters" } }
tool_choicestring or objectnone, auto, required or a named function
reasoning_effortstringlow, medium, high or max on reasoning models. Models lists the values each one takes
prompt_cache_keystringA stable key for the prompt cache and for keeping a session on one cached prefix
userstringYour end-user id, for attribution. safety_identifier is an alias
metadataobjectFlat 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.

cURLbash
curl -sSf https://api.zdr.sh/v1/chat/completions \  -H "Authorization: Bearer $ZDR_API_KEY" \  -H "Content-Type: application/json" \  -d '{"model": "glm-5.3", "messages": [{"role": "user", "content": "Reply with the single word: ready."}]}'

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:

text
Idempotency-Key: 7d6f2a1e-...

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#

json
{"id":"chatcmpl-8f2f0a...","object":"chat.completion","model":"glm-5.3","choices":[{"index":0,"message":{"role":"assistant","content":"Hello.","reasoning_content":"..."},"finish_reason":"stop"}],"usage":{"prompt_tokens":128,"completion_tokens":12,"total_tokens":140,"prompt_tokens_details":{"cached_tokens":64},"completion_tokens_details":{"reasoning_tokens":540}},"ai_generated":true,"ai_notice":"[AI-generated content]"}
FieldTypeNotes
idstringThe completion id, chatcmpl- followed by random characters. It is not the request id: x-request-id is the value support searches for
objectstringAlways chat.completion
createdintegerUnix seconds
modelstringThe model that served the request
choices[].messageobjectcontent, reasoning_content when the model thought, tool_calls when it calls tools
choices[].finish_reasonstringstop, length, tool_calls or content_filter
usageobjectThe token counts below
ai_generatedbooleanAlways true. AI transparency
ai_noticestring[AI-generated content]

usage reports what the request was billed for:

FieldMeaning
prompt_tokensThe whole input, cached and written tokens included
completion_tokensGenerated tokens, reasoning included
total_tokensThe sum
prompt_tokens_details.cached_tokensInput tokens that hit the prompt cache
prompt_tokens_details.cache_write_tokensTokens written to the cache, when the model reports them. They are free
completion_tokens_details.reasoning_tokensTokens 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.

cURLbash
curl -sSfN https://api.zdr.sh/v1/chat/completions \  -H "Authorization: Bearer $ZDR_API_KEY" \  -H "Content-Type: application/json" \  -d '{"model": "deepseek-v4.1-flash", "stream": true, "messages": [{"role": "user", "content": "Count to three."}]}'

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#

HeaderOnMeaning
x-request-idevery responseThe request id; quote it in a support message
x-token-costa request the balance paid forThe cost in EUR
x-ratelimit-*the key has limitslimit, remaining and reset for requests and for tokens
x-token-cachea cache hithit: the stored answer, billed at 10 percent of its price
x-token-noticethe service changed the requestA comma-separated list: a tool left out, a fallback model, a shortened request, a lowered output cap
x-ai-generated, x-ai-modelevery generated answerThe answer is AI-generated, and the model that wrote it
retry-after429 and 503Whole seconds to wait

No other headers are forwarded. Errors has the shape of a non-200 answer and Billing the pricing.