Send Chat Completion Request
Create an OpenAI-compatible chat completion.
Request Parameters
| Name | Type | Required | Description |
|---|---|---|---|
model | string | Primary model alias in brand/model format. Required unless models is provided. | |
models | string[] | Fallback model list. Use when model is omitted, or combine with model for explicit fallback chains. | |
messages | array | Conversation messages. Each item requires role; content may be string or structured content parts. Required unless input is provided. | |
input | string | array | Responses API input. String or array of input items. When provided instead of messages, auto-translated to Chat Completions format internally. | |
stream | boolean | Optional. Defaults to false. | |
temperature | number | Sampling temperature. Typical range: 0-2. | |
max_tokens | integer | Maximum output tokens. | |
top_p | number | Nucleus sampling. Typical range: 0-1. | |
tools | array | Tool definitions. | |
tool_choice | object | string | Tool selection mode. | |
response_format | object | Output format: { "type": "text" } (default), { "type": "json_object" } (a single JSON object), or { "type": "json_schema", "json_schema": { "name", "schema", "strict" } } (schema-constrained). On the Claude models listed in the structured-outputs guide, json_object is instruction-based with validation (other Claude models return 400 unsupported_feature): you get a JSON object or the error code json_object_output_invalid (not charged). Non-streaming failures are HTTP 422; streaming replies arrive in one chunk at the end, and a streaming validation failure is an SSE error event after HTTP 200, followed by [DONE]. Separately, if the upstream stream ends early and retries are exhausted, no buffered content is sent: an SSE error with code "502" arrives after HTTP 200, then [DONE], and the failed request is not charged (a successful retry is billed normally). A length-truncated reply is returned unvalidated only if its visible text starts with "{". More than 20 json_object_output_invalid failures on Claude models in one UTC hour pause json_object requests to Claude models for the rest of that hour (HTTP 429 json_object_invalid_rate_limited). Streamed replies over 2 MiB (visible text, reasoning and tool-call arguments combined) fail with json_object_output_invalid and count toward that limit. See /docs/guides/features/structured-outputs#claude-json-object. | |
reasoning | object | Normalized reasoning controls: { "mode": "off" | "low" | "medium" | "high" | "dynamic", "budget_tokens": integer (optional) }. budget_tokens supersedes the mode-derived budget when present (anthropic/* models cap it at 100000; openai/* models do not accept a token budget and drop it). Honored consistently across all routed models β see "Reasoning across models" below. | |
logprobs | boolean | Include token logprobs. | |
top_logprobs | integer | Top logprobs count. | |
logit_bias | object | Token bias map. | |
max_completion_tokens | integer | Maximum completion tokens. | |
stream_options | object | Streaming options. Set stream_options.include_usage=true to receive a separate final usage chunk. | |
stop | string | string[] | Stop sequences. | |
frequency_penalty | number | Frequency penalty. | |
presence_penalty | number | Presence penalty. | |
seed | integer | Deterministic sampling seed. | |
user | string | End-user identifier for analytics and abuse controls. |
Reasoning across models
The reasoning object is translated to each model family's native reasoning surface, so the same request behaves consistently no matter which model serves it (including fallback):
| Name | Type | Required | Description |
|---|---|---|---|
openai/* models | mode maps to reasoning_effort ("low" | "medium" | "high"); "dynamic" falls back to "medium"; "off" omits the field. budget_tokens is not supported by this family and is dropped. | ||
anthropic/* models | mode maps to a thinking budget: low=1024, medium=4096, high=16384 tokens; "dynamic" uses the medium budget; "off" disables thinking. An explicit budget_tokens overrides the ladder (capped at 100000). On Claude 4.6+ models thinking is adaptive β the model self-selects its reasoning depth and the budget ladder does not apply ("off" still disables). | ||
google/gemini-* models | mode maps to a thinking budget: low=1024, medium=8192, high=32768 tokens; "dynamic" lets the model decide how much to think; "off" disables thinking. An explicit budget_tokens overrides the ladder. |
The response field usage.reasoning_tokens reports reasoning/thinking token usage when the model provides it. An explicit 0 means the model reported zero reasoning tokens; an absent field means the model reported nothing.
Gemini OpenAI-compat base URL
POST /v1beta/openai/chat/completions is an alias of this endpoint with identical schema, authentication, and billing. It exists for clients of Gemini's OpenAI-compatibility layer, whose base-URL convention ends in /v1beta/openai/ β point such clients at https://api.therouter.ai/v1beta/openai and they work unchanged.
OpenRouter Routing Extensions
| Name | Type | Required | Description |
|---|---|---|---|
models | string[] | Model fallback chain. The gateway composes a route and tries models in order. | |
provider | object | Provider preferences: allow_fallbacks, order, only, ignore. | |
route | string | Routing strategy: fallback or sort. |
Examples (Non-Streaming)
curl -X POST https://api.therouter.ai/v1/chat/completions
-H "Authorization: Bearer $THEROUTER_API_KEY"
-H "Content-Type: application/json"
-d '{
"model": "anthropic/claude-sonnet-4.5",
"messages": [{"role": "user", "content": "Write a haiku about routing."}],
"temperature": 0.7,
"max_tokens": 180,
"top_p": 1,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": ["\n\n"],
"seed": 42,
"models": ["anthropic/claude-sonnet-4.5", "openai/gpt-4o"],
"provider": {"allow_fallbacks": true, "order": ["openai-api"]},
"route": "fallback",
"stream": false
}'Examples (Streaming)
curl --no-buffer -X POST https://api.therouter.ai/v1/chat/completions
-H "Authorization: Bearer $THEROUTER_API_KEY"
-H "Content-Type: application/json"
-d '{
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Stream a short answer."}],
"stream": true,
"stream_options": {"include_usage": true}
}'Streaming Behavior
data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":10,"completion_tokens":4,"total_tokens":14}}
data: [DONE]Usage is emitted as a separate final chunk with choices: [] only when stream_options.include_usage is true.
Keepalive frames before the first token
A streaming response is opened as soon as the request has been routed, before the upstream model has produced anything. Until the first chunk arrives the gateway writes an SSE comment line every 15 seconds (the first one immediately):
: keepalive
: keepalive
data: {"id":"chatcmpl_...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}Comment lines are ignored by every SSE parser, including the OpenAI SDKs' stream iterators; no code change is needed. They keep proxies and load balancers from closing an idle connection while a reasoning model is still thinking, so long reasoning turns complete instead of ending in a 5xx from an intermediary. Errors that happen before routing (unknown model, insufficient balance) are still returned as plain HTTP errors; an error after the stream has been opened arrives as a single data: frame with "object": "error" followed by data: [DONE].
Non-streaming requests: 100-second first-byte limit
A non-streaming request receives no bytes until its JSON body is ready. The edge in front of the gateway closes a connection that has produced no byte for 100 seconds and returns 524 to you, whatever the gateway does afterwards. This is an edge limit, not a gateway guarantee: for reasoning models and long prompts use stream: true, where the keepalive removes it. A request cut this way is charged for its prompt only, never for output you did not receive.
What you are charged when a stream is not delivered in full
If your client disconnects, or the edge closes the connection, before the stream completes, the request is charged for the prompt plus the output chunks that were actually written to your connection β nothing the model generated after that. The request log and CSV export show why in delivery_outcome:
| Name | Type | Required | Description |
|---|---|---|---|
client_disconnected_before_dispatch | string | Your client closed the connection before the request was sent upstream. Charge: 0. | |
client_disconnected_before_first_byte | string | Your client closed the connection before the first output chunk. Charge: prompt only. | |
client_disconnected_after_first_byte | string | Your client closed the connection mid-stream. Charge: prompt + the chunks written before the close. | |
upstream_interrupted_after_first_byte | string | The upstream failed after output had been delivered; you received an error frame. Charge: prompt + the chunks delivered. | |
intermediary_timeout | string | The edge closed the connection at the 100-second first-byte limit before any byte was written. Charge: prompt only. |
An empty delivery_outcome means the response was delivered in full and charged as usual.
Error Shape
{
"error": {
"message": "Missing required parameter: model",
"type": "invalid_request_error",
"code": 400,
"param": "model"
}
}Error bodies do not include an error.metadata object. Request IDs are returned via x-request-id response header.
Response Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Cache | response header | Cache status: HIT or MISS. | |
X-Cache-Model | response header | Upstream model used for cache hit. | |
x-request-id | response header | Request trace ID (UUIDv4). | |
X-RateLimit-Limit | response header | Request quota window limit. | |
X-RateLimit-Remaining | response header | Remaining requests in the current window. | |
X-RateLimit-Reset | response header | Unix epoch reset timestamp. | |
Retry-After | response header | Present on 429 responses. Value is seconds. |
model or models. Requests without both are invalid.