Console
API reference

Chat completions

The OpenAI-compatible chat endpoint.

POST/v1/chat/completions

The OpenAI-compatible chat endpoint. Point any OpenAI SDK or client at https://demo-api.routerplus.com/v1 with your RouterPlus key, and call any chat model in the catalog, Claude models included. The gateway translates requests, streams and errors between provider dialects.

Image models answer on POST /v1/images/generations only. The Anthropic-native endpoint is POST /v1/messages.

POST https://demo-api.routerplus.com/v1/chat/completions

Examples

curl
curl -N https://dev2-api.routerplus.com/v1/chat/completions \
  -H "Authorization: Bearer $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"claude-haiku-4-5","stream":true,"max_tokens":100,"messages":[{"role":"user","content":"Say hello."}]}'
python
import os
from openai import OpenAI

client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"])

resp = client.chat.completions.create(
    model="claude-sonnet-4-5",  # a Claude model over the OpenAI wire: the gateway translates
    max_tokens=200,
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
print(resp.choices[0].message.content)
print(resp.usage)  # prompt/completion tokens, cache splits, cost (USD)
typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://demo-api.routerplus.com/v1",
  apiKey: process.env.TM_API_KEY,
});

const stream = await client.chat.completions.create({
  model: "gpt-4o-mini",
  stream: true,
  messages: [{ role: "user", content: "Count to five." }],
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
  if (chunk.usage) console.log("\n", chunk.usage); // final chunk: tokens, cache splits, cost
}

The Python and TypeScript examples use the official openai SDK with only the base URL changed.

Authentication

HeaderFormat
AuthorizationBearer tm_vk_... (the standard OpenAI header)
x-api-keytm_vk_... (also accepted, same key)

A missing or invalid key returns 401 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).

