Chat Completions format
Generates a model reply from the conversation history, with streaming and non-streaming responses, image input and tool calls. It is compatible with the OpenAI Chat Completions API: the official OpenAI SDKs and most OpenAI-compatible applications work once you set the Base URL to https://api.lyrouter.com/v1. The same model may be served by a channel that speaks the OpenAI-compatible protocol or the Anthropic protocol; the gateway converts requests and responses automatically, and this page also explains which parameters change during conversion.
/v1/chat/completionsAuthentication
Send your API key in a request header:
Authorization: Bearer <API Key>
Bearer is case-sensitive. You can use x-api-key: <API Key> instead; when both are present, only Authorization is read. Create API keys on the workspace's API Keys page. Your API key is used only to authenticate with LYRouter and is never forwarded to the upstream provider. See Base URLs and authentication.
Headers
| Header | Required | Description |
|---|---|---|
Content-Type |
No | application/json is recommended. Without it, the gateway parses the request body as JSON; any other value (such as application/x-www-form-urlencoded) returns 400 |
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | No | Model ID, such as the system model identifier copied from the model marketplace; you can also use a virtual model @vmodel/<name> or lyrouter/auto. When it is omitted, "" or null, the workspace's Default Model is used; without a Default Model, the request returns 400. See Model identifiers and discovery |
messages |
array | Yes | Conversation messages; see the structure below |
stream |
boolean | No | When true, the response is streamed over SSE. Defaults to false |
stream_options |
object | No | For streaming requests the gateway always sets it to {"include_usage": true}, so the last chunk carries usage; any value you pass is overwritten |
max_tokens |
integer | No | Maximum number of tokens to generate in this call. Anthropic-protocol channels require it; when the request has neither max_tokens nor max_completion_tokens, the gateway adds 4096 |
max_completion_tokens |
integer | No | OpenAI's newer output limit field, passed through to OpenAI-compatible channels; for Anthropic-protocol channels it is used as max_tokens when max_tokens is absent |
temperature |
number | No | Sampling temperature. The valid range depends on the model (0–1 on Anthropic-protocol channels); the gateway does not rescale it |
top_p |
number | No | Nucleus sampling |
stop |
string | array | No | Stop sequences; converted to stop_sequences for Anthropic-protocol channels |
tools |
array | No | Functions the model can call; see Tool calls |
tool_choice |
string | object | No | "auto", "none", "required" or {"type":"function","function":{"name":"..."}}; for how these map across protocols, see Tool calls |
parallel_tool_calls |
boolean | No | Whether several tool calls may be returned at once; removed for Anthropic-protocol channels |
response_format |
object | No | {"type":"json_object"} or {"type":"json_schema",...}; takes effect only on OpenAI-compatible channels and is removed for Anthropic-protocol channels |
reasoning_effort |
string | No | Reasoning effort; the gateway handles it per provider, see "Reasoning parameters" below |
enable_thinking / thinking_budget |
boolean / integer | No | Thinking switch and thinking budget; the gateway handles them per provider, see "Reasoning parameters" below |
seed |
integer | No | Random seed; passed through to OpenAI-compatible channels, and whether it takes effect depends on the model; removed for Anthropic-protocol channels |
user |
string | No | End-user identifier, passed through to the upstream; whether it takes effect depends on the model |
frequency_penalty / presence_penalty / logprobs / n |
— | No | Passed through to OpenAI-compatible channels; whether they take effect depends on the model; removed for Anthropic-protocol channels |
Other parameters not listed in the table are forwarded unchanged to OpenAI-compatible channels; whether they take effect depends on the model.
Structure of messages
Each message has a role and content:
role |
Purpose | Notes |
|---|---|---|
system |
System prompt | Goes first. For Anthropic-protocol channels, all system and developer messages are merged into Anthropic's system; write their content as a string |
developer |
System prompt in newer OpenAI versions | Passed through to OpenAI-compatible channels; support depends on the model. Anthropic-protocol channels treat it as system |
user |
User input | content can be a string or an array of content blocks |
assistant |
The model's earlier replies | Put back unchanged in multi-turn conversations; carries tool_calls when it contains tool calls |
tool |
Tool result | Must carry tool_call_id, matching the call ID in the preceding assistant message |
content can be written in two ways. Write plain text directly as a string; to include images, use an array of content blocks:
{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this image?" },
{ "type": "image_url", "image_url": { "url": "https://example.com/cat.jpg" } }
]
}
image_url.urlcan be a publicly accessiblehttps://URL or Base64 data in the formdata:image/png;base64,<data>.- The model must support image input; text-only models return 400
This model accepts text input only. Use the "Input modalities" filter in the model marketplace to find suitable models; see Multimodal capabilities. - Anthropic-protocol channels convert only text blocks and image blocks; other content blocks such as
input_audioandfilework only on OpenAI-compatible channels.
Reasoning parameters
The three reasoning parameters follow different providers' conventions. The gateway keeps or removes them according to the provider that actually serves the request, and does not convert one into another (requests to Anthropic-protocol channels are converted to thinking, as the table shows):
| Upstream that serves the request | reasoning_effort |
enable_thinking |
thinking_budget |
|---|---|---|---|
| OpenAI official API | Forwarded | Removed | Removed |
| Alibaba Cloud Model Studio (DashScope-compatible API) | Removed | Forwarded | Forwarded |
| DeepSeek official API | Removed | Removed | Removed (whether the model thinks depends on the selected model) |
| Anthropic-protocol channel | Removed | true without a budget becomes thinking: {"type": "adaptive"}; false leaves thinking off |
Greater than 0 becomes thinking: {"type": "enabled", "budget_tokens": N} |
| Other OpenAI-compatible providers | Forwarded unchanged | Forwarded unchanged | Forwarded unchanged |
- The provider that served the request is shown in "Provider" in the request log details. When the same model has several sources, verify for each source whether the reasoning parameters take effect.
- When thinking is on for an Anthropic-protocol channel, the upstream requires
max_tokensto be greater thanbudget_tokens. The gateway does not adjust these parameters; if the requirement is not met, the upstream returns 400. - When a tool call is forced (
tool_choiceis"required"or a specified function) and the request has noenable_thinking/thinking_budget, Alibaba Cloud Model Studio channels automatically getenable_thinking: false, because these upstreams do not accept forced tool calls in thinking mode. - The OpenAI Python SDK does not define
enable_thinkingorthinking_budget; pass them inextra_body, for exampleextra_body={"enable_thinking": False}. In the Node.js SDK, put them directly in the request parameters.
Request examples
curl https://api.lyrouter.com/v1/chat/completions \
-H "Authorization: Bearer $LYROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Introduce LYRouter in one sentence."}
],
"temperature": 0.7,
"max_tokens": 512
}'
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LYROUTER_API_KEY"],
base_url="https://api.lyrouter.com/v1",
)
completion = client.chat.completions.create(
model="MODEL_ID",
messages=[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Introduce LYRouter in one sentence."},
],
temperature=0.7,
max_tokens=512,
)
print(completion.choices[0].message.content)
print(completion.usage)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.LYROUTER_API_KEY,
baseURL: "https://api.lyrouter.com/v1",
});
const completion = await client.chat.completions.create({
model: "MODEL_ID",
messages: [
{ role: "system", content: "You are a concise assistant." },
{ role: "user", content: "Introduce LYRouter in one sentence." },
],
temperature: 0.7,
max_tokens: 512,
});
console.log(completion.choices[0].message.content);
console.log(completion.usage);
Response
A non-streaming request returns a chat.completion object; an error returns an error object. The 200 example below is a typical response from an OpenAI-compatible channel; the 400 is a request without model in a workspace that has no default model; the 429 means the API key has used up its daily token quota (see 429: rate limits and quotas).
{
"id": "chatcmpl-9f1c2a7e4b",
"object": "chat.completion",
"created": 1791950400,
"model": "MODEL_ID",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "LYRouter is a trusted model service and token governance platform that lets you call many model providers with a single API key."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 1250,
"completion_tokens": 32,
"total_tokens": 1282,
"prompt_tokens_details": { "cached_tokens": 1024 },
"completion_tokens_details": { "reasoning_tokens": 0 }
}
}
{
"error": {
"message": "you must provide a model parameter",
"type": "invalid_request_error",
"code": "missing_model"
}
}
{
"error": {
"message": "rate limit exceeded on apikey total_tokens/day",
"type": "rate_limit_error",
"subject": "apikey",
"metric": "total_tokens",
"window": "day",
"limit": 1000000,
"used": 1000523,
"reset_at": "2026-10-14T16:01:00Z"
}
}
| Field | Description |
|---|---|
id |
Response ID generated by the upstream, not LYRouter's Request ID. The gateway does not return the Request ID in the response; copy it from the top of the log details when troubleshooting |
model |
Model name returned by the upstream, which may be written differently from the model ID in the request; the model actually used is the one shown in "Model (actual)" in the logs |
choices[0].message.content |
Answer text; may be null or an empty string when only tool calls are returned |
choices[0].message.tool_calls |
Tools the model asks to call; see Tool calls |
choices[0].message.reasoning_content |
Thinking process returned by some models; it also appears here when an Anthropic-protocol channel serves the request with thinking on |
choices[0].message.reasoning_blocks |
LYRouter extension field that appears only when thinking is on for an Anthropic-protocol channel, holding the original signed thinking blocks. In multi-turn tool calls, put the assistant message back unchanged so the gateway can hand the thinking blocks back to the upstream |
choices[0].finish_reason |
stop (normal end), length (output limit reached), tool_calls (waiting for tool results), content_filter (filtered by the upstream), and so on. For Anthropic-protocol channels, end_turn and stop_sequence map to stop, max_tokens maps to length, and tool_use maps to tool_calls |
usage |
Token usage for this call, reported by the upstream; see the table below |
usage and the four token types in logs
The fields in usage depend on the upstream that served the request. They map to the four types under "Token usage" in the request logs as follows:
| Name in logs | OpenAI-compatible channel | Anthropic-protocol channel |
|---|---|---|
| "Input (cache hit)" | prompt_tokens_details.cached_tokens (prompt_cache_hit_tokens for upstreams such as DeepSeek) |
cache_read_input_tokens |
| "Input (cache miss)" | prompt_tokens minus cache hits |
prompt_tokens |
| "Output" | completion_tokens, including reasoning tokens (completion_tokens_details.reasoning_tokens) |
completion_tokens |
| "Explicit cache write" | Not present | cache_creation_input_tokens |
When an Anthropic-protocol channel serves the request, usage in the response looks like this, and prompt_tokens does not include cache hits or cache writes:
"usage": {
"prompt_tokens": 226,
"completion_tokens": 32,
"total_tokens": 258,
"cache_read_input_tokens": 1024,
"cache_creation_input_tokens": 0
}
If you estimate costs from usage yourself, first confirm which kind of upstream served the request, then price the four types separately; see Estimating costs and cache pricing. When reconciling, the logs are authoritative.
Streaming
With "stream": true, the response Content-Type is text/event-stream, and each event is one data: <JSON> line followed by a blank line:
data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"content":"LYRouter is"},"finish_reason":null}]}
data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{"content":" a trusted model service and token governance platform."},"finish_reason":null}]}
data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-9f1c2a7e4b","object":"chat.completion.chunk","created":1791950400,"model":"MODEL_ID","choices":[],"usage":{"prompt_tokens":1250,"completion_tokens":32,"total_tokens":1282,"prompt_tokens_details":{"cached_tokens":1024}}}
data: [DONE]
- Answer text arrives piece by piece in
choices[0].delta.content; the thinking process is indelta.reasoning_content(delta.reasoningon a few upstreams). - The gateway always turns on
include_usage, so just before the end there is a chunk whosechoicesis an empty array and which carriesusage. Check that the array is not empty before readingchoices[0]. data: [DONE]marks a normal end. If the connection closes before it arrives, or you receive an error event with"code": "stream_interrupted", the output is incomplete; see Timeouts and interrupted streams.- When an Anthropic-protocol channel serves the request, the gateway generates the chunks by conversion, and the chunk with
usagehas only three fields:prompt_tokens,completion_tokensandtotal_tokens. Check cache counts in the logs.
curl -N https://api.lyrouter.com/v1/chat/completions \
-H "Authorization: Bearer $LYROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"MODEL_ID","stream":true,"messages":[{"role":"user","content":"Write a two-line poem about autumn"}]}'
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LYROUTER_API_KEY"],
base_url="https://api.lyrouter.com/v1",
)
stream = client.chat.completions.create(
model="MODEL_ID",
messages=[{"role": "user", "content": "Write a two-line poem about autumn"}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\n", chunk.usage)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.LYROUTER_API_KEY,
baseURL: "https://api.lyrouter.com/v1",
});
const stream = await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: "Write a two-line poem about autumn" }],
stream: true,
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content;
if (text) process.stdout.write(text);
if (chunk.usage) console.log("\n", chunk.usage);
}
For a comparison of event formats across the three chat endpoints and key points for client implementations, see Streaming.
Gateway behavior
Compared with calling the upstream directly, calling through LYRouter differs in these ways:
- Channel selection and fallback: the gateway selects a channel for the model according to the workspace's routing strategy, moves to the next channel on failure, and may finally switch to the workspace's Default Model; see Retries and fallback and Channels and routing. Once streaming output has started, the gateway no longer switches channels.
- Protocol conversion: when the channel uses the Anthropic protocol, the gateway converts the request to Anthropic Messages format and converts the response back to Chat Completions format.
response_format,seed,n,logprobs,top_logprobs,frequency_penalty,presence_penalty,logit_bias,parallel_tool_callsandreasoning_effortare removed, andmetadatakeeps onlyuser_id; these removals are not reported in response headers. For a comparison, see Capabilities and compatibility. - Reasoning parameters: kept, removed or converted according to the table in "Reasoning parameters" above.
- Prompt cache markers:
cache_controltakes effect only on Anthropic-protocol channels, where markers on text content blocks and tool definitions are kept. With"ttl": "1h", the gateway sets theanthropic-betarequest header to the value required for the 1-hour cache (overriding any value the client sent). For OpenAI-compatible channels, all markers are removed and the upstream's implicit caching does the work. - Extension fields in history messages: before sending to OpenAI-compatible channels, the gateway removes
reasoning_content,reasoning_blocksandcache_controlfrom messages. - Fixed JSON key order: the gateway reorders the keys of every JSON object in the request body sent upstream into a fixed order, including JSON inside tool call argument strings, so that the same content always produces the same bytes and the upstream prefix cache hits more often; see Reducing costs and improving cache reuse.
- Number precision: the gateway parses and re-encodes the request body, and numbers keep at most 14 significant digits. Longer integers (such as 19-digit IDs) become scientific notation; pass them as strings.
- Usage: streaming requests always return
usage; the fields inusagevary by upstream, see the mapping table above.
Common errors
Error responses take the form {"error":{"message":"...","type":"...","code":"..."}}; only some errors include code.
| Status code | error.message |
error.code |
Cause and fix |
|---|---|---|---|
| 400 | you must provide a model parameter |
missing_model |
No model was given and the workspace has no Default Model. Specify a model ID. Some versions also return this error when the request body is not a valid JSON object; see the next row |
| 400 | request body must be a JSON object |
invalid_json |
The request body is not a valid JSON object, for example the JSON is truncated or the top level is an array. Some versions return 400 you must provide a model parameter (missing_model) instead. When the workspace has a Default Model, the response is {"message":"could not get parse JSON request body: <parse error>"}, with no outer error |
| 400 | This model accepts text input only |
unsupported_model_input |
Non-text content such as images was sent to a text-only model; use a model that supports image input |
| 400 | This model does not support the chat_completions API |
unsupported_model_api |
The model does not support the chat endpoint, for example an image generation model |
| 403 | Model is not available in the model plaza |
model_not_available |
The workspace has not enabled this model. Ask an administrator to enable it in the model marketplace, or use a model that is enabled |
| 503 | all routed candidates exhausted |
no_route_candidate |
No channel can serve this request, commonly because @vmodel/<name> is misspelled or the model has no available source; see Unavailable models |
- For 401 / 403 authentication issues, see 401 / 403: authentication and access; for 429, see 429: rate limits and quotas.
- 500 / 504 mean the connection to the upstream failed or the wait for the upstream timed out; the response body is an HTML page rather than JSON. See Timeouts and interrupted streams.
- Some 400s generated by the gateway itself are a single line of plain text rather than JSON, for example
unsupported content-type: …whenContent-Typeis notapplication/json. Their response header is currently stillContent-Type: application/json, so check whether the body is JSON before parsing the error. - Errors returned by the upstream provider are forwarded unchanged; the status code and error text come from the upstream. For the full list, see Errors and request identifiers.