Anthropic format
Generates a model reply from the conversation history, with streaming, image input and tool calls. It is compatible with the Anthropic Messages API: the system prompt goes in system, the conversation goes in messages made of content blocks, and the response's content is also an array of content blocks. The official Anthropic SDKs and Anthropic-protocol clients such as Claude Code connect by setting the Base URL to the gateway root, https://api.lyrouter.com. The gateway converts between Messages and the upstream protocol, so besides the Claude family, this endpoint can also call the platform's other chat models.
/v1/messagesAuthentication
Send your API key in a request header, using either of the two forms below:
x-api-key: <API Key>
Authorization: Bearer <API Key>
When both are present, only Authorization is read; Bearer is case-sensitive. The Anthropic SDK's api_key parameter sends x-api-key, and its auth_token parameter sends Authorization: Bearer; Claude Code's ANTHROPIC_AUTH_TOKEN uses the latter. 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.
The Anthropic SDKs and Claude Code append /v1/messages themselves, so do not add /v1 to the Base URL; otherwise the request path becomes /v1/v1/messages and returns 404.
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 |
x-api-key or Authorization |
Yes | LYRouter API key; see Authentication above for the format. Used only for gateway authentication and never forwarded to the upstream |
anthropic-version |
No | Not validated by the gateway. When an Anthropic upstream is called, the channel sets the version, usually 2023-06-01 |
anthropic-beta |
No | Forwarded to the upstream as is. When the request contains a cache marker with "ttl": "1h" and goes to an Anthropic upstream, the gateway sets it to extended-cache-ttl-2025-04-11, replacing the value in the request |
x-gateway-passthrough |
No | LYRouter extension header. require means the request body must be sent to the Anthropic upstream unchanged, and the gateway returns 400 if it cannot. Native passthrough is currently not enabled on the public gateway, so do not send this header unless you need it; see Gateway headers and extensions |
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | No | Model ID; you can also write @vmodel/<name> (virtual model) or lyrouter/auto (Auto routing). When it is omitted, "" or null, the workspace's default model is used; without a default model, the request returns 400. The Anthropic SDKs require it |
max_tokens |
integer | Yes | Output limit. The gateway does not reject a request that omits it: for an Anthropic upstream it fills in 4096, and for an OpenAI-compatible upstream it sends nothing and the upstream default applies. Whether thinking counts toward this limit is up to the upstream; Alibaba Cloud Model Studio, for example, does not count it: with qwen3.7-plus set to 16, output_tokens is 100 and stop_reason is end_turn |
messages |
array | Yes | Each item is {"role": "user" or "assistant", "content": a string or an array of content blocks}; see Content blocks below |
system |
string / array | No | System prompt. Either a string or an array of text blocks (blocks can carry cache_control); multiple blocks are merged into one passage, separated by a blank line |
stream |
boolean | No | When true, the response is returned as an SSE event stream; see Streaming below |
temperature, top_p, top_k |
number | No | Sent to the upstream as is; whether they take effect depends on the model |
stop_sequences |
array | No | Stop sequences. Converted to stop for OpenAI-compatible upstreams |
tools |
array | No | Custom tools {"name", "description", "input_schema"}, which can carry cache_control; see Tool calls below |
tool_choice |
object | No | {"type": "auto"}, {"type": "any"} or {"type": "tool", "name": "..."}. {"type": "none"} and disable_parallel_tool_use are not sent to the upstream; if you do not want the model to call tools, do not pass tools |
thinking |
object | No | {"type": "enabled", "budget_tokens": 2048}, {"type": "adaptive"} or {"type": "disabled"}; for the conversion rules, see Gateway behavior below |
metadata |
object | No | Only user_id is sent to Anthropic upstreams; nothing is sent to OpenAI-compatible upstreams |
service_tier |
string | No | Sent to the upstream as is |
context_management, output_config |
object | No | Sent only to Anthropic upstreams |
Other fields not listed in the table are not forwarded to the upstream.
Content blocks
| Block type | Where it appears | Description |
|---|---|---|
text |
system, user, assistant |
Text; can carry cache_control |
image |
user, inside tool_result |
source is {"type": "base64", "media_type", "data"} or {"type": "url", "url"}. Converted to image_url for OpenAI-compatible upstreams, which requires a model that supports image input; see Multimodal capabilities |
tool_use |
assistant |
A tool call made by the model: id, name, input |
tool_result |
user |
A tool's result: tool_use_id and content (a string, or text / image blocks) |
thinking, redacted_thinking |
assistant history |
Thinking blocks returned in the previous turn; keep signature when you send them back |
Blocks of other types (such as document) are not forwarded to the upstream.
Prompt caching
Add "cache_control": {"type": "ephemeral"} to a system block, a text block in a message or a tool definition (for a 1-hour cache, write {"type": "ephemeral", "ttl": "1h"}) to mark where a cacheable prefix ends:
| Marker location | Sent to an Anthropic-protocol upstream | Sent to an OpenAI-compatible upstream |
|---|---|---|
system block |
Kept; after multiple blocks are merged, the last block with a marker counts | Removed |
text block in a message |
Kept | Removed |
Tool definition in tools |
Kept | Removed |
image block, tool_result block |
Not forwarded | Removed |
The part written to the cache is recorded as "Explicit cache write", and later hits are recorded as "Input (cache hit)". OpenAI-compatible upstreams have only the caching the upstream performs automatically and do not produce "Explicit cache write". For how to place markers and improve hit rates, see Reducing costs and improving cache reuse.
Request examples
Install the SDK first: pip install anthropic or npm install @anthropic-ai/sdk.
curl https://api.lyrouter.com/v1/messages \
-H "x-api-key: $LYROUTER_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model": "MODEL_ID",
"max_tokens": 1024,
"system": "You are a concise assistant.",
"messages": [
{ "role": "user", "content": "Explain what an API gateway is in one sentence." }
]
}'
import os
import anthropic
client = anthropic.Anthropic(
api_key=os.environ["LYROUTER_API_KEY"],
base_url="https://api.lyrouter.com",
)
message = client.messages.create(
model="MODEL_ID",
max_tokens=1024,
system="You are a concise assistant.",
messages=[{"role": "user", "content": "Explain what an API gateway is in one sentence."}],
)
print(message.content[0].text) # content may be empty; see "Response" below
print(message.usage)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.LYROUTER_API_KEY,
baseURL: "https://api.lyrouter.com",
});
const message = await client.messages.create({
model: "MODEL_ID",
max_tokens: 1024,
system: "You are a concise assistant.",
messages: [{ role: "user", content: "Explain what an API gateway is in one sentence." }],
});
console.log(message.content[0].text);
console.log(message.usage);
Replace MODEL_ID with a model ID enabled in your workspace. To use Authorization: Bearer instead, in cURL replace the x-api-key header with -H "Authorization: Bearer $LYROUTER_API_KEY"; in the SDK, replace api_key / apiKey with auth_token / authToken.
Response
A non-streaming request returns a message object; an error returns an error object in the Anthropic format. The 200 example below is a typical response from an Anthropic-protocol upstream; the 400 is a request without model in a workspace that has no default model; the 401 means the API key is invalid; the 429 means the API key has used up its daily token quota (see 429: rate limits and quotas).
{
"id": "msg_01HCDu5LRGeP2o7s2xGmxyZa",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-5-20250929",
"content": [
{
"type": "text",
"text": "An API gateway is the single entry point between clients and backend services, handling authentication, routing, rate limiting and monitoring."
}
],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 294,
"output_tokens": 46,
"cache_read_input_tokens": 1536,
"cache_creation_input_tokens": 212
}
}
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "you must provide a model parameter"
}
}
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "invalid api key"
}
}
{
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "rate limit exceeded on apikey total_tokens/day"
}
}
| Field | Description |
|---|---|
id |
Taken from the identifier the upstream returns. Anthropic upstreams use the msg_ prefix; other upstreams use different formats, so do not rely on the prefix |
model |
The actual model name returned by the upstream, which can differ from the model ID in the request. Corresponds to "Model (actual)" in the logs |
content |
A non-streaming response contains only text and tool_use blocks; to get the thinking content, use streaming. For exceptions, see the notes below the table |
stop_reason |
end_turn, max_tokens or tool_use, converted from the upstream's stop reason. Other upstream stop reasons (such as stop_sequence and refusal) are all returned as end_turn. When a tool is specified with {"type": "tool"}, it may currently be end_turn; see Tool calls below |
stop_sequence |
Currently not present in the response, nor in message_start or message_delta when streaming. Read through the official SDKs, it is None in Python and undefined in Node.js |
usage |
Token usage for this call; see the table below |
contentcan be{}: when the upstream outputs only thinking and is cut off bymax_tokenswhile still thinking, thecontentof a non-streaming response is currently an empty object{}rather than an empty array[](seen on glm-5.3, kimi-k2.7-code and deepseek-v4). Leave enough room inmax_tokens, and when parsing, treat acontentthat is not an array as empty.<think>tags: some models (such as minimax-m3) write their thinking as<think>…</think>insidetextblocks, both in non-streaming and streaming responses. The gateway does not strip it; remove it yourself if needed.
usage and the four token types in logs
usage maps one to one to the four items under "Token usage" in the logs, and the four fields do not overlap:
usage field |
Meaning | Item in the logs |
|---|---|---|
input_tokens |
Input that did not hit the cache, excluding cache reads and cache writes | "Input (cache miss)" |
cache_read_input_tokens |
Input that hit the prompt cache | "Input (cache hit)" |
cache_creation_input_tokens |
Input written to the cache in this call; appears only with Anthropic-protocol upstreams | "Explicit cache write" |
output_tokens |
Output, including thinking content | "Output" |
The two cache fields are omitted when they are 0. When the upstream uses the OpenAI-compatible protocol, the gateway subtracts the cache hits from the upstream's prompt_tokens to get input_tokens, matching Anthropic's definition; the total input for the call is input_tokens + cache_read_input_tokens + cache_creation_input_tokens. For billing, see Estimating costs and cache pricing.
Tool calls
Declare the tool in the request:
{
"model": "MODEL_ID",
"max_tokens": 1024,
"tools": [
{
"name": "get_weather",
"description": "Get the current weather for a city",
"input_schema": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
],
"messages": [{ "role": "user", "content": "What's the weather in Shanghai right now?" }]
}
When the model decides to call the tool, a tool_use block appears in content, and stop_reason is usually tool_use:
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "city": "Shanghai" }
}
To decide whether to run a tool, check whether content contains a tool_use block rather than relying only on stop_reason: currently, when tool_choice is {"type": "tool", "name": "..."}, the response carries a tool_use block but stop_reason is end_turn; with any or auto it is tool_use.
After running the tool, put the assistant turn back into messages unchanged, then return tool_result in a user message, with tool_use_id matching the call's id:
{
"model": "MODEL_ID",
"max_tokens": 1024,
"tools": ["...same as in the first request..."],
"messages": [
{ "role": "user", "content": "What's the weather in Shanghai right now?" },
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "city": "Shanghai" }
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "{\"temp_c\":22,\"condition\":\"cloudy\"}"
}
]
}
]
}
The tools value in the example is abbreviated; a real request must include the full tool definitions. Keep the following in mind:
- Use only custom tools that have
input_schema. Anthropic server tools (such asweb_search) are sent as ordinary custom tools and are not executed on the server. - In a
usermessage that containstool_result, anytextandimageblocks next to it are not forwarded. To add notes, put them in thetool_result'scontentor in the nextusermessage. is_errorintool_resultis not forwarded; when a tool fails, describe the error incontent.
For a comparison with the Chat Completions format, see Tool calls.
Streaming
With "stream": true, the response is text/event-stream, and each event consists of an event: line and a data: line:
event: message_start
data: {"type":"message_start","message":{"id":"msg_01HCDu5LRGeP2o7s2xGmxyZa","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[],"usage":{"input_tokens":294,"output_tokens":0}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"An API gateway is"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":294,"output_tokens":46,"cache_read_input_tokens":1536,"cache_creation_input_tokens":212}}
event: message_stop
data: {"type":"message_stop"}
- Event order:
message_start→ for each content block,content_block_start,content_block_delta,content_block_stop→message_delta→message_stop.message_stopmarks a normal end. - Delta types: text uses
text_delta; tool call arguments useinput_json_delta(thepartial_jsonpieces form complete JSON only after they are joined); thinking content usesthinking_delta, and an Anthropic upstream's thinking block also has one completesignature_deltabeforecontent_block_stop. Thinking content returned by OpenAI-compatible upstreams is likewise output asthinkingblocks, but without a signature. - Usage: the complete
usage, including the cache fields, is inmessage_delta. Theusageinmessage_starthas onlyinput_tokensand anoutput_tokensof 0, and when the upstream is OpenAI-compatible,input_tokensis 0 as well, so rely onmessage_delta. - Errors mid-stream: if the upstream breaks off after output has started, the gateway sends an
event: errorwhose data is{"type": "error", "error": {"type": "api_error", "message": "upstream timed out after the response had started"}}, and nomessage_stopfollows. Error events sent by the upstream itself (such asoverloaded_error) are passed to the client in the same format. Treat a stream withoutmessage_stopas failed; see Timeouts and interrupted streams.
curl -N https://api.lyrouter.com/v1/messages \
-H "x-api-key: $LYROUTER_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"MODEL_ID","max_tokens":1024,"stream":true,"messages":[{"role":"user","content":"Write a two-line poem"}]}'
import os
import anthropic
client = anthropic.Anthropic(
api_key=os.environ["LYROUTER_API_KEY"],
base_url="https://api.lyrouter.com",
)
with client.messages.stream(
model="MODEL_ID",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a two-line poem"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
print("\n", stream.get_final_message().usage)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.LYROUTER_API_KEY,
baseURL: "https://api.lyrouter.com",
});
const stream = client.messages.stream({
model: "MODEL_ID",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a two-line poem" }],
});
stream.on("text", (text) => process.stdout.write(text));
const final = await stream.finalMessage();
console.log("\n", final.usage);
For a comparison of streaming formats across protocols, see Streaming.
Gateway behavior
Internally, the gateway handles all chat requests in the OpenAI Chat Completions format. A /v1/messages request is first converted to the internal format, then sent in the protocol of the selected channel: for an Anthropic-protocol channel it is converted back into a Messages request, and for an OpenAI-compatible channel it becomes a Chat Completions request; responses and streaming events are converted back into the Messages format. This conversion happens even when the upstream is Anthropic, so requests and responses are not forwarded byte for byte.
| Request content | Sent to an Anthropic-protocol upstream | Sent to an OpenAI-compatible upstream |
|---|---|---|
system |
Merged into one passage of text | Converted to a system message |
text, image blocks |
Kept | Converted to text and image_url in content |
tool_use, tool_result |
Kept | Converted to tool_calls and tool messages |
tools, tool_choice |
Kept (except none) |
Converted to function tools; any becomes required, and a named tool becomes a named function |
thinking parameter |
enabled keeps its budget, adaptive is kept, disabled is not sent |
Converted to enable_thinking and thinking_budget; removed for upstreams known not to support them |
| Thinking blocks in the history | Signed blocks are sent back unchanged; if the history contains thinking blocks without signatures, all thinking blocks are removed and thinking is turned off for this turn | Removed |
max_tokens |
Kept; 4096 is filled in when omitted | Kept; not sent when omitted |
stop_sequences |
Kept | Converted to stop |
metadata |
Only user_id is kept |
Removed |
context_management, output_config |
Kept | Removed |
cache_control |
See Prompt caching above | Removed |
- Thinking blocks and switching models: when a conversation switches midway from a model on an OpenAI-compatible upstream to an Anthropic upstream, the thinking blocks in the history have no signatures, so that turn is sent with thinking turned off to keep the upstream from rejecting the request.
- Model name:
modelis replaced with the selected channel's actual model name, and themodelin the response is the name the upstream returns. - Virtual models: when the request has no
system, the gateway fills in the virtual model's system prompt; whensystemis present, nothing is injected; see Virtual models. - Key order: in the request JSON sent to the upstream, object keys are reordered (array order is unchanged) so identical prefixes reliably hit the upstream cache.
- Fallback on failure: when the upstream returns an error or the connection fails before output starts, the gateway switches channels as described in Retries and fallback, and if needed moves on to fallback models and the workspace's default model. Once streaming output has started, the gateway no longer switches channels.
- Response headers: the upstream's
Retry-Afteris forwarded to the client. Responses carryanthropic-ratelimit-*headers: when the upstream returned these headers they are forwarded as is; otherwise, when quotas are configured, they are generated from the gateway quota. Forx-aigw-quota-*, see Gateway headers and extensions.
Common errors
Errors on /v1/messages are always converted to the Anthropic format. message keeps its original text, error.code is no longer included, and error.type is determined by the status code:
{
"type": "error",
"error": {
"type": "permission_error",
"message": "Model is not available in the model plaza"
}
}
| HTTP status code | error.type |
|---|---|
| 400, 402, 422 | invalid_request_error |
| 401 | authentication_error |
| 403 | permission_error |
| 404 | not_found_error |
| 413 | request_too_large |
| 429 | rate_limit_error |
| 503, 529 | overloaded_error |
| Other 5xx | api_error |
| Status code | error.message |
Cause and fix |
|---|---|---|
| 401 | missing credentials / invalid api key |
No API key was sent, or the key is invalid, deleted or expired. Check x-api-key or Authorization; see 401 / 403: authentication and access |
| 400 | you must provide a model parameter |
The request has no model, and the workspace has no default model set either |
| 400 | tool_choice 'required' is incompatible with thinking enabled |
With tool_choice set to any or a specific tool, the upstream rejects the forced call in thinking mode. Use {"type": "auto"} instead, or turn thinking off with thinking: {"type": "disabled"}. On kimi-k2.7-code turning thinking off has no effect and this error is still returned, so use auto; on Alibaba Cloud Model Studio's qwen models turning it off works |
| 403 | Model is not available in the model plaza |
The model is not enabled in the workspace. Claude Code's background tasks often send requests with official model names; complete the model configuration as described in Claude Code |
| 402 | insufficient credit balance |
The account's Credits balance is insufficient; see Unavailable models |
| 429 | rate limit exceeded on <subject> <metric>/<window> |
A quota or budget is used up. The Anthropic SDK retries 429 automatically, but retrying does not help once a quota is used up; see 429: rate limits and quotas |
| 503 | No configured credential can serve this model |
The model has no usable channel or credential in this workspace; see Unavailable models |
| 503 | all routed candidates exhausted |
No channel can serve this request, for example because @vmodel/<name> is misspelled and the virtual model does not exist. error.type is overloaded_error, so the Anthropic SDK retries automatically, but retrying does not help when the name is wrong. Check the name; see Unavailable models |
The full list is in Errors and request identifiers. To tell apart specific causes under the same status code, rely on message, or check "Status explanation" for the request in the request logs. Gateway responses do not include a request ID; when troubleshooting, copy the Request ID from the top of the log details.