ParameterTypeBehavior
modelstring, requiredA catalog id. See Model discovery. An unknown id is a 404. The gateway never substitutes another model. Each Claude model also answers to one rp- name, for clients such as Cursor that do not send claude- names to another base URL: rp-sonnet-5-5 is claude-sonnet-5-5 (see Models & catalog). This route also serves a deployed endpoint from Optimize (tm/<name>-v<n>), with a key of the organization that owns it, and a dedicated endpoint (<your-namespace>/<name>).
messagesarray, requiredRoles system, developer, user, assistant, tool. Content is a string or text parts. A model that takes them also accepts image_url, file, input_audio and video_url parts (see architecture.input_modalities in GET /v1/models). A part the model does not take is a typed 400 that names the part. See Content parts.
streambooleanServer-sent events. See Streamed response.
max_tokensnumberOutput cap, from 1 to the model's own maximum (max_output_tokens in the catalog). The maximum is 128,000 on the Claude 5, GPT-6 and GPT-5.6 models, and can be below 32,768 (GPT-4o: 16,384). If you send neither max_tokens nor max_completion_tokens, 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. If the catalog declares no maximum, or does not list the model (on your own connection), the value is 32,768. null counts as not set. A value under 1 (0, -1) or above the maximum is a 400.
max_completion_tokensnumberAn alias of max_tokens. Both with different values is a 400.
temperaturenumberPassed through. A value above 1 cannot be translated to an Anthropic-dialect provider. See Wire compatibility.
top_pnumberPassed through.
stopstring | string[]Becomes Anthropic stop_sequences on cross-dialect routes.
toolsarraytype: "function" tools work on every model. A tool without parameters gets an empty object schema on Anthropic-dialect routes. Custom (free-form) tools work on the GPT-6 and GPT-5.6 models (all seven call OpenAI's Responses API): {"type":"custom","custom":{"name":"…","description":"…","format":{"type":"text"}}}, or a grammar format in lark or regex syntax. The call comes back as a tool_calls entry {id, type: "custom", custom: {name, input}}. Claude models take function tools only: a custom tool to a Claude model is a 400 that names it. See Wire compatibility.
tool_choicestring | object"auto", "none", "required", {"type":"function","function":{"name":"..."}}, or, on the models above, {"type":"custom","custom":{"name":"..."}}.
parallel_tool_callsbooleanfalse becomes disable_parallel_tool_use on Anthropic-dialect routes.
nnumberA value above 1 is a 400 invalid_request. See Why n > 1 is rejected.
stream_optionsobjectUsage is always on. An explicit {"include_usage": false} is honored: you are billed the same, but you get no usage chunk. Otherwise an OpenAI-dialect provider gets the object with include_usage set. On a translated route the gateway acts on it itself, and does not forward it or name it in x-tm-dropped-params.
providerobjectRouting 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.
session_idstringA session id. The gateway sends it to OpenAI and Azure as prompt_cache_key, and to Morph as the header x-session-id. The headers x-session-id, x-session-affinity and x-claude-code-session-id work too. See Sessions and prompt caching.
prompt_cache_keystringOpenAI's cache key, sent unchanged to OpenAI and Azure.
prompt_cache_retentionstring"in_memory" or "24h". Sent unchanged to OpenAI and Azure, dropped and recorded elsewhere.
safety_identifierstringYour end-user id. On a house route the gateway sends a hash of it with your organization. See Sessions and prompt caching.
cache_controlobjectAutomatic prompt caching, as on the Anthropic API. Sent to Anthropic, dropped and recorded on OpenAI and Azure. A field that Anthropic would refuse is dropped and recorded on every route (see Wire compatibility). A cache_control mark on a text part is kept for Anthropic and passes unchanged to OpenAI and Azure, which ignore it.

A top-level api_key, base_url, connection_id or other credential or destination field is a 400. You set keys and endpoints in the console, never in a request.

Everything else

The table above is the portable set: it works on any provider. Other parameters depend on the dialect of the provider that serves the request:

Serving providerWhat happens
OpenAI dialectThe OpenAI chat parameters (response_format, reasoning_effort, seed, user, logprobs, top_logprobs, frequency_penalty, presence_penalty, logit_bias, metadata, store, prediction) reach it as you sent them.
Translated to the Anthropic dialectresponse_format, prediction, audio and modalities are a typed 400, because they change what the model produces. reasoning_effort becomes Claude's own effort (output_config.effort). A Claude model that takes no effort drops it and records the drop. Other unknown parameters are dropped, never changed into something else.
  • Any other top-level key is dropped and recorded on every route.
  • The x-tm-dropped-params header names every drop.
  • "provider": {"require_parameters": true} turns a drop into a 400.
  • The full tables are on Wire compatibility.

Content parts

The gateway never drops content, because the provider would still bill for it. A part the model does not take is a typed 400 that names the field (for example messages[0].content[1]), on every route, before anything is billed or sent. An image to a text-only model is one such case.

  • On an OpenAI-dialect provider, a part that the model takes passes through as sent.
  • On a route translated to the Anthropic dialect (Claude), the gateway translates an image_url part (a JPEG, PNG, GIF or WebP data URL, or an http(s) URL) and a file part with a PDF data URL.
  • On those routes, audio parts, video parts, files given by id, other file types and unknown part types are a typed 400 that names the field.

Why n > 1 is rejected

The gateway returns one choice. It refuses n above 1 before dispatch, so you never pay for fewer choices than you asked for:

json
{
  "error": {
    "code": "invalid_request",
    "message": "n>1 is not supported in v1; request a single choice",
    "type": "invalid_request_error",
    "metadata": { "error_type": "invalid_request" }
  },
  "request_id": "..."
}

Non-streamed response

A standard chat.completion object.

  • On a translated response, id is chatcmpl-<request-id>.
  • On an OpenAI-dialect provider, the body passes through with the provider's own id, keeping OpenAI's fields and the extensions below only. Fields a provider adds of its own (its cost, its token accounting, its host's name) are dropped. The message passes as the model wrote it (what an answer keeps).
  • model names the model you asked for, also when the provider knows it under another id.
  • For the /v1/generation?id= audit, use the x-request-id response header. It carries the bare request id on every route.
  • On a closed dedicated endpoint, the body keeps a fixed set of fields on every route. model is the endpoint id, id is chatcmpl- and the request id without dashes, and provider fields such as system_fingerprint and native_finish_reason are dropped.
json
{
  "id": "chatcmpl-1c9a7b2e-...",
  "object": "chat.completion",
  "created": 1757000000,
  "model": "claude-sonnet-4-5",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello!" },
      "finish_reason": "stop",
      "native_finish_reason": "end_turn"
    }
  ],
  "usage": {
    "prompt_tokens": 12,
    "completion_tokens": 5,
    "total_tokens": 17,
    "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
    "cost": 0.000111
  }
}

Usage fields (the billing contract, see Wire compatibility):

FieldMeaning
prompt_tokensAll billed input tokens, cache included: uncached input + cache reads + cache writes.
prompt_tokens_details.cached_tokensCache reads, billed at the cached rate.
prompt_tokens_details.cache_write_tokensCache writes, billed at their own rate. Always present on billed responses, 0 when none.
completion_tokensOutput tokens. Reasoning tokens are a subset, in completion_tokens_details.reasoning_tokens when the provider reports them (streams always carry the field).
costSettled cost in USD, on billed keys only. The gateway computes it with the same integer micro-USD math as the ledger. It covers the full request debit, including attempts that failed over before your answer started, so you can recompute your bill from the response.

finish_reason is one of stop, length, tool_calls, content_filter. On a response translated from an Anthropic-dialect provider, choices[0].native_finish_reason keeps the provider's raw stop reason (for example end_turn, stop_sequence). When the provider returns the model's thinking, it is in message.reasoning_content.

Streamed response

With stream: true, the gateway sends SSE frames in chat.completion.chunk shape, in this order:

  1. 01
    A role delta ({"role":"assistant","content":""}) as soon as the provider answers.
  2. 02
    Content deltas: delta.content, delta.reasoning_content (thinking models), and delta.tool_calls fragments addressed by index. The id and function.name arrive on each call's first fragment.
  3. 03
    A finish chunk with finish_reason set, plus native_finish_reason on translated streams.
  4. 04
    A usage chunk with empty choices. It is always sent, without stream_options:
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1757000000,"model":"claude-haiku-4-5","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"completion_tokens_details":{"reasoning_tokens":0},"cost":0.000037}}

