Wire compatibility
How RouterPlus maps each API format.
This page defines how the gateway forwards, translates and drops parameters between the OpenAI and Anthropic formats, and how it counts the tokens you are billed for. A change to these rules is a change to your billing contract.
Two surfaces, one catalog
| Surface | Endpoint | Auth |
|---|---|---|
| OpenAI-compatible | POST https://demo-api.routerplus.com/v1/chat/completions | Authorization: Bearer or x-api-key |
| Anthropic-compatible | POST https://demo-api.routerplus.com/v1/messages | Authorization: Bearer or x-api-key |
| OpenAI Responses | POST https://demo-api.routerplus.com/v1/responses | same |
| OpenAI Images | POST https://demo-api.routerplus.com/v1/images/generations | same |
- Every chat model in the catalog answers on both chat surfaces.
- Image models answer on the Images surface only.
- The Responses surface passes OpenAI's models through. Other chat models are a preview there (POST /v1/responses).
GET https://demo-api.routerplus.com/v1/modelslists what your key can serve, in the OpenAI shape, or the Anthropic shape with ananthropic-versionheader.
Each request takes one of two routes:
- Passthrough route: your format matches the provider's dialect. The gateway forwards your dialect's parameters as sent, and adds
stream_options.include_usageto a streamed OpenAI-dialect dispatch. The provider decides what it accepts, including Anthropic prompt caching (cache_control),thinking,top_kand theanthropic-betavalues below. A top-level key outside your dialect's parameter set is dropped and recorded. Session ids and cache marks follow Sessions and prompt caching. - Translated route: the dialects differ. The gateway translates the request, the stream and the errors, by the rules below.
A "typed 400" below is decided per deployment. If another deployment of the model can serve the request (it speaks your dialect, or takes a content part), the request goes there. You get the 400 only when no deployment can. See Content is never silently dropped.
Parameter handling on passthrough routes
Any other top-level key is dropped and recorded.
| Dialect | Forwarded as sent |
|---|---|
| OpenAI → OpenAI-dialect provider | model, messages, stream, stream_options, temperature, top_p, max_tokens, max_completion_tokens, stop, n, tools, tool_choice, parallel_tool_calls, response_format, reasoning_effort, seed, user, logprobs, top_logprobs, frequency_penalty, presence_penalty, logit_bias, metadata, store, prediction |
| Anthropic → Anthropic-dialect provider | model, messages, system, max_tokens, stream, temperature, top_p, top_k, stop_sequences, tools, tool_choice, metadata, thinking, output_config, cache_control, context_management (to Anthropic's own API; on Bedrock it is dropped and recorded) |
The GPT-6 and GPT-5.6 models are translated to OpenAI's Responses API (see GPT-6 and GPT-5.6 on the chat surface).
What an answer from an OpenAI-dialect provider keeps
A chat completion or stream chunk from an OpenAI-dialect provider keeps only OpenAI's fields and the gateway's documented extensions. Other fields a provider adds at the top level, on a choice or in usage (its own cost, token accounting or host name) are dropped.
| Where | Kept |
|---|---|
| Top level | id, object, created, model, choices, usage, system_fingerprint, service_tier, obfuscation, error |
| Each choice | index, message or delta, finish_reason, logprobs, native_finish_reason, error |
usage | prompt_tokens, completion_tokens, total_tokens, prompt_tokens_details (cached_tokens, audio_tokens, cache_write_tokens), completion_tokens_details (reasoning_tokens, audio_tokens, accepted_prediction_tokens, rejected_prediction_tokens), and cost: the gateway's, never the provider's |
- The message and delta pass as written: content, reasoning (
reasoning_content,reasoning,reasoning_details), tool calls and annotations. modelnames the model you asked for. The provider's own id, if different, is in thex-tm-upstream-modelheader. A provider sent your own id can answer with a dated snapshot (gpt-4o-mini-2024-07-18).- Through your own provider key (BYOK), the answer passes as sent.
- A closed dedicated endpoint keeps a smaller set of fields.
Provider-specific keys
- The
reasoningobject ({"enabled": false},{"effort": "low"}) is dropped and recorded. Sendreasoning_effort. DeepInfra takes"none"(its"enabled": false),"low","medium"and"high". prompt_cache_retentionandsafety_identifiergo to OpenAI and Azure only.- A
cache_controlmark on a content part passes unchanged to every OpenAI-dialect provider. OpenAI and Azure ignore it. - On Bedrock, a top-level
cache_controlbecomes a mark on the last block that can carry one. - On every route, a top-level
cache_controlthat Anthropic would refuse is dropped and recorded ascache_control. Anthropic refuses:- a field other than
{"type": "ephemeral"}with an optionalttlof"5m"or"1h" - a fifth mark
- a 1-hour field after a 5-minute mark
- a field whose TTL differs from the mark on the last block
- a field other than
mcp_serversandcontainerare a typed 400 on every route.
The anthropic-beta header
The gateway forwards to Anthropic-dialect providers only the anthropic-beta values that do not change how a token is billed:
- On every Anthropic-dialect provider: the prefixes
prompt-caching,token-efficient-tools,fine-grained-tool-streaming,interleaved-thinking,claude-code,oauthandcomputer-use. - On Anthropic's own API, also these values for Claude Code on the Claude 5 models:
- the prefixes
per-turn-controlandmid-conversation-output-config(effort inside a message) context-management(context editing; compaction's own beta is not forwarded)mid-conversation-tool-changesandinline-tools(tools added during a conversation)thinking-display-updates(readable progress updates)
- the prefixes
Any other value is dropped and recorded as header:anthropic-beta:<value>. Examples: advisor-tool (a second model at its own rates), and the auto-mode values dangerous-tool-use and afk-mode with the safeguards field (their result field is not passed back).
On Anthropic's direct API, Claude Code's thinking: {"type":"adaptive","display":"updates"} goes with its thinking-display-updates-2026-08-18 beta, and progress updates and signatures survive streaming. Bedrock drops thinking.display: "updates" and that beta, and keeps the thinking mode. Both drops are reported, and require_parameters refuses the request.
Parameter handling on translated routes
OpenAI surface → Anthropic-dialect provider
| Parameter | Handling |
|---|---|
model, messages, stream, temperature, top_p | Translated or copied as sent |
max_tokens, max_completion_tokens | Translated. With neither, the gateway sets the model's declared maximum output (32,768 when none is declared), fitted to its context window. See POST /v1/chat/completions |
stop (string or array) | → stop_sequences |
system / developer messages | → the system string, joined with blank lines. If a part has a cache_control mark, system becomes text blocks, and each mark ends a block |
cache_control on a text part | Kept on the Anthropic block. A role:"tool" message's mark goes on its tool_result |
| Other keys of a text part | Not forwarded, not recorded |
cache_control (top-level) | → the Anthropic top-level field (automatic caching). On Bedrock it becomes a mark on the last block |
tools, tool_choice | auto→{type:"auto"}, required→{type:"any"}, {function:{name}}→{type:"tool",name}, none→{type:"none"}. With none and no tools in the history, tools are left out, so you do not pay for them. A tool without parameters gets an empty object schema. The Claude 5 models refuse any and tool (a forced tool choice) with a 400 |
role:"tool" messages | → tool_result blocks. Consecutive results merge into one Anthropic user message |
| Tool-call ids | Kept when valid for Anthropic (letters, digits, _, -, at most 64 characters). Others become toolu_ and a hash of the id, the same in the call, its result and every turn, so pairs match and the cached prompt start is stable. Answers carry the provider's own ids |
image_url parts in user messages | → image blocks. A base64 data URL (JPEG, PNG, GIF or WebP, typed by its bytes) becomes a base64 source. An http(s) URL becomes a URL source that Anthropic fetches. A host that refuses Anthropic's fetcher gives a 400, with no failover. A cache_control mark is kept. A detail other than auto is dropped and recorded. Another image type is a typed 400 naming the part |
file parts in user messages | A PDF sent as a data URL → a document block, with the file's filename as its title. A file_id, or a file of another type, is a typed 400 naming the part |
An assistant message with content: null and no tool_calls (OpenAI's refusal shape) | Dropped and recorded |
stream_options | Usage is on. An explicit include_usage: false removes the usage chunk, and you are still billed. It is not forwarded and not named in x-tm-dropped-params |
n | n > 1 is a 400 invalid_request on /v1/chat/completions and /v1/messages. The images route takes n up to 4 (see /docs/api-images) |
temperature | Forwarded when ≤ 1. temperature > 1 is a typed 400, never clamped (Anthropic's range is 0 to 1) |
top_p | Forwarded only without temperature (Anthropic takes one of the two). Otherwise dropped and recorded |
parallel_tool_calls: false | → tool_choice.disable_parallel_tool_use: true |
tools[].function.strict | Mapped 1:1 |
response_format, prediction, audio, modalities | Typed 400 naming the parameter. Not mapped across dialects yet |
reasoning_effort | → output_config.effort at the same level (low to max; minimal → low). none → thinking: {"type": "disabled"} on Claude Haiku 5.5, Sonnet 5 and Opus 5, and low on the other Claude 5 models. Without it, thinking is the model's default. On Claude Sonnet 4.5 and Haiku 4.5 it is dropped and recorded (none is not recorded). A value outside OpenAI's levels is a typed 400 |
Trailing assistant message | Typed 400 (an assistant prefill, which current Claude models reject) |
Everything else (logprobs, seed, penalties, …) | Dropped and recorded, never forwarded |
Anthropic surface → OpenAI-dialect provider
| Parameter | Handling |
|---|---|
model, stream, temperature, top_p | Copied as sent |
max_tokens | → max_completion_tokens (o-series and GPT-5 reasoning models reject max_tokens). When absent, set to the model's maximum, as above |
system (string or text blocks) | → one leading system message |
stop_sequences | → stop |
messages | tool_use ↔ tool_calls, tool_result blocks → role:"tool" messages (one per result). A system message inside the conversation stays at its place. See below |
image blocks in user messages | → image_url parts, from a base64 source or a URL source. An image in an assistant message or a tool result, or an image given by a file id, is a typed 400 naming the block |
document blocks in user messages | A base64 PDF → a file part. The block's title, context and citations are dropped and recorded. A document given by URL or as text is a typed 400 naming the block |
tools, tool_choice | auto→"auto", any→"required", none→"none", {type:"tool",name}→{function:{name}}. disable_parallel_tool_use: true → parallel_tool_calls: false. tools[].strict maps 1:1. An empty tools list is sent as no list, without auto, none or the parallel flag. Server tools (computer use, web search, bash, text editor) are a typed 400 |
thinking | adaptive, enabled → the model's own reasoning stays on (budget_tokens dropped and recorded). disabled → reasoning_effort: "none". A type or display value the gateway does not know is a typed 400. See below |
output_config | effort → reasoning_effort. format json_schema → a strict json_schema response_format. Another format type is a typed 400 |
top_k, metadata | Dropped and recorded. On a house route, metadata.user_id becomes the end-user id and is not recorded (see Sessions and prompt caching) |
cache_control (nested and top-level) | Dropped and recorded with the full path, for example messages[0].content[0].cache_control |
tool_result.is_error (nested) | Dropped and recorded with its full path |
Every drop is recorded per request, with its full path, in the x-tm-dropped-params header, in each attempt's dropped_params in GET /v1/generation, and in the ledger (which holds no content). With "provider": {"require_parameters": true}, the first drop is a typed 400 instead. Only parameters are dropped, never content.
Reasoning and structured output on OpenAI-dialect routes
When an OpenAI-dialect provider serves an Anthropic-format request (GLM 5.3, Kimi K3, DeepSeek and the other open models), reasoning and output controls map to these OpenAI fields:
| You send | The provider gets |
|---|---|
thinking: {"type": "adaptive"} or {"type": "enabled", ...} | Nothing: the model reasons by default. budget_tokens is dropped and recorded |
thinking: {"type": "disabled"} | reasoning_effort: "none". An output_config.effort beside it is dropped and recorded |
output_config.effort | reasoning_effort at the same level, or the nearest level a provider declares. A system message's own effort applies from its place on, so the last one wins |
output_config.format with type: "json_schema" | response_format: {"type": "json_schema", "json_schema": {"name": "output", "schema": <your schema>, "strict": true}}, only on providers that take json_schema. If none does, a typed 400 names output_config.format before anything is held or sent |
A system message inside messages | A system message at the same place. A turn-scoped one (clear_at: "next_user_message") is left out once a user message follows it. One with no content only sets effort. A tool added or removed in one that still shows is a typed 400 |
thinking.display sets how reasoning comes back. An open model's reasoning is its raw chain of thought.
"omitted"and"updates": eachthinkingblock has an emptythinkingfield, as on Anthropic's API, and the text travels in itssignature. Send the block back unchanged: Morph's and Mistral's routes read the reasoning from it. Under"updates", Claude Code shows such a block as a progress line, without the raw reasoning."summarized"or nodisplay: readablethinkingblocks with the reasoning text.
Dropped and recorded:
thinkingand the effort, when a provider serves the model with thinking off (for example Qwen3.8 Max on DeepInfra) or the model does not reason (Mistral's Codestral, Large 3 and Ministral 3).context_management(context editing) andsafeguards(Claude Code's auto-mode classifier context).
GPT-6 and GPT-5.6 on the chat surface
The GPT-6 and GPT-5.6 models (GPT-6 Astra, GPT-6.1 Sol, GPT-6 Sol, GPT-6 Luna, GPT-5.6 Sol, GPT-5.6 Terra and GPT-5.6 Luna) take tools only on OpenAI's Responses API. You call them on /v1/chat/completions or /v1/messages, and the gateway translates. This applies on the marketplace route and on your own OpenAI connection.
| Chat field | Handling |
|---|---|
messages | → Responses input items: text, image and file parts; an assistant's tool calls as function_call or custom_tool_call items; a tool message as the matching output item. Audio parts are a typed 400 |
max_tokens, max_completion_tokens | → max_output_tokens. A limit under 16 is sent as 16 (OpenAI's floor) and recorded as dropped |
tools with type: "function" | → function tools. A tool without strict is sent with strict: false, the Chat Completions default |
tools with type: "custom" | → custom (free-form) tools, with a format of text or a grammar in lark or regex syntax. Both the chat form ({type: "custom", custom: {name, description, format}}) and Cursor's flat form work. A call comes back as a tool_calls entry {id, type: "custom", custom: {name, input}}. Send its result as a role:"tool" message. input must be a string |
tool_choice | → the Responses form. It may name a custom tool |
| Tool-call ids | An id over 64 characters becomes call_ and a hash of it, the same in the call and its result |
response_format, reasoning_effort | → their Responses forms |
store | Sent as false unless you set it |
n, stop, seed, logprobs, top_logprobs, logit_bias, prediction, the penalties | Dropped and recorded. A value equal to the default (n: 1, a penalty of 0) is dropped without a record |
temperature, top_p | Dropped and recorded, except temperature: 1 and top_p: 1. These models take only their default sampling |
The answer is a chat completion, or chat.completion.chunk records on a stream. finish_reason is tool_calls after a tool call and length at max_output_tokens. A short GPT-6 commentary before the answer is joined to it with a blank line. Reasoning does not carry from one turn to the next.
Content is never silently dropped
A provider that drops a content part still bills the request, so the gateway never drops content.
A part goes only to a model that takes it.
- Each listing declares what a message may carry: text, plus any of image, file (a document such as a PDF), audio and video.
GET /v1/modelsshows it for your key inarchitecture.input_modalities. A model's page lists only the inputs that every listing takes. - The gateway sends a part only to a deployment whose listing takes its kind. A part that no deployment takes is a typed 400 naming the exact field, on both surfaces, before anything is reserved or sent:
messages[0].content[1]: model "gpt-4.1-mini" does not take audio input; it takes text, image and file. - Checked parts:
image_url,file,input_audioandvideo_url(orinput_video) on the OpenAI surface;imageanddocumentblocks on the Anthropic surface, also inside atool_result. - A BYOK route declares nothing and gets every part as sent.
A translation names what it cannot carry. Images (JPEG, PNG, GIF and WebP) and PDFs in user messages cross between the formats, as the tables show. On a translated route, this content is a typed 400 naming the exact field (messages[2].content[0] and so on):
- audio parts
- an image or a file given by file id
- a file other than a PDF
- an image in an assistant message or a tool result
- unknown block types
Exception: echoed thinking blocks. On the Anthropic surface, assistant thinking and redacted_thinking blocks that you echo back are dropped, not rejected. The gateway itself emits them for an OpenAI-dialect model's reasoning (on a stream, and as a leading block on a non-streamed reply).
- Thinking is never forwarded to an OpenAI-dialect provider.
- A signed
thinkingblock (Claude's own) and aredacted_thinkingblock are named inx-tm-dropped-params. - An unsigned block, or one with the gateway's own signature (text hidden by
thinking.display), is dropped without a record, andrequire_parametersdoes not refuse the next turn for it. - On an Anthropic-dialect provider, the gateway's own blocks are left out without a record, because Anthropic verifies every signature. A conversation served elsewhere while Anthropic was unavailable can continue on Anthropic.
Sessions and prompt caching
A provider keeps a prompt's cache on one machine. The gateway passes on what each provider needs to route a conversation back to it.
Session id. The gateway takes the first valid value of these inputs, in this order:
| Order | Input | Where |
|---|---|---|
| 1 | session_id | Body |
| 2 | x-session-id | Header |
| 3 | x-session-affinity | Header |
| 4 | x-claude-code-session-id | Header |
- A valid value has 1 to 256 printable ASCII characters. The body field
prompt_cache_keyfollows the same rule. - An invalid value is ignored, with no error. The gateway uses the next input and records the ignored one as
session_id (invalid),prompt_cache_key (invalid)orheader:<name> (invalid). Withprovider.require_parameters: true, it is a 400. - A valid session input is never recorded as dropped.
Each route gets its own field:
| Route | Field sent | Value |
|---|---|---|
| OpenAI (house or BYOK), Azure | prompt_cache_key | Your prompt_cache_key, else the session id. Nothing if you sent neither |
| Morph | Header x-session-id | The session id, else your prompt_cache_key. Nothing if you sent neither. Morph picks region and worker by it |
| Anthropic, Bedrock | Nothing | Its cache matches the prompt prefix |
| DeepInfra | Nothing | Its cache matches the prompt prefix without a key |
- Values go unchanged. A value longer than the provider accepts is replaced by
tm-followed by 43 base64url characters: a hash of the value. prompt_cache_retention("in_memory"or"24h") goes unchanged to OpenAI and Azure. Other routes drop and record it.
End-user ids on house routes. A house route runs on the marketplace's provider account, shared by all customers. It gets one end-user id: a hash of your organization and your safety_identifier (else user, else, on /v1/messages, metadata.user_id), or of your organization alone.
| House route | Field sent | Not sent |
|---|---|---|
openai | safety_identifier | user |
anthropic | metadata.user_id | user, safety_identifier |
BYOK routes get your values unchanged: user and metadata, and safety_identifier on OpenAI and Azure from /v1/chat/completions. Anthropic and Bedrock BYOK routes, and any translation from /v1/messages, drop and record safety_identifier.
Cache marks.
- The gateway never adds a
cache_controlmark to a block or a part. - On a request with no
cache_controlat all, the gateway may add the top-level field (Automatic caching on Claude). - The ledger prices every cache write at the 5-minute rate. On a house route,
ttl: "1h"is removed from every mark and recorded as<path>.cache_control.ttl, for examplesystem[0].cache_control.ttl.ttl: "5m"passes. BYOK routes keepttl: "1h".
Staying on the first route. When the gateway's quota pool for your first route is full, a follow-up request (an assistant or tool message after a user message) waits up to 5 seconds for that route, so it does not move to a cold cache. x-tm-affinity-wait-ms gives the milliseconds waited.
Automatic caching on Claude
Coding agents that call Claude through an OpenAI-compatible setting (Cline, Cursor, Kilo Code, OpenCode and others) send no cache marks. The gateway turns on Anthropic's automatic caching when all of these are true:
- 01Anthropic's own API serves it: the marketplace's
anthropicroute, or your own Anthropic connection. Not Bedrock or dedicated endpoints. - 02The request has no
cache_controlanywhere (top level, system prompt, tool, message, or a block inside a tool result). - 03It has tools, or an assistant turn before its last message. A single question without tools is left alone.
- 04The prompt (system prompt, tools and messages, at four bytes a token) reaches the model's minimum cacheable length:
- 512 tokens on Claude Opus 5.5, Opus 5, Fable 5.1, Fable 5, Sonnet 5.5 and Haiku 5.5
- 1,024 on Claude Sonnet 5 and Sonnet 4.5
- 4,096 on Claude Haiku 4.5
- 05You did not turn it off with the request header
x-tm-auto-cache: off(any letter case).
The gateway then adds only the top-level cache_control: {"type": "ephemeral"}. Anthropic puts one cache point on the last cacheable block and moves it forward as the conversation grows. The cache lasts 5 minutes, and the answer does not change. The first turn pays a cache write (1.25 times the input price). Later turns read at the cache-read price, a tenth of the input price or less.
A response from a Claude route carries x-tm-auto-cache: applied, or skipped: and the reason (opted-out, client-markers, single-turn, short-prompt, bedrock, dedicated and a few more). A closed dedicated endpoint never sends it. x-tm-dropped-params does not change.
Cache privacy. Marketplace requests reach Anthropic from one RouterPlus account, and Anthropic keeps caches per account. A cache read needs a prompt start that is byte for byte the same as an earlier one, which can be another customer's. So someone who knows your exact prompt start could tell from their own cache reads that it was sent in the last few minutes. This applies also to requests with their own marks. To avoid it, send x-tm-auto-cache: off and no marks.
Usage accounting (how tokens are counted)
| Concept | OpenAI surface | Anthropic surface |
|---|---|---|
| Billable input | prompt_tokens, cache-inclusive: input + cache reads + cache writes | input_tokens, which excludes cache reads and writes |
| Cache reads | prompt_tokens_details.cached_tokens | cache_read_input_tokens |
| Cache writes | prompt_tokens_details.cache_write_tokens | cache_creation_input_tokens |
| Output | completion_tokens | output_tokens |
| Reasoning | completion_tokens_details.reasoning_tokens (a subset of output) | not reported separately |
Translation converts by this table. total_tokens = prompt_tokens + completion_tokens.
- Usage is always on, streamed or not. Only an explicit
include_usage: falseremoves the usage chunk (seestream_optionsabove). - Cache writes bill at their own rate. On billed OpenAI-surface responses,
prompt_tokens_details.cache_write_tokensis always present (0 when the provider reports none). - Every billed response carries
usage.cost(USD), in the final usage chunk on a stream, else in the body. It uses the ledger's integer micro-USD math and covers the full debit, including attempts that failed over before your answer started. - Rounding. Prices are micro-USD per million tokens. The reservation rounds up, and settlement rounds down.
- Web search has a fee per search, at the provider's price, on top of the tokens: Anthropic's web search server tool, and OpenAI's hosted web search on
POST /v1/responses. The fee is insideusage.cost. See Pricing & billing.
An Anthropic-surface stream that dies mid-answer never sends message_delta, its only usage slot. Get those requests from GET https://demo-api.routerplus.com/v1/generation?id=REQUEST_ID, which returns every attempt with tokens and settled cost. The OpenAI surface emits known usage and cost before the terminal error event.
Streams
Event order on the OpenAI surface:
- 01A role-priming delta, sent as soon as the upstream responds.
- 02Content deltas.
- 03The finish chunk.
- 04The usage chunk.
- 05
data: [DONE].
The gateway never sends [DONE] after an error. Nothing follows a terminal event.
- Keep-alives. After output starts, each silence of 15 seconds or more gets a keep-alive frame, so proxies do not close a slow stream: an SSE comment (
: processing) on the OpenAI surface, a nativepingevent on the Anthropic surface. SDKs skip both. - The commit boundary. Before output starts, the gateway fails over provider failures with zero backoff, and you see one response with one event sequence.
x-tm-attemptscounts the dispatches. After output starts, the gateway never switches providers. A failure becomes one terminal error event inside the 200 stream, and you decide whether to retry. A refusal is never rerouted. - The request deadline. 450 seconds from dispatch, or a routing policy's
timeout_ms. A stream still open then gets the same terminal error event. - Mid-stream terminal shape. On the OpenAI surface: a full chunk envelope with
finish_reason: "error"and a top-levelerrorobject. On the Anthropic surface: anevent: errorframe.
finish_reason and native_finish_reason
Translated responses map finish reasons both ways:
Anthropic stop_reason | OpenAI finish_reason |
|---|---|
end_turn | stop |
stop_sequence | stop |
max_tokens | length |
tool_use | tool_calls |
refusal | content_filter |
Two stop reasons both become stop. Translated responses on the OpenAI surface carry the raw value in choices[0].native_finish_reason, in the body and in the stream's finish chunk. Use it for analytics. Passthrough responses are relayed as sent.
Errors
The gateway writes every error body in the dialect you called with.
- Each body carries a stable
error_typewith the canonical class:error.metadata.error_typeon the OpenAI surface,error.error_typeon the Anthropic surface. - Each body also carries the dialect's native fields, so SDK error handling and retries work. On the Anthropic surface these are the native type strings (
billing_error,rate_limit_error,authentication_error,not_found_error,api_error,invalid_request_error). - The gateway never relays a provider's raw error body. The message carries the provider's own message, up to 1,000 characters, with keys, account ids and host names removed (never on a closed dedicated endpoint).
x-tm-upstream-statuscarries the provider's HTTP status.- The body's
metadatanames where the failure came from (origin, and the limit that refused it when one did). See Errors.
A prompt too long for the model is a 400 in the surface's standard shape, which coding agents compact on: code context_length_exceeded with OpenAI's wording, or prompt is too long: N tokens > M maximum on the Anthropic surface. The class is context_overflow (see Context overflow).
Canonical classes and how routing treats them:
| Upstream signal | error_type | Failover |
|---|---|---|
| 429 (Retry-After honored, capped at 59 s; 10 s when the provider names none) | rate_limit | yes |
| 401/402/403 from a provider (a problem with the RouterPlus account, not yours) | auth | yes |
| Context/length errors | context_overflow | no: fix the request or pick a bigger window |
| Content policy / refusal | content_policy | never: a refusal is not rerouted |
| 404 / model not found upstream | model_unavailable | yes |
| 5xx / 529 / overloaded | upstream_error | yes |
| Other 4xx | upstream_error | no |
On POST /v1/decisions, some signals map differently. The gateway reads them by their status and the provider's own error code, never by the provider's name:
- These are a 503
model_unavailablewithmetadata.retryable: false: Levanto's 402, Fastino's 402 or 403, a System One provider's 401, 402 or 403 on the RouterPlus account, and Cloudflare's 429 with code 3036 (the free daily allocation is used up). - Cloudflare's 413 with code 5021 is a 413
context_overflow. - On Perplexity Decider v1 27B, Perplexity's 400 that names the model's maximum context length is a 400
context_overflow, by the context rule above. - Timeouts: a System One provider's 408 is a retryable 504
upstream_error. - OpenAI's 404
model_not_foundon GPT-6 Luna Decisions means RouterPlus's access to the model was withdrawn. It is a 404model_unavailableby the 404 rule above, with no second provider to try. - A decisions provider's 429 never counts against the deployment's health circuit.
On GPT-6 Luna Decisions, OpenAI can decline one question and answer the rest. That is not a content_policy error. The response is a 200, that question's answer is {"type": "refusal"}, and the call is billed. See POST /v1/decisions.
Out of credits is a 429 insufficient_quota, which OpenAI's SDKs back off on. Error bodies never echo flagged input content. Errors lists the fix for each status.
Response headers
| Header | When | Meaning |
|---|---|---|
x-request-id | every call | the id that GET /v1/generation?id= audits |
x-tm-provider | served requests | the deployment that served the answer |
x-tm-attempts | served or failed dispatches | physical dispatches, failovers included |
x-tm-upstream-status | when an upstream responded | the provider's own HTTP status |
x-tm-error-code | failures | canonical class |
x-tm-error-origin | limit, provider and infrastructure failures | where it came from: gateway_admission, upstream_quota, upstream, gateway_infrastructure or authorization |
x-tm-limit-scope, x-tm-limit-kind, x-tm-limit-id | a refused limit | which limit refused the request (see Limits and capacity) |
x-tm-cap-reset | spend-cap 429s | when the monthly cap resets (the first of next UTC month) |
x-should-retry | spend-cap 429s | false: do not retry before x-tm-cap-reset. OpenAI's and Anthropic's SDKs and Claude Code obey it |
x-tm-auto-cache | a Claude route served the request (never on a closed dedicated endpoint) | applied, or skipped: and the reason (see Automatic caching on Claude) |
x-tm-dropped-params | when not empty | comma-joined names of the parameters the gateway removed. Every drop is recorded |
x-tm-upstream-model | when the route knows the model by another id | the id the gateway sent upstream |
x-tm-served-by | when a route reports one | the provider that ran the request |
x-tm-route-plan-id | billed requests | the id of the route plan that chose the deployments |
x-tm-admission-mode | billed requests | shared, or bounded_local while the shared limit store is unreachable |
x-tm-remaining-rpm, x-tm-remaining-tpm | admitted requests | the smallest remaining allowance across the limits the request claimed |
retry-after | 429/503 | whole seconds to wait, at most 59 (usage_limited sends 60 or 3,600). A provider's value is honored, capped at 59. A provider's 429 without one gets 10 |
Browsers can read every header above except x-tm-error-code and x-tm-route-plan-id, which are not CORS-exposed. In browser code, use the body's error_type.
Optional: send HTTP-Referer and X-Title to identify your app in per-app analytics. They carry no content.
Not supported
- Audio and video input, and files other than PDFs, across dialects (see Content is never silently dropped).
- Image edits, variations and streaming (see POST /v1/images/generations).
n > 1on the chat surfaces.- Model id variants. An id that contains
:is not in the catalog and returns 404. The gateway never substitutes another model. - On passthrough routes, the provider decides how to handle forwarded parameters. RouterPlus guarantees only what this page documents.
See also: Migrate · Quickstart · Errors
For agents: /docs/compat.md and /llms.txt