Messages
The Anthropic-compatible chat endpoint.
/v1/messagesThe Anthropic-compatible endpoint. Point the Anthropic SDK or Claude Code at https://demo-api.routerplus.com with your RouterPlus key, and call any chat model in the catalog, GPT models included. The gateway translates requests, streams and errors between provider dialects.
Image models answer on POST /v1/images/generations only. The OpenAI-native endpoint is POST /v1/chat/completions.
POST https://demo-api.routerplus.com/v1/messagesThe gateway accepts a query string. Claude Code sends /v1/messages?beta=true, which routes normally.
Examples
A GPT model over the Anthropic wire:
curl -N https://dev2-api.routerplus.com/v1/messages \
-H "x-api-key: $TM_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"gpt-4o-mini","stream":true,"max_tokens":100,"messages":[{"role":"user","content":"Say hello."}]}'import os
from anthropic import Anthropic
client = Anthropic(base_url="https://demo-api.routerplus.com", api_key=os.environ["TM_API_KEY"])
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=300,
messages=[{"role": "user", "content": "Count to five."}],
) as stream:
for text in stream.text_stream:
print(text, end="")
print("\n", stream.get_final_message().usage) # tokens, cache splits, costimport Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
baseURL: "https://demo-api.routerplus.com",
apiKey: process.env.TM_API_KEY,
});
const stream = client.messages.stream({
model: "claude-haiku-4-5",
max_tokens: 200,
messages: [{ role: "user", content: "Say hello." }],
});
stream.on("text", (t) => process.stdout.write(t));
console.log("\n", (await stream.finalMessage()).usage);The examples use the official anthropic and @anthropic-ai/sdk SDKs with only the base URL changed. The SDK appends /v1/messages itself.
Claude Code works unmodified:
ANTHROPIC_BASE_URL=https://dev2-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY claudeHeaders
| Header | Required | Behavior |
|---|---|---|
x-api-key | one of these two | Your RouterPlus key, tm_vk_.... The Anthropic-native header. |
Authorization | Bearer tm_vk_..., also accepted, same key. | |
content-type | yes | application/json. |
anthropic-version | no | Accepted for SDK compatibility. The gateway sends anthropic-version: 2023-06-01 to Anthropic providers, whatever you send. |
x-tm-auto-cache | no | off turns off the automatic prompt caching that the gateway may add for an agent's conversation on Claude (see Automatic caching on Claude). The response header of the same name says whether it was applied. |
anthropic-beta | no | See below. No effect on other routes. |
On an Anthropic-dialect provider, the gateway forwards the anthropic-beta values that do not change how a token is billed:
- On every Anthropic-dialect provider: the
prompt-caching,token-efficient-tools,fine-grained-tool-streaming,interleaved-thinking,claude-code,oauthandcomputer-useprefixes. - On Anthropic's own API also:
per-turn-control,mid-conversation-output-config,context-management,mid-conversation-tool-changesandinline-tools(what Claude Code needs on the Claude 5 models). - Any other value is dropped and recorded in
x-tm-dropped-paramsasheader:anthropic-beta:<value>, for exampleadvisor-tool(a second model that bills at its own rates).
A missing or invalid key returns 401 in the Anthropic error shape with error_type: "auth". To identify your app, send HTTP-Referer and X-Title. They carry no content.
Request
The body is JSON, at most 10 MB (413 request_too_large above that).
| Parameter | Type | Behavior |
|---|---|---|
model | string, required | A catalog id: any chat model, Claude or not. The rp- names of the Claude models (rp-sonnet-5-5) work here too. An unknown id is a 404. An image model is a 400 that names /v1/images/generations. A deployed endpoint id (tm/<name>-v<n>) is served on POST /v1/chat/completions only, and is a 400 here. |
max_tokens | number | Output cap, from 1 to the model's own maximum (max_output_tokens in the catalog). The maximum is 128,000 on the Claude 5 models, and can be below 32,768 (GPT-4o: 16,384). If the catalog declares no maximum, the value is 32,768. When you omit it, the gateway sets the model's own maximum on the forwarded request, on every provider. It lowers the value when needed so that your input and the output fit the context window. null counts as not set. A value under 1 or above the maximum is a 400. max_tokens: 0, which on Anthropic's API only warms the cache, is not served. |
messages | array, required | Roles user, assistant and system. Content is a string or blocks: text, tool_use (assistant), tool_result (user), thinking / redacted_thinking (assistant). image and document blocks work on a model that takes them (see architecture.input_modalities in GET /v1/models). A block the model does not take is a typed 400 that names it, also inside a tool_result. On Anthropic-dialect routes the messages pass through as sent. See Messages on an OpenAI-dialect route. |
system | string | array | A string or text blocks. |
stream | boolean | SSE streaming. See Streamed response. |
temperature, top_p | number | Passed through. |
stop_sequences | string[] | Becomes OpenAI stop on cross-dialect routes. |
tools | array | {name, description?, input_schema}. Anthropic server tools (computer use, web search, bash, text editor) pass through on Anthropic-dialect routes, and are a typed 400 when an OpenAI-dialect provider would serve the request. Web search is billed at Anthropic's price per search, inside usage.cost. See Pricing & billing. An empty list goes to an OpenAI-dialect provider as no list. |
tool_choice | object | {"type":"auto"}, {"type":"any"}, {"type":"none"}, or {"type":"tool","name":"..."}. disable_parallel_tool_use: true becomes parallel_tool_calls: false on OpenAI-dialect routes. |
thinking | object | Passes through on Anthropic-dialect routes. For OpenAI-dialect routes, see Thinking on an OpenAI-dialect route. |
output_config | object | Passes through on Anthropic-dialect routes. On an OpenAI-dialect route, effort becomes reasoning_effort (the nearest level the provider declares). format with type: "json_schema" becomes a strict json_schema response_format with the whole schema, and only providers that take json_schema serve it. When none does, the request is a typed 400 that names output_config.format. Another format type is a typed 400. |
context_management | object | Context editing. Passes to Anthropic's own API with the context-management beta. Dropped and recorded on Bedrock and on OpenAI-dialect routes. |
top_k, metadata | Pass through on Anthropic-dialect routes. Dropped and recorded on OpenAI-dialect routes. On a house route, metadata.user_id becomes a hash with your organization. See Sessions and prompt caching. | |
cache_control | object | Automatic prompt caching, on the request or on blocks. Passes to Anthropic. On Bedrock, the request-level field becomes a mark on the last block. Dropped and recorded on OpenAI and Azure. A request-level field that Anthropic would refuse is dropped and recorded on every route (see Wire compatibility). On a house route, ttl: "1h" is removed (so the TTL is 5 minutes) and recorded. When you send no cache_control, the gateway may add the request-level field for an agent's conversation. See Automatic caching on Claude. |
session_id | string | A session id, also accepted as the header x-session-id, x-session-affinity or x-claude-code-session-id. It reaches OpenAI or Azure as prompt_cache_key, and Morph as the header x-session-id. Anthropic has no session input. See Sessions and prompt caching. |
mcp_servers, container | A typed 400 on every route, because removing them would change who runs what. | |
provider | object | Routing controls: require_parameters, order, only, ignore, allow_fallbacks and the rest. The gateway reads them and never forwards them. An unknown control is a 400. See Routing policies. |
- Any other top-level key is dropped and recorded on every route, and the
x-tm-dropped-paramsheader names it. The gateway never changes it into something else. - A top-level
api_key,base_urlorconnection_idis a 400. - The gateway never drops content, because the provider would still bill for it. A block the model does not take, or one a translation cannot carry, is a typed 400 that names the field.
- The gateway decides a translation 400 per deployment. When another deployment of the model speaks the Anthropic dialect, the request goes there instead. See Wire compatibility.
Messages on an OpenAI-dialect route
- A
systemmessage inside the conversation applies from its place onward, as on Anthropic's API. On an OpenAI-dialect provider it goes as a system message at the same place, with itsoutput_config.effortasreasoning_effort. - A turn-scoped system message (
clear_at: "next_user_message") is left out once a user message follows it. A tool added or removed inside one that still shows is a typed 400. imageblocks (base64 or URL) and base64 PDFdocumentblocks in user messages are translated.- A document given by URL or as text, an image given by file id, and an image in an assistant message are a typed 400.
Thinking on an OpenAI-dialect route
adaptiveandenabledleave the model's own reasoning on.budget_tokenshas no counterpart, and is dropped and recorded.disabledbecomesreasoning_effort: "none".displaysets how the reasoning comes back. With"omitted"or"updates", it comes back asthinkingblocks with an emptythinkingfield, and the text travels in the block'ssignaturefor the next turn. Otherwise it comes back as readablethinkingblocks.- A route that serves the model with thinking off, or a model that does not reason (Mistral's Codestral, Large 3 and Ministral 3), drops and records
thinking. - See Wire compatibility.
Transcripts with thinking blocks round-trip safely. When a model served over the OpenAI dialect returns reasoning, this endpoint encodes it as thinking blocks: in a stream, and as a leading block in a reply that is not streamed. When you send that transcript back to an OpenAI-dialect provider, the gateway drops assistant thinking/redacted_thinking blocks instead of rejecting them. The gateway's own blocks (unsigned, or with the gateway's signature when thinking.display hid their text) are not named in x-tm-dropped-params, and Morph's and Mistral's routes read them back as the model's reasoning. The gateway also leaves them out of the history an Anthropic-dialect route gets, because Anthropic verifies every thinking signature. A conversation that moved to another provider while Anthropic was unavailable therefore continues on Anthropic. The route that produced your transcript never returns a 400 on it.
Streamed response
With stream: true, you get the Anthropic Messages event sequence, in the same encoding on every provider. On a translated stream the message id is msg_<request-id>, which joins with the x-request-id header and /v1/generation?id=.
event: message_start
data: {"type":"message_start","message":{"id":"msg_1c9a7b2e-...","type":"message","role":"assistant","model":"claude-sonnet-4-5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"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":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":12,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"output_tokens":5,"cost":0.000111}}
event: message_stop
data: {"type":"message_stop"}- Block types:
text(text_delta),thinking(thinking_delta, plussignature_deltafor multi-turn thinking with tools),redacted_thinking, andtool_use(input_json_deltafragments). - Streamed
stop_reasonis one ofend_turn,max_tokens,tool_use,refusal. - Usage fields follow the billing contract.
input_tokensexcludes cache reads and writes, which are incache_read_input_tokensandcache_creation_input_tokens.usage.cost(USD, billed keys only) in the finalmessage_deltais the full request debit, failed-over attempts included, computed with the same math as the ledger.
Read usage from the final message_delta. On same-dialect routes (a Claude model on this endpoint), the gateway relays the stream record for record, so message_start has the provider's real token counts and its own message id. On cross-dialect routes (an OpenAI-dialect provider behind this endpoint), the gateway re-encodes the stream, and the counts in message_start are zeros. The final message_delta has the real numbers on every route.
- When a started stream is silent for 15 s or more, the gateway sends native
pingevents ({"type":"ping"}) so that proxies keep the stream open. - If the serving provider fails after output started, you get one
errorevent inside the 200 stream, and nothing after it. It has the Anthropic-nativetypeplus a stableerror_type. The gateway never restarts the answer on another provider. - The same event ends a stream that reaches the request deadline (450 s from dispatch, or a routing policy's
timeout_ms). - The Anthropic wire has no place for usage outside
message_delta. For a billed stream that fails mid-answer, get the request's cost from/v1/generation?id=.
Non-streamed response
{
"id": "msg_1c9a7b2e-...",
"type": "message",
"role": "assistant",
"model": "gpt-4o-mini",
"content": [{ "type": "text", "text": "Hello!" }],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 12,
"output_tokens": 5,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 0,
"cost": 0.000004
}
}On a translated response, stop_reason is end_turn, max_tokens, tool_use, or refusal. On an Anthropic-dialect provider, the body passes through with its native values (including stop_sequence), plus the added cost.
Errors
Errors use the Anthropic error shape, with the native type string your SDK reads (authentication_error, rate_limit_error, billing_error, not_found_error, ...), plus a stable error_type with the canonical class:
{
"type": "error",
"error": {
"type": "not_found_error",
"message": "model \"claude-9\" is not in the catalog; GET /v1/models lists what this key can serve",
"error_type": "model_unavailable"
},
"request_id": "..."
}| Case | Status and type |
|---|---|
| Out of credits, or a spend-cap breach | 429, native type billing_error, error_type: "insufficient_quota". On a cap breach, the message and the x-tm-cap-reset header give the UTC reset time. |
| A key whose signup email is not verified yet | 403, native type permission_error, error_type: "email_not_verified". |
| An organization that has not added credit | Its keys and the organization as a whole run at 20 RPM. Above that: 429 rate_limit with retry-after. |
| A prompt too long for the model | Anthropic's standard 400 invalid_request_error, prompt is too long: N tokens > M maximum, with error_type: "context_overflow". |
The gateway never relays a provider's raw error body. When a provider rejected the request, the class says so. The message has the provider's own words (up to 1,000 characters, without keys, account ids and host names), and x-tm-upstream-status has its status. Full table: Errors.
Headers:
- Every response carries
x-request-id. - Served requests add
x-tm-providerandx-tm-attempts. - Failures add
x-tm-error-code, andx-tm-error-originwhen a limit, a provider or RouterPlus's infrastructure refused the request. x-tm-upstream-statusappears whenever a provider responded.- The other headers are the same as on POST /v1/chat/completions.
Model discovery
GET https://demo-api.routerplus.com/v1/models returns the catalog. Send an anthropic-version header (the Anthropic SDK does) to get the Anthropic list shape instead of the OpenAI one. That shape lists chat models only. The public directory with prices and context lengths is https://demo.routerplus.com/api/models.json.
See also: POST /v1/chat/completions · Errors · Wire compatibility & billing contract · Migrate in one prompt
POST /v1/messages/count_tokens
An unbilled passthrough to Anthropic-dialect deployments. It takes the same request shape as /v1/messages, and the provider's own tokenizer answers:
curl -s https://dev2-api.routerplus.com/v1/messages/count_tokens \
-H "x-api-key: $TM_API_KEY" \
-H "content-type: application/json" \
-d '{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "hello"}]}'{ "input_tokens": 8 }- The answer is the count only.
- The call creates no ledger attempt and costs nothing. It counts against your key's rate limit and the provider's pool like any request.
- An unknown model is a 404.
- If only OpenAI-dialect deployments (or a Bedrock connection) serve the model, the response is a typed 400. The gateway does not estimate a count.
For agents: /docs/api-messages.md and /llms.txt