data: [DONE]
  1. 05
    data: [DONE].
  • These are the shapes of a translated stream.
  • On an OpenAI-dialect provider, the gateway relays the stream record for record and adds usage.cost to its usage chunk. Each chunk keeps OpenAI's fields only and names the model you asked for.
  • On a closed dedicated endpoint, each chunk keeps only the fixed set of fields, and the provider's own SSE comment lines are dropped.
  • When a started stream is silent for 15 s or more (slow reasoning models), the gateway sends an SSE comment (: processing) so that proxies keep the stream open. SDKs ignore it.
  • See Streaming for the full detail.
Warning

If the serving provider fails after your answer started, the stream ends with one error chunk. It is a full chunk envelope with finish_reason: "error" and a top-level error object with metadata.error_type. No [DONE] follows an error. The gateway never restarts an answer on another provider mid-stream. Failover happens only before the first byte of output, and you do not see it. The same chunk ends a stream that reaches the request deadline (450 s from dispatch, or a routing policy's timeout_ms).

Errors

Every error uses the OpenAI envelope with a stable error.metadata.error_type (auth, rate_limit, insufficient_quota, model_unavailable, invalid_request, context_overflow, content_policy, upstream_error, gateway_error, ...). Errors has the full table with fixes.

  • 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 the x-tm-upstream-status header has its status.
  • A prompt too long for the model is OpenAI's standard 400. error.code is context_length_exceeded, with OpenAI's wording ("This model's maximum context length is M tokens. However, your messages resulted in N tokens. …"), so coding agents compact and retry.
  • Out of credits is the OpenAI-standard 429 insufficient_quota, which your SDK recognizes. It covers an empty balance or a monthly spend cap. On a cap breach, the message and the x-tm-cap-reset header give the UTC reset time.
  • First trial keys, shown after verified sign-in, have a 20 RPM limit. Until an organization buys credit, all its keys together run at 20 RPM. The 429 rate_limit carries retry-after.
Tip

A billed request reserves an estimated cost before dispatch: one token per byte of input, plus 65,536 per image part, and all of max_tokens (at most 32,768 tokens when you omit it). If your balance cannot cover it, with your organization's other in-flight reservations, you get insufficient_quota before any provider is called. You pay the observed usage only, so a large max_tokens never costs extra. The gateway holds it in full, though, so on a small balance set max_tokens to what you need, or leave it out. See Reserve, then settle.

Response headers

HeaderMeaning
x-request-idGateway request id. Joins /v1/usage and /v1/generation?id=.
x-tm-providerDeployment that served the request. routerplus on a closed dedicated endpoint.
x-tm-attemptsDispatches made, failovers included.
x-tm-upstream-statusThe provider's own HTTP status.
x-tm-error-codeCanonical error class, on failures. x-tm-error-origin says where the failure came from, and x-tm-limit-scope / x-tm-limit-kind name a limit that refused the request. See Errors.
x-tm-upstream-modelThe id the gateway sent, when the provider knows the model under its own id. The response's model field still names the model you asked for. Absent on a closed dedicated endpoint.
x-tm-served-byThe provider that ran the request, when the route reports one. Absent on a closed dedicated endpoint.
x-tm-dedicated-endpoint, x-tm-route-roleOn a dedicated endpoint: its id, and primary or fallback.
x-tm-queue-msOn a dedicated endpoint with a paced limit: how long the request waited for its turn, in milliseconds.
x-tm-auto-cacheOn a Claude route: applied when the gateway turned on Anthropic's automatic caching, else skipped: and the reason. Send the request header x-tm-auto-cache: off to turn it off. Absent on a closed dedicated endpoint. See Automatic caching on Claude.
x-tm-dropped-paramsComma-joined field paths of every parameter the gateway removed for this route. Absent when nothing was dropped. Send "provider": {"require_parameters": true} to get a typed 400 instead.
x-tm-route-plan-idOn billed requests: the id of the route plan that chose the deployments.
x-tm-admission-mode, x-tm-remaining-rpm, x-tm-remaining-tpmOn billed requests: how the request was admitted, and the smallest allowance left across the limits it claimed. See Limits and capacity.

Model discovery

GET https://demo-api.routerplus.com/v1/models (authenticated) returns the catalog in the OpenAI list shape, one row per model id, plus your organization's dedicated endpoints. The public directory with prices and context lengths is https://demo.routerplus.com/api/models.json.

  • A model id that is not listed returns 404 model_unavailable with the id you asked for.
  • If every deployment of a listed model is cooling down after failures, you get a 503 gateway_error with retry-after: 5 and x-tm-limit-kind: health. Retry the same request.

See also: POST /v1/messages · Errors · Wire compatibility & billing contract · Quickstart

For agents: /docs/api-chat-completions.md and /llms.txt