Send Chat Completion Request

Create an OpenAI-compatible chat completion.

POST/v1/chat/completions

Request Parameters

NameTypeRequiredDescription
model
stringPrimary 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
arrayConversation messages. Each item requires role; content may be string or structured content parts. Required unless input is provided.
input
string | arrayResponses API input. String or array of input items. When provided instead of messages, auto-translated to Chat Completions format internally.
stream
booleanOptional. Defaults to false.
temperature
numberSampling temperature. Typical range: 0-2.
max_tokens
integerMaximum output tokens.
top_p
numberNucleus sampling. Typical range: 0-1.
tools
arrayTool definitions.
tool_choice
object | stringTool selection mode.
response_format
objectOutput 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
objectNormalized 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
booleanInclude token logprobs.
top_logprobs
integerTop logprobs count.
logit_bias
objectToken bias map.
max_completion_tokens
integerMaximum completion tokens.
stream_options
objectStreaming options. Set stream_options.include_usage=true to receive a separate final usage chunk.
stop
string | string[]Stop sequences.
frequency_penalty
numberFrequency penalty.
presence_penalty
numberPresence penalty.
seed
integerDeterministic sampling seed.
user
stringEnd-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):

NameTypeRequiredDescription
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

NameTypeRequiredDescription
models
string[]Model fallback chain. The gateway composes a route and tries models in order.
provider
objectProvider preferences: allow_fallbacks, order, only, ignore.
route
stringRouting strategy: fallback or sort.

Examples (Non-Streaming)

curl
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
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

text
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):

text
: 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:

NameTypeRequiredDescription
client_disconnected_before_dispatch
stringYour client closed the connection before the request was sent upstream. Charge: 0.
client_disconnected_before_first_byte
stringYour client closed the connection before the first output chunk. Charge: prompt only.
client_disconnected_after_first_byte
stringYour client closed the connection mid-stream. Charge: prompt + the chunks written before the close.
upstream_interrupted_after_first_byte
stringThe upstream failed after output had been delivered; you received an error frame. Charge: prompt + the chunks delivered.
intermediary_timeout
stringThe 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

json
{
  "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

NameTypeRequiredDescription
X-Cache
response headerCache status: HIT or MISS.
X-Cache-Model
response headerUpstream model used for cache hit.
x-request-id
response headerRequest trace ID (UUIDv4).
X-RateLimit-Limit
response headerRequest quota window limit.
X-RateLimit-Remaining
response headerRemaining requests in the current window.
X-RateLimit-Reset
response headerUnix epoch reset timestamp.
Retry-After
response headerPresent on 429 responses. Value is seconds.
Model Requirement
Provide either model or models. Requests without both are invalid.
Help & contact