# RouterPlus: all documentation > One API key for many LLM providers. OpenAI-compatible /v1/chat/completions and > Anthropic-compatible /v1/messages on one gateway. Pass-through pricing, > per-request cost on the wire, honest failover that never switches mid-answer. Gateway base URL: https://demo-api.routerplus.com Create an account in your browser: https://demo.routerplus.com/signup. Complete secure verification, then create an API key at https://demo.routerplus.com/console/keys. The index is https://demo.routerplus.com/llms.txt. Every page follows, in sidebar order, each under its source URL. --- Source: https://demo.routerplus.com/docs/overview.md # Overview RouterPlus is an LLM gateway. One API key and one prepaid balance give you models from many providers, over an OpenAI-compatible API and an Anthropic-compatible API. Every chat model works on both APIs. RouterPlus translates requests, streams and errors between them. You pay the listed per-token prices, and each billed response states its cost in `usage.cost`. ## Get a key 1. Open [Sign up](https://demo.routerplus.com/signup). Complete Clerk authentication and email verification. 2. Save the first API key shown after sign-in. 3. Buy credits in [Billing](https://demo.routerplus.com/console/billing). Signup does not add free credit. If you have an account, [sign in](https://demo.routerplus.com/login) and create another key at [API keys](https://demo.routerplus.com/console/keys). If purchase matching is offered for your account, Billing shows the cap and remaining matching credit. ## Make a call ```bash export TM_API_KEY=tm_vk_... curl -N https://demo-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":60,"messages":[{"role":"user","content":"hello"}]}' ``` The last usage chunk of the stream has the token counts and `cost`: the USD amount this request debited. With an SDK, change only the base URL and the key: ```python import os from openai import OpenAI client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"]) r = client.chat.completions.create( model="claude-haiku-4-5", # a Claude model over the OpenAI format max_tokens=60, messages=[{"role": "user", "content": "hello"}], ) print(r.model_dump()["usage"]["cost"]) # USD debit for this request ``` ```python import os import anthropic client = anthropic.Anthropic(base_url="https://demo-api.routerplus.com", api_key=os.environ["TM_API_KEY"]) msg = client.messages.create( model="gpt-4o-mini", # a GPT model over the Anthropic format max_tokens=60, messages=[{"role": "user", "content": "hello"}], ) ``` ## Two APIs, one key | API | Base URL | Endpoint | Works with | |---|---|---|---| | OpenAI-compatible | `https://demo-api.routerplus.com/v1` | `POST /v1/chat/completions`, and `POST /v1/responses` for OpenAI's models | OpenAI SDKs, Codex, Cursor, Cline, Kilo Code, OpenCode, any OpenAI-format client | | Anthropic-compatible | `https://demo-api.routerplus.com` | `POST /v1/messages` | Anthropic SDKs, Claude Code | The key works on both APIs, as `Authorization: Bearer` or `x-api-key`. The API you use does not limit the model: OpenAI-format code can call Claude models, and Anthropic-format code can call GPT models. See [Authentication](/docs/authentication). ## How requests behave - **Cost.** Every billed response has `usage.cost` in USD. It covers the full debit, including attempts that failed over before your answer started. See [Pricing & billing](/docs/pricing). - **Failover.** Before any output reaches you, RouterPlus fails over to another provider. After output starts, the request stays with that provider. A failure then ends the stream with one error event. See [Routing & failover](/docs/routing). - **Keep-alives.** During a silence, the stream sends a keep-alive every 15 seconds. See [Streaming](/docs/streaming). - **No substitution.** An unknown model id returns a 404 that names the id you sent. RouterPlus does not pick another model. - **Strict requests.** `n>1` returns a 400. Content or a parameter that cannot be translated returns a typed 400 that names the field. RouterPlus drops unknown top-level parameters and names each one in the `x-tm-dropped-params` header. See [Wire compatibility](/docs/compat). - **Metadata only.** Metering and logs record token counts, timings, outcomes and cost. See [Data policy](/docs/data-policy). ## Gateway endpoints All gateway endpoints at `https://demo-api.routerplus.com` take your API key. | Endpoint | What it does | |---|---| | `POST /v1/chat/completions` | OpenAI-compatible completions, streamed or not | | `POST /v1/messages` | Anthropic-compatible messages, streamed or not | | `POST /v1/messages/count_tokens` | Anthropic token counting, not billed. Only models with an Anthropic-format deployment | | `POST /v1/responses` | OpenAI Responses API. See [POST /v1/responses](/docs/api-responses) | | `POST /v1/embeddings` | Text to vectors. See [POST /v1/embeddings](/docs/api-embeddings) | | `POST /v1/audio/transcriptions`, `POST /v1/audio/speech` | Speech to text and text to speech. See [POST /v1/audio](/docs/api-audio) | | `POST /v1/images/generations` | OpenAI Images API, base64 in the body. See [POST /v1/images/generations](/docs/api-images) | | `POST /v1/videos` | Video from a text prompt. See [POST /v1/videos](/docs/api-videos) | | `POST /v1/decisions` | Typed answers from a decision model. See [POST /v1/decisions](/docs/api-decisions) | | `/v1/files`, `/v1/batches` | Many requests from one file. See [Batches](/docs/api-batches) | | `GET /v1/models` | Models your key can call. OpenAI list shape by default, Anthropic shape when you send an `anthropic-version` header | | `GET /v1/usage` | Balance, credited and spent totals, recent requests with cost | | `GET /v1/generation?id=` | Audit of each attempt for one request: tokens, provenance, cost, outcome | | `POST /v1/route` | Shows how a request would route, without sending it. See [Routing policies](/docs/routing-policies) | | `GET /v1/limits?model=` | The rate limits that apply to your key. See [Limits and capacity](/docs/admission) | ## Site | Page | What it does | |---|---| | [`/api/models.json`](https://demo.routerplus.com/api/models.json) | Public catalog: ids, prices, context windows, who serves each model. No auth. See [Models & catalog](/docs/models) | | `/models/` | Each model's providers, prices, latency and uptime over the last 24 hours | | [`/playground`](https://demo.routerplus.com/playground) | Chat with any catalog model, compare up to three side by side, judge with a decision model, and make images. See [Playground](/docs/playground) | | [`/model-search`](https://demo.routerplus.com/model-search) | Optimize: find a cheaper model or mix that scores as well as yours on your Braintrust evals, then deploy it. See [Optimize](/docs/model-search) | | [`/endpoints`](https://demo.routerplus.com/endpoints) | Your [dedicated endpoints](/docs/dedicated-endpoints) and the models you deployed from Optimize. Each has a `tm/...` model id that your keys can call | | [`/console`](https://demo.routerplus.com/console) | Keys, usage, logs, credits, limits, team and governance. See [Console](/docs/console) | | `/signup`, `/login` | Browser signup and sign-in through Clerk | | [`/llms.txt`](https://demo.routerplus.com/llms.txt), [`/llms-full.txt`](https://demo.routerplus.com/llms-full.txt) | The docs index for agents, and every docs page as raw markdown in one file | ## Where next - [Quickstart](/docs/quickstart) - browser signup to a streamed API call in five steps. - [Agent integration guide](/docs/agent-integration) - the page an AI agent reads to build RouterPlus into your product. - [Coding agents](/docs/install) - setup for Claude Code, Codex, Cursor, Cline, Kilo Code, OpenCode and more. - [Migrate in one prompt](/docs/migration) - move the OpenAI SDK, Anthropic SDK, Claude Code or Codex with one paste. - [Errors & remediation](/docs/errors) - every `error_type`, what it means and what to do. > [!TIP] > If a coding agent does the work, point it at > [https://demo.routerplus.com/docs/agent-integration.md](https://demo.routerplus.com/docs/agent-integration.md) to build > RouterPlus into a product, or at [Install](/docs/install) to set up a coding tool. > [https://demo.routerplus.com/llms.txt](https://demo.routerplus.com/llms.txt) is the index it reads first. --- Source: https://demo.routerplus.com/docs/quickstart.md # Quickstart Go from signup to a streamed, billed model call in five steps. You need a browser, `curl` and an email address. One RouterPlus key works on two APIs: | API | Endpoint | Works with | |---|---|---| | OpenAI-compatible | `POST https://demo-api.routerplus.com/v1/chat/completions` | OpenAI SDKs, any OpenAI-format client | | Anthropic-compatible | `POST https://demo-api.routerplus.com/v1/messages` | Anthropic SDKs, Claude Code | Every chat model works on both APIs. Image models use [POST /v1/images/generations](/docs/api-images). > [!TIP] > To set up with a coding agent, give it the runbook on [Coding agents](/docs/install). > Complete browser signup first, then let the agent configure your client. ## 1. Sign up and save your key 1. Open [Sign up](https://demo.routerplus.com/signup) and complete Clerk authentication and email verification. 2. Save the `tm_vk_` API key shown after your first verified sign-in. You can copy it again later from its row at [API keys](https://demo.routerplus.com/console/keys). 3. Buy credits in [Billing](https://demo.routerplus.com/console/billing) before your first call. Signup does not add free credit. If purchase matching is offered for your account, Billing shows the cap and remaining matching credit. If you have an account, [sign in](https://demo.routerplus.com/login) with the same verified email and create a key at [API keys](https://demo.routerplus.com/console/keys). Your balance and keys stay. You can create an account only in the browser. `POST /v1/signup` is removed. See [Authentication](/docs/authentication). Export the key: ```bash export TM_API_KEY=tm_vk_... ``` ## 2. Pick a model ```bash # Public catalog, no auth: ids, prices, context windows, provider retention curl -s https://demo.routerplus.com/api/models.json # Models your key can call, in your SDK's list shape curl -s -H "Authorization: Bearer $TM_API_KEY" https://demo-api.routerplus.com/v1/models ``` `GET /v1/models` returns the OpenAI list shape. It returns the Anthropic shape when you send an `anthropic-version` header. See [Models & catalog](/docs/models). > [!NOTE] > An id that is not in the catalog returns a `404` with `error_type: model_unavailable` > and the id you sent. RouterPlus does not substitute another model. ## 3. Make a streamed call on the OpenAI API This calls a Claude model in the OpenAI format: ```bash curl -N https://demo-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": 60, "messages": [{"role": "user", "content": "Say hello in five words."}] }' ``` The stream sends a role delta, content deltas, a finish chunk, a **usage chunk**, then `data: [DONE]`. The usage chunk holds the cost: ```json { "prompt_tokens": 13, "completion_tokens": 9, "total_tokens": 22, "prompt_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0 }, "cost": 0.000058 } ``` `cost` is in USD. RouterPlus computes it with the same integer micro-USD math as the ledger. It covers the full request, including attempts that failed over before your answer started. You can compute it again from the token counts and the public prices. See [Pricing & billing](/docs/pricing). With the OpenAI SDK, change only `base_url` and the key: ```python import os from openai import OpenAI client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"]) stream = client.chat.completions.create( model="claude-haiku-4-5", max_tokens=60, stream=True, messages=[{"role": "user", "content": "Say hello in five words."}], ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="") if chunk.usage: # the final chunk before [DONE] print(f"\ncost: ${chunk.usage.cost}") ``` ```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: "claude-haiku-4-5", max_tokens: 60, stream: true, messages: [{ role: "user", content: "Say hello in five words." }], }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } ``` > [!TIP] > Before dispatch, RouterPlus reserves the cost of the call: about (estimated input + > `max_tokens`) at the model's prices. Without `max_tokens` it reserves at most 32,768 > output tokens. After the call, it settles to the real usage. On a small balance, keep > `max_tokens` low. If the reservation is larger than your balance, you get > `insufficient_quota` before RouterPlus calls a provider. See [Rate limits & spend caps](/docs/limits). ## 4. Use the same key on the Anthropic API This calls a GPT model in the Anthropic format: ```bash curl -N https://demo-api.routerplus.com/v1/messages \ -H "x-api-key: $TM_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "gpt-4o-mini", "stream": true, "max_tokens": 60, "messages": [{"role": "user", "content": "Say hello in five words."}] }' ``` The last `message_delta` event has the usage: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens` and `cost` in USD. ```python import os import anthropic client = anthropic.Anthropic( base_url="https://demo-api.routerplus.com", api_key=os.environ["TM_API_KEY"], ) with client.messages.stream( model="gpt-4o-mini", max_tokens=60, messages=[{"role": "user", "content": "Say hello in five words."}], ) as stream: for text in stream.text_stream: print(text, end="") ``` > [!NOTE] > Both endpoints accept both auth headers: `Authorization: Bearer` and `x-api-key`. > Use the one your SDK sends. See [Authentication](/docs/authentication). ## 5. Check the cost ```bash curl -s -H "Authorization: Bearer $TM_API_KEY" https://demo-api.routerplus.com/v1/usage ``` ```json { "balance_usd": 4.999942, "credited_usd": 5.0, "spent_usd": 0.000058, "recent_attempts": [ { "request_id": "5a2e…", "deployment": "anthropic", "model": "claude-haiku-4-5", "outcome": "completed", "usage_provenance": "observed", "input_tokens": 13, "output_tokens": 9, "cost_usd": 0.000058, "billing_source": "house", "at": "2026-09-04T10:14:03.201Z" } ] } ``` The example is shortened. Each attempt also has its price snapshot and inference cost. `recent_attempts` lists your last 20 attempts. See [GET /v1/usage](/docs/api-usage). For the full audit of one request, pass the `x-request-id` header of any response. The audit shows every attempt, including failovers, cache splits, and reserved and settled cost: ```bash curl -s -H "Authorization: Bearer $TM_API_KEY" \ "https://demo-api.routerplus.com/v1/generation?id=REQUEST_ID" ``` Both endpoints return metadata only: token counts, timings, outcomes and cost. See [Data policy](/docs/data-policy). You can also see usage and each request in the console: open [Usage](https://demo.routerplus.com/console/usage) or [Logs](https://demo.routerplus.com/console/logs). ## Rules to know - An unknown model id returns a `404`. RouterPlus does not substitute another model. See [Routing & failover](/docs/routing). - `n > 1` returns a `400 invalid_request`. - The first key shown after verified sign-in starts at 20 requests per minute. Until your organization buys credit, all its keys together stay at 20 requests per minute. After the first purchase, keys you create in the console get 300. Every organization also has token-per-minute and concurrency limits. See [Rate limits & spend caps](/docs/limits). - Every error body has a stable `error_type`, on both APIs, in your SDK's error shape. See [Errors](/docs/errors). ## Next steps - [Agent integration guide](/docs/agent-integration) - the page an AI agent follows to build RouterPlus into your product - [Coding agents](/docs/install) - set up Claude Code, Codex, Cursor, Cline or OpenCode - [Migrate](/docs/migration) - move an existing codebase with one prompt - [Streaming](/docs/streaming) - event order, keep-alives and mid-stream failures - [Pricing & billing](/docs/pricing) - the math behind `usage.cost` --- Source: https://demo.routerplus.com/docs/agent-integration.md # Agent integration guide This page is for an AI coding agent that builds RouterPlus into a product. It covers these steps in order: the key, the SDK, model calls, provider pinning, sessions and caching, errors, limits and cost. Each step links to the reference page for details. - Every docs page is also raw markdown. Add `.md` to its URL, for example `https://demo.routerplus.com/docs/errors.md`. - The index of all pages is `https://demo.routerplus.com/llms.txt`. - All pages in one file: `https://demo.routerplus.com/llms-full.txt`. > [!NOTE] > **For agents.** Do not guess model ids, request fields or headers. Copy them from this page or from the live catalog. If an error message disagrees with your plan, trust the error message and read the page it names. Some steps need a person: see [Steps only a human can do](#steps-only-a-human-can-do). Stop and ask at those steps. ## The short version 1. Point the OpenAI SDK at `https://demo-api.routerplus.com/v1`, or the Anthropic SDK at `https://demo-api.routerplus.com`. Read the key from `TM_API_KEY`, on the server only. 2. Copy model ids from the catalog. The gateway never substitutes a model. An unknown id is a 404. 3. Send `max_tokens` on every request, from 1 to the `max_output_tokens` of the model. Without it, the model gets its own maximum (128,000 tokens on the Claude 5 and GPT-6 models). 4. For provider features such as server tools, call Claude models on `/v1/messages` and other models on `/v1/chat/completions`. 5. Keep the start of the prompt identical from turn to turn, and only append. This keeps prompt caches warm. 6. Pin a provider only when you must, with the `provider` object. Check the plan first with `POST /v1/route`. 7. Switch on `error_type`, never on message text. Retry `rate_limit` after `retry-after`. Stop on `insufficient_quota` and tell a human. 8. There is no idempotency key. A retry is a new request. It is billed again if it reaches a provider. 9. Keep at most 8 requests in flight per organization, unless you raised that limit. 10. Log `x-request-id` and `usage.cost` for every call. ## At a glance | Item | Value | |---|---| | OpenAI-compatible base URL | `https://demo-api.routerplus.com/v1`, for `POST /v1/chat/completions`, and `POST /v1/responses` for OpenAI's models | | Anthropic-compatible base URL | `https://demo-api.routerplus.com`, for `POST /v1/messages` (the SDK adds `/v1`) | | API key | `tm_vk_` followed by 48 hex characters | | Auth header | `Authorization: Bearer ` or `x-api-key: `, on every route | | Model list | `GET https://demo-api.routerplus.com/v1/models` with your key, or `https://demo.routerplus.com/api/models.json` (public, with prices) | | Cost of a call | `usage.cost`, in USD, in every billed response | | Audit of a call | the `x-request-id` response header, then `GET https://demo-api.routerplus.com/v1/generation?id=` | | Other routes | `POST /v1/images/generations`, `POST /v1/decisions`, `POST /v1/embeddings`, `/v1/audio`, `/v1/batches`. See [Overview](overview.md#gateway-endpoints) | ### What is not available Do not build on these: - Moderation. The path returns 404 `not_found`. - Stored responses on the Responses API. `store` is always `false`, and `previous_response_id` is a 400. The Responses API serves the OpenAI models. Claude and the other models are a preview there. See [POST /v1/responses](api-responses.md). - Server-side conversation memory and idempotency keys. Session ids exist: see [Sessions and caching](#sessions-and-caching). - More than one answer per request. `n` above 1 is a 400. - Model aliases. The one exception is the `rp-` names of the Claude models, for Cursor. In code, use the catalog ids. A `models` list is a fallback list. See [Model fallback](model-fallback.md). - Other sort values. `provider.sort` takes only `"price"`, `"throughput"`, `"latency"` and `"quality"` (or the object form `{"by": ..., "partition": ...}`). Any other value is a 400. - `provider.sort` and `provider.max_price` on a dedicated endpoint. Both are a 400 there, because the endpoint's own constraints apply. - A price filter on the model list. `GET /v1/models?provider=...` with `max_price` is a 400 that names `provider.max_price`, because the list does not compare prices. Set `max_price` on each request. The list accepts `sort`, but `sort` changes nothing there. - Provider keys or endpoints in a request. `api_key`, `base_url` and `connection_id` in the body are a 400. ## 1. Get a key and keep it safe | Key | Where it comes from | Requests per minute | Use it for | |---|---|---|---| | First trial key | Shown after verified browser sign-in through Clerk | 20 | The first test calls | | Console key | [https://demo.routerplus.com/console/keys](https://demo.routerplus.com/console/keys) | 300 | Production, staging and CI | | Identity API key | `POST https://demo.routerplus.com/api/identity/keys` | 300 | One key for each end user or service (step 7) | To get a key, the user: 1. Opens [Sign up](https://demo.routerplus.com/signup) and completes Clerk authentication and email verification. 2. Saves the first key shown after sign-in. 3. Adds paid credits in [Billing](https://demo.routerplus.com/console/billing). Signup does not grant free credit. Existing customers sign in at [https://demo.routerplus.com/login](https://demo.routerplus.com/login) with the same verified email. They create a key at [API keys](https://demo.routerplus.com/console/keys), or copy an existing one from its row. If purchase matching is offered for your account, Billing shows the cap and remaining matching credit. Signup works only in the browser. No route signs up a user or sends a magic link. When `TM_API_KEY` is available, the agent can configure clients and make API calls. Rules for the key: 1. Keep the key on the server. Read it from the `TM_API_KEY` environment variable or from your secret store. Never commit it, log it or put it in a URL. 2. Do not put the key in a browser or a mobile app. Anyone can copy it from there. The gateway accepts cross-origin requests, so a copied key works from any web page. If a browser-only internal tool must call the gateway, give the tool its own key with a low monthly spend cap. 3. Use one key for each service and each environment. RouterPlus keeps spend, caps and logs for each key. You can disable one key and the others continue to work. 4. Use a console key in production. A trial key allows only 20 requests per minute. 5. To rotate a key, create the new key, deploy it, then disable the old key in the console. A disabled key stops working within about 5 seconds. The console cannot enable it again. An owner or admin can enable it again with the identity API. Details: [Authentication](authentication.md). ## 2. Connect the SDK Change two values in the client that you already use: the base URL and the key. Set both in code. Do not depend on `OPENAI_BASE_URL` or `ANTHROPIC_BASE_URL` in the environment. If that variable is missing, the SDK sends the request and its key to the provider's own API. Python, the OpenAI SDK: ```python import os from openai import OpenAI client = OpenAI( base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"], # Optional: these name your app in usage analytics. They carry no content. default_headers={"HTTP-Referer": "https://your-app.example", "X-Title": "Your App"}, ) ``` TypeScript, the OpenAI SDK: ```typescript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://demo-api.routerplus.com/v1", apiKey: process.env.TM_API_KEY, defaultHeaders: { "HTTP-Referer": "https://your-app.example", "X-Title": "Your App" }, }); ``` With the Anthropic SDK, the base URL has no `/v1`: ```python import os import anthropic client = anthropic.Anthropic(base_url="https://demo-api.routerplus.com", api_key=os.environ["TM_API_KEY"]) ``` ```typescript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://demo-api.routerplus.com", apiKey: process.env.TM_API_KEY }); ``` ### Which surface to call Every chat model answers on both surfaces. When your surface matches the format of the route that serves the request, the gateway forwards the request as sent, with all provider features. The native surface of each model family's first route: | Model ids | Native surface | Features that need it | |---|---|---| | `claude-*` | `/v1/messages` (Anthropic SDK) | `thinking`, server tools such as web search, PDFs given by URL | | `gpt-*` | `/v1/chat/completions` (OpenAI SDK) | `response_format`, `reasoning_effort`, `seed`, `logprobs`, custom tools | On the other surface, the gateway translates: - A field that the translation cannot carry is dropped and named in the `x-tm-dropped-params` response header. - If the drop would change the answer, the request is a 400 that names the field. Before it refuses, the gateway tries another provider of the same model that speaks your format. - The `x-tm-provider` header names the provider that served you. - The catalog gives each model's native format in `providers[0].dialect`. - If your code sends only text, images, PDFs and function tools, either surface works for every model. - Prompt caching marks (`cache_control`) reach Claude from both surfaces. Details: [Wire compatibility](compat.md). ## 3. Call models ### Rules for every request 1. **Use exact model ids.** Copy them from `GET /v1/models` or `/api/models.json`. Claude and GPT ids have no prefix: `claude-sonnet-5`, `gpt-4o-mini`. Decision models keep their author prefix: `typesafe/jev-1.13`. Variants such as `:free` do not exist. A dated snapshot of a listed model, such as `claude-haiku-4-5-20251001`, also works. It routes and bills as its model. 2. **Keep model ids in one place in your configuration.** Check them against `GET /v1/models` when your service starts. 3. **Use the right route for the output.** Chat models answer on the two chat surfaces. Image models answer only on `POST /v1/images/generations`, and decision models only on `POST /v1/decisions`. The catalog field `output_modalities` (`text`, `image` or `decisions`) tells you which is which. A decision model also takes its own question format. The catalog field `decisions_wire` in `/api/models.json` names it (`systemone` for Jev, the Bespoke Nimble models, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider v1 27B, GPT-6 Luna Decisions, Sage and ScaleDown Classify-1, and `gliner`). [POST /v1/decisions](/docs/api-decisions) describes each one. Do not guess the format from the model's name. 4. **Send `max_tokens` every time**, from 1 to the model's declared `max_output_tokens` (16,384 on GPT-4o; 128,000 on the Claude 5 models). When a model declares no maximum, the maximum is 32,768. - If you leave it out, the gateway writes the model's own maximum into the request. When needed, it lowers that value so that the input and the output fit the model's context window. - A value above the model's maximum is a 400. - While the call runs, the gateway holds balance for the output limit that you send (at most 32,768 tokens when you send none). A realistic value helps on a small balance and lets more calls run at once. 5. **Ask for one answer.** `n` above 1 is a 400. Make separate calls for several samples. 6. **Send the full conversation on every turn.** The gateway keeps no conversation state between calls. See [Data policy](data-policy.md) for how request data is handled. 7. **Keep `temperature` at 1 or less** in code that can reach Claude models. On a translated request, a higher value is a 400. 8. **Refuse drops where they matter.** A field outside the forwarded set is dropped and named in `x-tm-dropped-params`. Send `"provider": {"require_parameters": true}` to get a 400 instead of a drop. - The 400 comes from the first route that would drop a field. The gateway does not go on to a later route that could carry it, so combine it with `only` or `order`. - Example: the `anthropic` route drops `seed`. A Claude request on `/v1/chat/completions` that sends `seed` and `"require_parameters": true` is a 400. ### Streaming Stream answers that a person watches, and long answers. - The OpenAI surface sends a role chunk, content chunks, a finish chunk, a usage chunk with an empty `choices` list and `usage.cost`, then `data: [DONE]`. - The Anthropic surface sends the native event sequence, with usage and `cost` in the final `message_delta`. - If the provider fails after the answer starts, the stream ends with one error event and nothing after it (no `[DONE]`). The SDKs raise an error. RouterPlus bills the partial answer. The gateway never continues an answer on another provider. To try again, send the full turn again. - During a silence of 15 seconds or more, the gateway sends a keep-alive: an SSE comment on the OpenAI surface, a `ping` event on the Anthropic surface. The SDKs skip them. If you parse SSE yourself, skip them too. - The first response headers must arrive within 20 seconds. If they do not, the gateway tries the next provider. A request can run for at most 450 seconds. - To stop an answer, close the connection. The provider stops, and you pay for the usage that the provider reported. - If the provider reported no final usage, you pay for the input and for the output already streamed to you. The input count is the provider's count when it sent one, or else the gateway's estimate. - If nothing streamed yet, you pay for the input only. Python, the OpenAI SDK, keeping the cost and handling a failure in the middle of an answer: ```python from openai import APIError parts, usage = [], None try: stream = client.chat.completions.create( model="claude-sonnet-5", max_tokens=2048, stream=True, messages=messages, ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: parts.append(chunk.choices[0].delta.content) if chunk.usage: # the usage chunk; when usage is known, it also comes before an error usage = chunk.usage except APIError: # The answer failed part way. What arrived is billed. Do not join a retry onto # `parts`: send the whole turn again, or show the error. raise print("".join(parts), "cost:", usage.cost if usage else None) ``` Details: [Streaming](streaming.md). ### Calls that do not stream - Set the client timeout above 450 seconds. The SDK default of 10 minutes works. - The Anthropic SDKs refuse a call that does not stream when `max_tokens` is above about 21,000, unless you pass an explicit `timeout`. Stream those calls instead. - If you close the connection, the gateway stops the provider. Nothing was streamed to you, so you pay for the input only. - An image render does not stop when you close the connection. It finishes and is billed at the usage that the provider reports. - A call that sends no `max_tokens` can reach the 450-second budget on a long answer. It then fails with a 504, billed $0. Send `max_tokens`, or stream. ### Tools, reasoning and structured output - **Function tools** work on both surfaces for every chat model. The gateway translates tool calls and tool results. - **Custom (free-form) tools**, `type: "custom"`, work on `/v1/chat/completions` for the GPT-6 and GPT-5.6 models. Their calls come back as `tool_calls` entries of `type: "custom"`. - **Anthropic server tools** (web search, computer use, bash, text editor) work only for Claude models on `/v1/messages`, while the `anthropic` route serves them. Web search costs $10 per 1,000 searches (the Anthropic price). RouterPlus adds that cost to the tokens in `usage.cost`. The `anthropic` route refuses `mcp_servers` and `container`. A request with either one is a 400 that names the field. - **`thinking` and `output_config.effort`** work on `/v1/messages` for every reasoning model. - For Claude they pass through as sent. - For a model that an OpenAI-format provider serves (GLM 5.3, Kimi K3, DeepSeek), effort becomes `reasoning_effort`. `adaptive` thinking leaves the model's reasoning on, and `disabled` turns it off ([details](/docs/compat#reasoning-and-structured-output-on-openai-dialect-routes)). - On `/v1/chat/completions`, Claude's thinking arrives as `reasoning_content`. - On `/v1/messages`, an answer from a non-Claude model has its reasoning as `thinking` blocks, streamed or not. A non-streamed answer starts with the reasoning. The block is readable. When you set `thinking.display` to `"omitted"` or `"updates"` (as Claude Code does), the block is empty and the text is in its `signature`. - You can send `thinking` blocks back in the next turn. - Claude Code works on these models with no change on your side. Set its model to the catalog id, for example `z-ai/glm-5.3`, with `z-ai/glm-5.3-flash` as its small model. - **`response_format`** needs an OpenAI-format provider: GPT ids and the open models. On `/v1/messages`, `output_config.format` with a JSON schema reaches an open model's provider as a strict json_schema `response_format`. - **JSON from a Claude model on `/v1/chat/completions`:** give the model one tool whose input schema is the JSON you want, and read the tool input. Claude Sonnet 4.5 and Haiku 4.5 take a forced `tool_choice`. The Claude 5 models refuse a forced tool choice (`required` or a named tool) with a 400, so use `auto` there. - Do not end the message list with an assistant message when a Claude model can serve the request from the OpenAI surface. Claude refuses a prefilled answer, so the request is a 400. ### Images in prompts Images and PDFs in user messages work on both surfaces. The gateway translates these parts between the OpenAI and Anthropic formats: - JPEG, PNG, GIF and WebP images, as a data URL or an `http(s)` URL - PDFs, as a data URL Rules: - When the gateway must translate, these parts are a 400 that names the part: an audio or video part, a file given by id, a file other than a PDF, and an image in an assistant message or a tool result. - Send an image, document, audio or video part only to a model that takes it. `architecture.input_modalities` in `GET /v1/models` lists each model's inputs. A part that the model does not take is a 400 that names the part, and nothing is billed. - While the call runs, each image or document part counts as 65,536 tokens against your limits and your balance hold. ### Other routes - **Images:** `POST /v1/images/generations` takes the OpenAI Images API body. The image comes back as base64 in the response. `response_format: "url"` is a 400. `n` is 1 unless the model allows more: see `supported_parameters.n` in `/api/models.json` (GPT Image models allow up to 4). There is no streaming. A failed render bills nothing. See [POST /v1/images/generations](api-images.md). - **Decisions:** `POST /v1/decisions` sends a state and typed questions to a decision model. It returns one typed answer for each question, for example a choice, a score or a yes/no probability. Use it for routing, classification and moderation points in your code. It replaces a chat model whose output you parse. The models: - `typesafe/jev-1.13` (Jev, from TypeSafe) - `bespokelabs/nimble-v4` and `bespokelabs/nimble-v4-max` (Bespoke Nimble v4 and v4 Max, from Bespoke Labs; `bespokelabs/nimble-latest` lets Bespoke pick the model) - `cloudflare/clef` and `cloudflare/clef-flash` (Clef and Clef-flash, from Cloudflare) - `routerplus/decider-2b` and `routerplus/kev-4b` (Decider 2B and Kev 4B, from RouterPlus) - `perplexity/pplx-decider-v1-27b` (Perplexity Decider v1 27B, from Perplexity) - `openai/gpt-6-luna-decisions` (GPT-6 Luna Decisions, from OpenAI; `gpt-6-luna` is OpenAI's chat model, not this one) - `levanto/sage-1.2` (Sage, from Levanto) - `scaledown/classify-1` (ScaleDown Classify-1, from ScaleDown) - `fastino/gliner-2.5-decide` (GLiNER-2.5-Decide, from Fastino) Rules for decision models: - The question format follows the model. System One questions (Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage and ScaleDown Classify-1) have a `type`. GLiNER takes a `schema` of classifications and extractions in place of `questions`. A question in another model's format is a 400. - GLiNER's numbers are confidences. They are not calibrated probabilities. - Perplexity Decider bills the state once per question, so ask it only the questions you need. - GPT-6 Luna Decisions adds about 150 input tokens for each question. It can decline one question, and that answer is `{"type": "refusal"}`, so check each answer's `type` before you read its value. - The gateway never fails over from one model to another. See [POST /v1/decisions](api-decisions.md). - **Token counting:** `POST /v1/messages/count_tokens` is free, for models that have an Anthropic-format provider. It counts against your requests per minute. - **Your deployed models:** an id such as `tm/-v1` from [Optimize](model-search.md) works on `/v1/chat/completions` only, with keys of the organization that deployed it. With `stream: true`, it returns the finished answer as one chunk. ## 4. Pin providers (only when you must) How routing works without a pin: - By default, RouterPlus orders a model's providers by the estimated cost of one good answer. Providers close to the best share the traffic. Until providers have measurements, the cheapest provider goes first. See [Provider routing](provider-routing.md). - Claude models go to `anthropic` first, and GPT models to `openai`. - The gateway moves to the next provider only when the current one fails before it sends any output, or has no capacity left. - Each provider charges its own price. You pay the price of the provider that serves the request: the one you name with `only`, or else the one that routing selects, fallbacks included. `/api/models.json` gives each provider's live price in `providers[].pricing_usd_per_million`. Pin a provider in these cases only: - A rule requires one provider. - A feature exists at one provider only. - You compare providers in a test. `only` and `"allow_fallbacks": false` remove fallbacks. `order` keeps every fallback. ### The `provider` object Send it in the request body on `/v1/chat/completions` or `/v1/messages`. The gateway reads it and never forwards it. An unknown key inside it is a 400. | Field | Value | Effect | |---|---|---| | `order` | provider ids | Try these providers first, in this order. The others stay as fallbacks. | | `only` | provider ids | Use only these providers. | | `ignore` | provider ids | Never use these providers. | | `allow_fallbacks` | boolean | `false` keeps only the first route. If that route is resting after failures, the request fails with 503 and `retry-after` and does not move on. | | `sort` | `"price"`, `"throughput"`, `"latency"` or `"quality"` | Turns off the default order. `"price"`: marketplace routes cheapest first, after `order`, by the prompt and completion prices per million at each provider's live price. A route billed at the cost the provider reports goes after the priced routes. `"throughput"`: the highest measured output speed first. `"latency"`: the lowest measured time to first token first. `"quality"`: the highest share of well-formed tool calls first. Without `sort`, the default order applies (see [Provider routing](provider-routing.md)), unless your key's [routing policy](routing-policies.md) orders its routes. | | `preferred_min_throughput`, `preferred_max_latency` | number (p50) or `{"p50", "p75", "p90", "p99"}` | Routes below the throughput (tokens per second) or above the latency (seconds to first token) go last. No route is left out. | | `quantizations` | list of `int4`, `int8`, `fp4`, `mxfp4`, `nvfp4`, `fp6`, `fp8`, `mxfp8`, `fp16`, `bf16`, `fp32`, `unknown` | Only routes that serve the model at these quantizations. A route that declares none is `unknown`. | | `max_price` | `{"prompt": USD, "completion": USD}` per million, at least one of them, each from 0 to 1,000,000 | Leaves out a marketplace route whose prompt or completion price is above the cap. See the rules below. | | `require_parameters` | boolean | `true`: when the first route that would serve you would drop one of your request fields, the request is a 400. The gateway does not try a later route. | | `connections`, `connection_order`, `funding` | ids, `"byok"` or `"house"` | Choose among your own provider connections and who pays. See [Routing policies](routing-policies.md). | | `regions`, `zdr`, `data_collection` | region list, boolean, `"allow"` or `"deny"` | Hard filters on the provider's published terms. `"zdr": true`: the provider keeps no prompts. `"data_collection": "deny"`: the provider does not train on or keep them. A route on your own connection passes with operator evidence. A route whose terms are not known is left out. | Rules for `max_price`: - Cached input, cache write and reasoning prices are not compared, so they are not capped. - The price compared is the price that you would pay, after the discount of a decision model. - If no route is left, the request is a 404 that names `provider.max_price`. This also happens when a route that takes your images, files or `response_format` is above the cap and the routes within it do not take them. - If the price cannot be read, the request is a 503 with `retry-after`. - A route billed at the cost the provider reports (some image and video models) has no price to compare, so `max_price` leaves it out. When every route left out is such a route, the request is a 400: `provider.max_price is not available for model "": it is billed at the cost the provider reports`. `order`, `only` and `ignore` take route labels. Marketplace route labels are the `providers[].id` values in `/api/models.json`, for example `anthropic` and `openai`. Your own connections use their profile: `openai`, `anthropic`, `azure` or `bedrock`. - A label that matches a route label selects or orders that route, even when that route does not serve the model. `only: ["anthropic"]` means the Anthropic route, so on a GPT model it leaves no route. - Any other label matches no route: it selects, orders or skips nothing. An `only` list without a route label leaves no route. | Goal | `provider` | |---|---| | Claude only from Anthropic's own API | `{"only": ["anthropic"]}` | | Fail instead of falling back to another provider | add `"allow_fallbacks": false` | Check a pin before you ship it. `POST /v1/route` returns the ordered candidates, and each excluded route with its reason. It calls no provider and costs nothing. If your controls exclude every route, a real request returns 404 `model_unavailable`. This is the same error as for an unknown model. When a pinned request gets a 404, run `/v1/route`. ```bash curl -s https://demo-api.routerplus.com/v1/route \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"claude-sonnet-5","provider":{"only":["anthropic"]}}' ``` Python: the OpenAI SDK sends fields it does not know through `extra_body`. ```python reply = client.chat.completions.create( model="claude-haiku-4-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], extra_body={"provider": {"only": ["anthropic"]}}, ) ``` TypeScript: pass a variable, so the SDK keeps the field its types do not know. ```typescript const request = { model: "claude-haiku-4-5", max_tokens: 1024, messages: [{ role: "user" as const, content: "Hello" }], provider: { only: ["anthropic"] }, // read by the gateway, never forwarded }; const reply = await client.chat.completions.create(request); ``` Check each answer with its response headers: | Header | Value | |---|---| | `x-tm-provider` | The provider | | `x-tm-served-by` | The provider that ran the request, when a route reports one | | `x-tm-upstream-model` | The id the provider received, when it differs from yours | | `x-tm-attempts` | The number of provider attempts | `GET /v1/generation?id=` shows the route of every attempt. To pin for an organization, workspace or key, save a routing policy with the policy API. The policy API takes a console session, so a human sets it up. A request's `provider` object can narrow a saved policy. It can never add routes. See [Routing policies](routing-policies.md). ### Your own provider keys (BYOK) You can route through your own OpenAI, Anthropic or Azure key, or an AWS Bedrock role. A human adds it one time in the console, in Access > Provider connections. Your code still calls the gateway with a RouterPlus key. The provider bills you directly, and `usage.cost` is 0. A key bound to a connection can call only that connection's model ids. It does not fall back to marketplace supply unless a routing policy allows it. See [Bring your own key](byok.md). ## 5. Keep sessions on one route and caches warm A prompt cache saves money and time only when the next request reaches the same provider with the same prompt start. - The gateway keeps no session state. Your app sends the full conversation on every turn. - A conversation stays on the provider that served its first turn while that provider can serve. Requests that share a long prompt prefix (system prompt and tools) go to the same provider across sessions. - A turn moves to the next route in three cases: - The first route fails before it sends output. One failure is enough. - The route's capacity pool is full. - Its provider's 429 pause is still running. The pause lasts for the provider's `retry-after`, 1 to 60 seconds. - After two failures in a row, later turns skip the route for 30 seconds. A turn that moves can miss the cache one time. - On a follow-up turn, the gateway first waits up to 5 seconds for a full pool. See [Waiting for the first route](#waiting-for-the-first-route). - If you send a session id, the gateway gives it to each provider in that provider's own field. See [Sessions and caching](#sessions-and-caching). Claude Code's `x-claude-code-session-id` header counts, with no change on your side. Rules for a high cache hit rate: 1. Keep the start of the prompt byte-identical: the same system prompt, the same tool definitions in the same order, and earlier messages unchanged. Only append. 2. Put content that changes (the time, ids, documents retrieved for this turn) after the stable part, at the end. 3. Keep the model and the `provider` object the same for every turn of the session. 4. For Claude, put `cache_control` on the block that ends the stable part (a system, tool or message block), or send the top-level `cache_control` field for automatic caching. Both surfaces keep the marks for Anthropic: see [Cache marks](#cache-marks). On marketplace routes the cache lasts 5 minutes, and `ttl: "1h"` is removed. If you send no marks, the gateway turns on automatic caching for a request with tools or earlier turns: see [Automatic caching on Claude](#automatic-caching-on-claude). 5. OpenAI caches long repeated prompt prefixes on its own, with no request setting. 6. Measure: - Cache reads are `usage.prompt_tokens_details.cached_tokens` on the OpenAI surface and `usage.cache_read_input_tokens` on the Anthropic surface. - Cache writes are `cache_write_tokens` and `cache_creation_input_tokens`. - RouterPlus bills cache reads at the lower `cached_prompt` price, and Claude cache writes at the higher `cache_write` price. The prices are in `/api/models.json`. Python, the Anthropic SDK, with a cache mark on a long system prompt that does not change: ```python reply = client.messages.create( model="claude-sonnet-5", max_tokens=1024, system=[{ "type": "text", "text": STABLE_INSTRUCTIONS, # the same bytes on every turn "cache_control": {"type": "ephemeral"}, # cache everything up to here }], messages=history, # append only ) print(reply.usage) # cache_read_input_tokens, cache_creation_input_tokens and cost ``` The provider caches a prompt start only above a minimum length. A short system prompt shows no cache reads. ### Sessions and caching #### Session ids On `POST /v1/chat/completions` and `POST /v1/messages`, the session id is the first valid value among these inputs, in this order. Token counting and images do not use it. | Order | Input | Kind | |---|---|---| | 1 | `session_id` | Top-level body field | | 2 | `x-session-id` | Header | | 3 | `x-session-affinity` | Header | | 4 | `x-claude-code-session-id` | Header that Claude Code sends by itself | - A valid value has 1 to 256 printable ASCII characters. `prompt_cache_key`, a top-level body field, follows the same rule. - The gateway ignores an invalid value and uses the next input. An invalid value alone is never an error. `x-tm-dropped-params` names the ignored input as `session_id (invalid)`, `prompt_cache_key (invalid)`, `header:x-session-id (invalid)`, `header:x-session-affinity (invalid)` or `header:x-claude-code-session-id (invalid)`. With `"require_parameters": true`, that makes the request a 400, as any dropped field does. - Valid session inputs are never named in `x-tm-dropped-params`, and they never trigger `require_parameters`. - `prompt_cache_key` stays OpenAI's own field. It wins over the session id on OpenAI routes. - When two valid inputs disagree, the order decides. There is no error. - Codex's `session-id` header is not read. Codex calls `POST /v1/responses` and sends its conversation id as `prompt_cache_key` in the body, which reaches OpenAI unchanged. What each route receives: | Route | Field | Value | |---|---|---| | `openai` (marketplace), and `openai` and `azure` (your connections) | `prompt_cache_key` | Your `prompt_cache_key`, or else the session id. Nothing if you sent neither | | `morphllm` | Header `x-session-id` | The session id, or else your `prompt_cache_key`. Nothing if you sent neither. Morph uses it to pick the region and the worker that hold your cache | | `anthropic` and `bedrock` | Nothing | Anthropic has no session input. Its cache matches the prompt start | - Values go to the provider unchanged. A value longer than the provider accepts is replaced by `tm-` followed by 43 base64url characters: a hash of the value. - The gateway keeps no session table. The id goes to the provider, and the provider routes on it. - `prompt_cache_retention` goes unchanged to `openai` and `azure` routes, both `"in_memory"` and `"24h"`. On other routes it is dropped and named. #### End-user ids on marketplace routes On marketplace routes, the provider gets a hash in place of your end user's id. The raw values do not go to the provider. - The id is the first string among the body fields `safety_identifier`, `user` and `metadata.user_id`. - The hash is `tm-` followed by 43 base64url characters, made from your organization id and that id. Without an id, it is made from your organization id alone. - Each marketplace route gets one field: | Marketplace route | Field that carries the hash | Fields removed | |---|---|---| | `openai`, `azure` | `safety_identifier` | `user` | | `anthropic` | `metadata.user_id` | `user`, `safety_identifier` | | every other route (OpenAI-compatible), dedicated capacity included | `user` | `safety_identifier`, `metadata.user_id` | | `mistral-ai` | none: Mistral has no end-user field | `user`, `safety_identifier`, `metadata.user_id`, each named in `x-tm-dropped-params` when you sent it | - When a provider blocks an id, it blocks one user of one customer. The RouterPlus account stays open. - On your own connections, `user` and `metadata` pass unchanged. `safety_identifier` passes unchanged to `openai` and `azure` connections. It is dropped and named on `anthropic` and `bedrock` connections. - Identity fields that a route uses are never named in `x-tm-dropped-params`. Mistral's route uses none, so it names each one you sent, and `require_parameters` refuses that route. #### Cache marks The gateway never adds a cache mark that you did not send. Automatic caching, below, adds only the top-level field, and only when you sent no marks. Block and part marks (`cache_control`): | Request | Route | What happens to the marks | |---|---|---| | `/v1/chat/completions` | Anthropic direct or Bedrock | Each text part keeps its mark. A marked system part turns the system prompt into blocks. A tool message's last mark goes on its `tool_result` block. Other part keys are not forwarded | | `/v1/chat/completions` | OpenAI direct or Azure | Part marks pass unchanged. OpenAI and Azure ignore them | | `/v1/messages` | Anthropic direct or Bedrock | Block marks pass unchanged | | `/v1/messages` | OpenAI direct or Azure | Marks are removed and named, for example `messages[i].content[j].cache_control` or `tools[i].cache_control` | A top-level `cache_control` field (automatic caching), on either surface: - Anthropic direct: sent unchanged. - Bedrock: turned into a mark on the last block that can carry one, counted back from the end of the messages. If no block can carry it, it is dropped and named as `cache_control`. - OpenAI direct and Azure: dropped and named as `cache_control`. - Any route: a field that Anthropic would refuse is dropped and named as `cache_control`. Anthropic refuses: - a field other than `{"type": "ephemeral"}` with an optional `ttl` of `"5m"` or `"1h"` - a fifth mark (when four marks exist already) - a 1-hour field after a 5-minute mark - a field whose TTL differs from the mark on the last block On marketplace routes, `ttl: "1h"` is removed from every mark, so the cache lasts 5 minutes. The drop is named as `.cache_control.ttl`, for example `system[0].cache_control.ttl`, `messages[2].content[0].cache_control.ttl`, or `cache_control.ttl` for the top-level field. `ttl: "5m"` passes. Your own connections keep `ttl: "1h"`. #### Automatic caching on Claude When a request has no cache marks, the gateway adds the top-level `cache_control: {"type": "ephemeral"}` field (5 minutes) if all of these are true: - Anthropic's own API serves the request: the `anthropic` route or your own Anthropic connection. Bedrock and dedicated endpoints do not get it. - The request has no `cache_control` anywhere. - It has tools, or an assistant turn before its last message. - The prompt is at least the model's minimum cacheable size: 512 tokens on the Claude 5 models except Sonnet 5, 1,024 on Claude Sonnet 5 and Sonnet 4.5, and 4,096 on Claude Haiku 4.5. - The request does not carry the header `x-tm-auto-cache: off`. The response header `x-tm-auto-cache` is `applied`, or `skipped:` with the reason, for example `skipped:single-turn` or `skipped:client-markers`. The first turn pays a cache write (1.25 times the input price). Later turns read the prompt at the cache price. Details: [Wire compatibility](compat.md#automatic-caching-on-claude). #### Waiting for the first route On a follow-up turn (an assistant or tool message after a user message), the first route's capacity pool can be full. The gateway then waits for that pool for up to 5 seconds (an operator setting) before it moves the request to the next route. It never waits past the request's deadline. The `x-tm-affinity-wait-ms` response header gives the wait in milliseconds, only when the wait was more than 0. ## 6. Handle errors and retries Every failure has one stable class, `error_type`. Read it from `error.metadata.error_type` on the OpenAI surface, or `error.error_type` on the Anthropic surface. The `x-tm-error-code` header has it too, but browsers cannot read that header. Never parse the message text. Log the `x-request-id` header with every failure. | `error_type` | HTTP | Retry | What to do | |---|---|---|---| | `rate_limit` | 429 | Yes | Wait `retry-after` seconds, then retry. A capped wait can take a second try. If `x-tm-limit-kind` is `concurrency`, send fewer requests at once. | | `email_not_verified` | 403 | No | Stop and tell a human to open the verify link in their email. The key starts working within seconds of the click. | | `insufficient_quota` | 429 | No | Stop and tell a human to add credits or raise a spend cap. On a cap, `x-tm-cap-reset` gives the reset time. | | `upstream_error` | 5xx | Yes, once, after about 2 seconds | The gateway already tried every other provider it could. If the retry fails too, report it with the `x-request-id`. | | `upstream_error` | 4xx | No | The provider refused the request. The message has the provider's own words. | | `gateway_error` | 503 | Yes | Wait `retry-after`. The request did not reach a provider. | | `gateway_error` | 504 | Yes, once | The deadline passed while the gateway tried providers. An earlier attempt may be billed: check `GET /v1/generation?id=` first. | | `gateway_error` | 400 | No | Send one output limit, from 1 to the maximum the message names (32,768 only when no maximum is declared). | | `gateway_error` | 500 | No | Report it with the `x-request-id`. | | `model_unavailable` | 404 | No | Fix the model id, or fix your `provider` object (check it with `POST /v1/route`). | | `model_unavailable` | 502 | Yes, once | The connection to every provider failed. | | `invalid_request` | 400 | No | Fix the field that the message names. | | `context_overflow` | 400 | No | Shorten the input, or use a model with a larger context. The body has the standard shape that agents compact on: `error.code` `context_length_exceeded` on the OpenAI surface, `prompt is too long: N tokens > M maximum` on the Anthropic surface. | | `content_policy` | 400 | No | Change the prompt. The gateway never sends it to another provider. | | `auth` | 401 | No | Check the header, the key, and whether the key was disabled. A 401, 402 or 403 relayed from every provider is a problem with the RouterPlus account: report it with the `x-request-id`. | | `request_too_large` | 413 | No | Keep the body under 10 MB. | | `not_found` | 404 | No | Use a route that exists. The list is in [Errors](errors.md). | Rules: 1. Let the SDK retry. The official OpenAI and Anthropic SDKs retry 429 and 5xx responses twice by default, and they wait for `retry-after`. Keep that default. Do not add a second retry loop around it. The SDKs also retry an `insufficient_quota` response. That costs nothing, but your code must then stop. 2. There is no idempotency key. A retried request is a new request. If it reaches a provider, it is billed again. A refusal by the gateway's own limits (`x-tm-error-origin: gateway_admission`) costs nothing. 3. Never retry `insufficient_quota`, `invalid_request`, `context_overflow`, `content_policy` or `auth` in a loop. The request or the account must change first. 4. An error in the middle of a stream is final for that answer. Send the full turn again. Never join two partial answers. Python, the OpenAI SDK, reading the class: ```python from openai import APIStatusError try: reply = client.chat.completions.create(model=MODEL, max_tokens=1024, messages=messages) except APIStatusError as e: error = e.response.json().get("error") or {} kind = (error.get("metadata") or {}).get("error_type") or e.response.headers.get("x-tm-error-code") request_id = e.response.headers.get("x-request-id") if kind == "insufficient_quota": notify_operator(request_id, error.get("message")) # money or a cap: a retry cannot help raise ``` Details: [Errors](errors.md). ## 7. Limits, spend caps and your users Default limits: | Scope | Requests per minute | Tokens per minute | In flight at once | |---|---|---|---| | Trial key | 20 | 1,000,000 | 8 | | Console or identity API key | 300 | 1,000,000 | 8 | | Organization, all keys together | 300 | 1,000,000 | 8 | Rules: 1. Limit how many requests your code sends at once. By default the organization allows 8 in flight. A ninth gets 429 `rate_limit` with `x-tm-limit-kind: concurrency`. Use a semaphore or a fixed pool of workers. - To run more at once, a human first raises the organization and key limits in the console, in Billing > [Rate limits](https://demo.routerplus.com/console/limits). A key's requests per minute cannot go above its ceiling (300 for a console key). - Each marketplace provider pool also limits one organization to half of the pool's capacity. That refusal has `x-tm-limit-scope: pool`. 2. Read `x-tm-remaining-rpm` and `x-tm-remaining-tpm` on each response, and slow down before they reach zero. `GET https://demo-api.routerplus.com/v1/limits?model=` returns the limits that apply to your key. 3. While a call runs, token limits count an estimate: one token for each four bytes of the request, rounded up, 65,536 for each image or document part, plus `max_tokens` (at most 32,768 of it). An image sent inline as base64 counts its 65,536 only, not its bytes. The real count replaces the estimate when the call ends. Small requests and a realistic `max_tokens` let more calls run at once. 4. Spend caps are monthly, for each key and for the organization, on the UTC calendar month. A human sets them in the console. A cap refuses with 429 `insufficient_quota` and the `x-tm-cap-reset` header. Give each feature or customer its own key and cap, so a bug or a loop cannot spend everything. Your users: - By default, one backend key serves all your users. The gateway does not know who your users are, so enforce per-user quotas in your app. - `safety_identifier`, `user` and `metadata.user_id` identify your end user to the provider only (see [End-user ids on marketplace routes](#end-user-ids-on-marketplace-routes)). They do not identify, limit or bill anyone on the gateway. Do not put emails or names in them. - For limits that the gateway enforces per user, use the identity API. Create an `end_user` principal for each user, issue a key for it, and set limits on the principal. Your backend keeps those keys. Never give a key to the user. The identity API takes a console session, not an API key, so a human with an owner or admin role sets it up. See [Workspaces and principals](identity.md). Details: [Rate limits and spend caps](limits.md) and [Limits and capacity](admission.md). ## 8. Track cost - Every billed response has `usage.cost` in USD. It is the full charge for the request, including provider attempts that failed before the answer. Store it with the `x-request-id`, the model and your own ids, such as the user and the feature. - An Anthropic-surface stream that fails in the middle of the answer has no usage in it. Read its cost from `GET /v1/generation?id=`. - `GET /v1/usage` returns the balance and the last 20 attempts. For a key bound to its own workspace or principal, the balance fields are `null`. The console's Usage and Logs pages show all keys of the organization. - Anthropic's web search adds its fee per search to `usage.cost` (see [Pricing and billing](pricing.md#web-search)). - To check a charge, multiply the token counts by the prices in `/api/models.json`. The gateway rounds the final charge down. Details: [Pricing and billing](pricing.md) and [GET /v1/usage and /v1/generation](api-usage.md). ## 9. Verify the integration One cheap call shows the answer, the cost and the headers that the gateway adds: ```bash curl -sS -i https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"gpt-4o-mini","max_tokens":16,"messages":[{"role":"user","content":"Reply with OK"}]}' ``` Then confirm that the ledger recorded it: ```bash curl -s -H "Authorization: Bearer $TM_API_KEY" https://demo-api.routerplus.com/v1/usage ``` The integration is done when: 1. The rules in [The short version](#the-short-version) hold in the code. 2. The key is not in the repository, in a client bundle or in a log. 3. Every model id in the configuration appears in `GET /v1/models`. 4. One streamed and one non-streamed test call return `usage.cost`, and both appear in `GET /v1/usage`. 5. The `x-tm-dropped-params` header is absent, or it names only fields that you accept losing. 6. `x-tm-provider` shows the route that you expect. ## Leave instructions for the next agent Add this block to the `AGENTS.md` or `CLAUDE.md` file of the repository that you integrated, so later coding sessions keep the integration correct: ```markdown ## LLM calls: RouterPlus gateway - Every LLM call goes through the RouterPlus gateway. Full guide: https://demo.routerplus.com/docs/agent-integration.md - OpenAI SDK base URL: https://demo-api.routerplus.com/v1. Anthropic SDK base URL: https://demo-api.routerplus.com. Key: the TM_API_KEY environment variable, on the server only. - Model ids live in configuration. Each one must exist in https://demo.routerplus.com/api/models.json. Never invent or alias an id. - Send max_tokens on every request (1 to the model's max_output_tokens in models.json; 32768 only when none is declared). Never send n above 1. - Claude models: /v1/messages, with cache_control on the stable start of the prompt. GPT models: /v1/chat/completions. - Keep the start of the prompt byte-identical across turns. Only append. - Send one id per conversation as the x-session-id header (or session_id in the body). Providers then keep its cache warm. - Provider pins go in the request body "provider" object (only, ignore, order, allow_fallbacks, require_parameters). Check a pin with POST https://demo-api.routerplus.com/v1/route. - Errors: switch on error_type. rate_limit: wait retry-after, then retry; a capped wait can take a second try. insufficient_quota: stop and tell a human. Never retry invalid_request, context_overflow or content_policy. - No idempotency key: a retry is a new, billed request. Add no retry loop on top of the SDK's own retries. - At most 8 requests in flight per organization, unless the limit was raised. - Log x-request-id and usage.cost for every call. - Not available: moderation. POST /v1/responses serves OpenAI models; Claude and the other models are a preview there. On it, store is always false and previous_response_id is a 400. ``` ## Steps only a human can do - Complete browser signup through Clerk, including email verification, then buy credits in Billing. Signup and card verification alone do not add credit. - Fix billing when `insufficient_quota` appears: add credits, or raise a spend cap in the console. - Sign in to the console to create keys, set spend caps and change limits. See [Console](/docs/console). - Add your own provider keys (BYOK), save routing policies, and set up workspaces and end-user principals. - Approve large test runs. Every call is billed. ## Where next - [Quickstart](/docs/quickstart) - the first streamed call, step by step - [Errors](/docs/errors) - every error class, and what to do about it - [Routing policies](/docs/routing-policies) - the provider object, saved policies and BYOK routes - [Streaming](/docs/streaming) - frame order, keep-alives and failures in the middle of an answer - [Rate limits & spend caps](/docs/limits) - the numbers behind every 429 - [Wire compatibility](/docs/compat) - which fields pass, which translate and which drop --- Source: https://demo.routerplus.com/docs/console.md # Console The console at [https://demo.routerplus.com/console](https://demo.routerplus.com/console) is where you manage your organization: keys, credits, limits, team, governance and the build tools. Sign in with the email you verified at signup. The sidebar shows the pages in the groups below. If you are a member of two or more organizations, pick one in the **Organization** menu at the top of the sidebar. Each page shows and changes only that organization. The balance card at the bottom of the sidebar opens Credits. ## Overview, Usage, Analytics and Logs | Page | What you do there | Docs | |---|---|---| | [Overview](https://demo.routerplus.com/console) | See spend per day, where it went and your recent requests. A new account sees its first steps: verify the email, create a key, make one call. | [Quickstart](/docs/quickstart) | | [Usage](https://demo.routerplus.com/console/usage) | See requests, input and output tokens, success rate and spend for the last 24 hours, 7 days or this month, grouped by model, provider or key. | [GET /v1/usage](/docs/api-usage) | | [Analytics](https://demo.routerplus.com/console/analytics) | Chart daily spend over 7, 30 or 90 days by model, provider, workspace, member, key or request label. Read the spend forecast and the savings table. | [Management API](/docs/api-management#activity) | | [Logs](https://demo.routerplus.com/console/logs) | Search and filter the requests of the last 30 days by outcome. Open one request to see its attempts and its cost. Logs hold metadata only. | [Data policy](/docs/data-policy) | ## Access | Page | What you do there | Docs | |---|---|---| | [API keys](https://demo.routerplus.com/console/keys) | Create, copy and disable keys. Set a monthly spend cap on a key. See each key's last use and spend this month. | [Authentication](/docs/authentication) | | [Security](https://demo.routerplus.com/console/security) | Find risky keys: no spend limit, no expiry, idle for 30 days, expiring within 7 days or no IP allowlist. Disable a key from its row. | [Management API](/docs/api-management#security-report) | | [Grants](https://demo.routerplus.com/console/grants) | Request, approve, reject and cancel time-boxed exceptions to a guardrail budget, a key limit or a guardrail's model rules. | [Guardrails](/docs/guardrails#grants) | | [Provider connections](https://demo.routerplus.com/console/byok) | Connect your own provider account (BYOK) and use it through your RouterPlus key. Your provider bills you directly. | [Bring your own key](/docs/byok) | | [Integrations](https://demo.routerplus.com/console/integrations) | Connect your Braintrust account so Optimize can read your evals. | [Optimize](/docs/model-search) | ## Billing | Page | What you do there | Docs | |---|---|---| | [Credits](https://demo.routerplus.com/console/billing) | See your balance and buy credits. Read the statement, set up auto top-up and set the organization's monthly spend cap. | [Pricing & billing](/docs/pricing) | | [Rate limits](https://demo.routerplus.com/console/limits) | See the gateway limits in effect. Set organization, key and model limits, and manage quota pools for your provider connections. | [Rate limits & spend caps](/docs/limits) | ## Team | Page | What you do there | Docs | |---|---|---| | [Members](https://demo.routerplus.com/console/members) | See each member's organization role, workspace roles, status and last sign-in. The owner invites members, changes roles and removes members. | [Workspaces and principals](/docs/identity) | | [Workspaces](https://demo.routerplus.com/console/workspaces) | See each workspace's spend this UTC month and its guardrail budgets. Create and manage workspaces. | [Workspaces and principals](/docs/identity) | | [Single sign-on](https://demo.routerplus.com/console/sso) | Add OIDC and SAML connections, verify email domains with DNS records, and create or rotate the SCIM token. | [Single sign-on](/docs/sso) | | [Audit log](https://demo.routerplus.com/console/audit) | Filter changes to governance, identity, connections and routing policies, and the sign-in log. Download the selection as CSV. | [Management API](/docs/api-management#audit-log) | ## Governance | Page | What you do there | Docs | |---|---|---| | [Guardrails](https://demo.routerplus.com/console/guardrails) | Create guardrails, assign them, and see the effective policy of one key or member. | [Guardrails](/docs/guardrails) | | [Budgets](https://demo.routerplus.com/console/budgets) | See every budget with this period's spend, percent used and reset time. Put a budget on a workspace or a member, and set the email, Slack or webhook channels for alerts. | [Guardrails](/docs/guardrails#budgets) | | [Guardrail events](https://demo.routerplus.com/console/guardrail-events) | See the requests a guardrail blocked, redacted or flagged. Filter by action, key, guardrail and time. | [Guardrails](/docs/guardrails#activity) | | [Presets](https://demo.routerplus.com/console/presets) | Create versioned request templates and copy their `@preset/` ids. | [Management API](/docs/api-management#presets) | ## Build | Page | What you do there | Docs | |---|---|---| | [MCP servers](https://demo.routerplus.com/console/mcp) | Register MCP servers, set each server's tool policy, and see its recent tool calls. | [MCP gateway](/docs/mcp-gateway) | | [Routers](https://demo.routerplus.com/console/routers) | Create custom routers, train them on labeled examples, and test which model a router picks for a prompt. | [Auto router](/docs/auto-router#custom-routers) | | [Evals](https://demo.routerplus.com/console/evals) | Create an eval, follow its progress, read results per model and per case, and cancel it. | [Evals](/docs/evals) | | [Exports and alerts](https://demo.routerplus.com/console/exports) | Send request data to webhook, OTLP, Langfuse, Datadog, ClickHouse, S3, Azure Blob, BigQuery or Snowflake. Set notification channels for budget alerts. | [Broadcast](/docs/broadcast) | What you can change on a page depends on your role. Each docs page above gives the rules for its feature. --- Source: https://demo.routerplus.com/docs/models.md # Models & catalog The catalog is one flat list of model ids. Every **chat** model works with one key on both APIs: OpenAI-compatible `/v1/chat/completions` and Anthropic-compatible `/v1/messages`. Image models answer only on `POST /v1/images/generations`, and decision models only on `POST /v1/decisions`. You pay the per-token prices in the catalog, and every billed response has `usage.cost`. | Surface | URL | Auth | For | |---|---|---|---| | Model directory | `https://demo.routerplus.com/` | none | Browsing, filters, featured model order | | Per-model pages | `https://demo.routerplus.com/models/` | none | Providers with prices, latency, uptime and data policy; snippets to copy | | Machine-readable JSON | `https://demo.routerplus.com/api/models.json` | none | Scripts, dashboards, price checks | | SDK-shaped list | `https://demo-api.routerplus.com/v1/models` | API key | `client.models.list()`. See [GET /v1/models](/docs/api-models) | ## Model ids Ids are exact strings. RouterPlus does no fuzzy matching and no substitution. An id that is not in the catalog is a 404 that names the id you sent. The table shows the catalog at the time of writing. The live list is [`/api/models.json`](https://demo.routerplus.com/api/models.json). | Model id | Providers | Context | Input /1M | Cached /1M | Cache write /1M | Output /1M | |---|---|---|---|---|---|---| | `claude-opus-5-5` | anthropic, deepinfra-labs, inference-net-chat | 1M | $4 | $0.2 | $5 | $20 | | `claude-fable-5-1` | anthropic | 1M | $10 | $0.25 | $12.5 | $50 | | `claude-sonnet-5-5` | anthropic, deepinfra-labs | 1M | $2 | $0.1 | $2.5 | $10 | | `claude-haiku-5-5` | anthropic | 1M | $0.1 | $0.01 | $0.125 | $0.5 | | `gpt-6-astra` | openai, inference-net-chat | 1.05M | $10 | $1 | $12.5 | $50 | | `gpt-6.1-sol` | openai | 1.05M | $2 | $0.1 | $2.5 | $10 | | `gpt-6-luna` | openai, inference-net-chat | 1.05M | $0.1 | $0.01 | $0.125 | $0.5 | | `gpt-5.6-terra` | openai, inference-net-chat | 1.05M | $2 | $0.2 | $2.5 | $12 | | `gpt-6-sol` | openai | 1.05M | $2 | $0.2 | $2.5 | $10 | | `gpt-5.6-sol` | openai, inference-net-chat | 1.05M | $4 | $0.4 | $5 | $20 | | `gpt-5.6-luna` | openai, inference-net-chat | 1.05M | $0.2 | $0.02 | $0.25 | $1.2 | | `claude-sonnet-5` | anthropic | 1M | $2 | $0.2 | $2.5 | $10 | | `claude-opus-5` | anthropic, deepinfra-labs, inference-net-chat | 1M | $5 | $0.5 | $6.25 | $25 | | `claude-fable-5` | anthropic, deepinfra-labs, inference-net-chat | 1M | $10 | $1 | $12.5 | $50 | | `claude-sonnet-4-5` | anthropic | 200K | $3 | $0.3 | $3.75 | $15 | | `claude-haiku-4-5` | anthropic, deepinfra-labs, inference-net-chat | 200K | $1 | $0.1 | $1.25 | $5 | | `gpt-4o-mini` | openai, inference-net-chat | 128K | $0.15 | $0.075 | – | $0.6 | | `gpt-4.1-mini` | openai, inference-net-chat | 1.05M | $0.4 | $0.1 | – | $1.6 | | `gpt-4o` | openai, inference-net-chat | 128K | $2.5 | $1.25 | – | $10 | | `deepseek/deepseek-v4.1-flash` | runinfra-chat, doubleword-chat, wafer, inference-net-chat, morphllm, deepinfra-chat, nebius-chat, novita-chat | 1.05M | $0.3 | $0.006 | – | $1.2 | | `tencent/hy4-preview` | deepinfra-chat, novita-chat | 1.05M | $0.834 | $0.042 | – | $2.501 | | `z-ai/glm-5.3-flash` | wafer, sail-research-chat, inference-net-chat, runinfra-chat, morphllm, deepinfra-chat, nebius-chat, novita-chat | 1.05M | $0.15 | $0.015 | – | $0.5 | | `z-ai/glm-5.3` | wafer, inference-net-chat, sail-research-chat, runinfra-chat, morphllm, deepinfra-chat, mistral-ai, nebius-chat | 1.05M | $1.4 | $0.06 | – | $4.4 | | `deepseek/deepseek-v4-flash-0731` | sail-research-chat, doubleword-chat, deepinfra-chat, nebius-chat, streamlake, chutes-chat, inference-net-chat | 1.05M | $0.14 | $0.015 | – | $0.28 | | `moonshotai/kimi-k3` | chutes-chat, sail-research-chat, wafer, morphllm, deepinfra-chat, nebius-chat, novita-chat, inference-net-chat | 1.05M | $3 | $0.285 | – | $15 | | `z-ai/glm-5.2` | doubleword-chat, wafer, chutes-chat, inference-net-chat, morphllm, deepinfra-chat, novita-chat, streamlake | 1.05M | $1.4 | $0.26 | – | $4.4 | | `minimax/minimax-m3` | deepinfra-chat, nebius-chat, streamlake | 524K | $0.3 | $0.056 | – | $1.2 | | `minimax/minimax-m3.1-flash` | streamlake | 1M | $0.15 | $0.015 | – | $0.6 | | `deepseek/deepseek-v4-pro` | deepinfra-chat, novita-chat, streamlake, inference-net-chat | 1.05M | $1.6 | $0.135 | – | $3.2 | | `deepseek/deepseek-v4-pro-0813` | sail-research-chat, deepinfra-chat, nebius-chat, novita-chat, streamlake, doubleword-chat | 1.05M | $1.32 | $0.1 | – | $3.96 | | `google/gemma-4-31b-it` | deepinfra-chat, novita-chat, sail-research-chat, doubleword-chat | 262K | $0.2 | – | – | $0.4 | | `qwen/qwen3.6-27b` | chutes-chat, deepinfra-chat, streamlake | 262K | $0.32 | – | – | $3.2 | | `qwen/qwen3.6-35b-a3b` | deepinfra-chat, doubleword-chat, streamlake | 262K | $0.1 | $0.1 | – | $0.95 | | `xiaomi/mimo-v2.6-pro` | deepinfra-chat, novita-chat | 1.05M | $0.435 | $0.0036 | – | $0.87 | | `xiaomi/mimo-v2.6-flash` | deepinfra-chat, novita-chat | 1.05M | $0.14 | $0.0028 | – | $0.28 | | `tencent/hy3` | novita-chat | 262K | $0.14 | $0.035 | – | $0.58 | | `claude-opus-4-8` | deepinfra-labs, inference-net-chat | 1M | $5 | – | – | $25 | | `claude-opus-4-7` | deepinfra-labs, inference-net-chat | 1M | $5 | – | – | $25 | | `claude-sonnet-4-6` | deepinfra-labs, inference-net-chat | 1M | $3 | – | – | $15 | | `google/gemini-3.7-flash` | deepinfra-labs, inference-net-chat | 1M | $0.75 | – | – | $3.75 | | `google/gemini-3.5-flash` | deepinfra-labs, inference-net-chat | 1M | $1.5 | – | – | $9 | | `google/gemini-3.1-pro` | deepinfra-labs | 1M | $2 | – | – | $12 | | `google/gemini-3.1-flash-lite` | deepinfra-labs, inference-net-chat | 1M | $0.25 | – | – | $1.5 | | `google/gemini-2.5-pro` | deepinfra-labs, inference-net-chat | 1M | $1.25 | – | – | $10 | | `google/gemini-2.5-flash` | deepinfra-labs, inference-net-chat | 1M | $0.3 | – | – | $2.5 | | `openai/gpt-oss-120b` | deepinfra-chat | 131K | $0.037 | – | – | $0.17 | | `openai/gpt-oss-20b` | deepinfra-chat | 131K | $0.03 | – | – | $0.14 | | `morph/morph-v3-fast` | morphllm | 262K | $0.8 | – | – | $1.2 | | `morph/morph-v3-large` | morphllm | 262K | $0.9 | – | – | $1.9 | | `morph/kimi-k3-fast` | morphllm | 1.05M | $6 | $0.6 | – | $22.5 | | `mistral-large-4` | mistral-ai | 1.05M | $1.36 | $0.14 | – | $4.18 | | `mistral-medium-2604` | mistral-ai | 262K | $1.5 | $1.5 | – | $7.5 | | `mistral-small-2603` | mistral-ai | 262K | $0.15 | $0.15 | – | $0.6 | | `mistral-large-2512` | mistral-ai | 262K | $0.5 | $0.5 | – | $1.5 | | `ministral-14b-2512` | mistral-ai | 262K | $0.2 | $0.2 | – | $0.2 | | `ministral-8b-2512` | mistral-ai | 262K | $0.15 | $0.15 | – | $0.15 | | `ministral-3b-2512` | mistral-ai | 131K | $0.1 | $0.1 | – | $0.1 | | `codestral-2508` | mistral-ai | 256K | $0.3 | $0.3 | – | $0.9 | | `qwen/qwen3.5-397b-a17b` | chutes-chat, nebius-chat, streamlake | 262K | $0.6 | $0.6 | – | $3.6 | | `qwen/qwen3-235b-a22b-2507` | nebius-chat | 262K | $0.2 | $0.2 | – | $0.6 | | `qwen/qwen3.8-27b` | runinfra-chat, chutes-chat, wafer, nebius-chat, doubleword-chat, streamlake | 262K | $0.45 | $0.45 | – | $3 | | `nvidia/nemotron-3-ultra-550b-a55b` | nebius-chat, doubleword-chat | 1.05M | $1 | $1 | – | $3 | | `nvidia/nemotron-3.5-lightning` | nebius-chat, runinfra-chat | 1.05M | $0.06 | $0.06 | – | $0.24 | | `moonshotai/kimi-k2.6` | chutes-chat, nebius-chat, streamlake, inference-net-chat | 262K | $0.95 | $0.95 | – | $4 | | `claude-opus-4-6` | inference-net-chat | 1M | $5 | $0.5 | – | $25 | | `deepseek/deepseek-v4-flash` | streamlake, inference-net-chat | 1M | $0.23 | $0.0028 | – | $0.62 | | `google/gemini-2.5-flash-lite` | inference-net-chat | 1.05M | $0.1 | $0.01 | – | $0.4 | | `google/gemini-3-flash-preview` | inference-net-chat | 1.05M | $0.5 | $0.05 | – | $3 | | `google/gemini-3.1-pro-preview` | inference-net-chat | 1.05M | $2 | $0.2 | – | $12 | | `google/gemini-3.5-flash-lite` | inference-net-chat | 1.05M | $0.3 | $0.03 | – | $2.5 | | `google/gemini-3.6-flash` | inference-net-chat | 1.05M | $1.5 | $0.15 | – | $7.5 | | `google/gemma-3-4b-it` | inference-net-chat | 131K | $0.05 | – | – | $0.1 | | `z-ai/glm-5` | inference-net-chat, streamlake | 205K | $0.95 | $0.2 | – | $2.55 | | `z-ai/glm-5.2-fast` | inference-net-chat | 1M | $2.6 | $0.4 | – | $8.8 | | `gpt-4.1` | inference-net-chat | 1.05M | $2 | $0.5 | – | $8 | | `gpt-4.1-nano` | inference-net-chat | 1.05M | $0.1 | $0.025 | – | $0.4 | | `gpt-5-mini` | inference-net-chat | 400K | $0.25 | $0.025 | – | $2 | | `gpt-5-nano` | inference-net-chat | 400K | $0.05 | $0.005 | – | $0.4 | | `gpt-5.4` | inference-net-chat | 1.05M | $2.5 | $0.25 | – | $15 | | `gpt-5.5` | inference-net-chat | 1.05M | $5 | $0.5 | – | $30 | | `x-ai/grok-4.5` | inference-net-chat | 500K | $2 | $0.3 | – | $6 | | `x-ai/grok-4.6` | inference-net-chat | 500K | $2 | $0.5 | – | $6 | | `moonshotai/kimi-k2.5` | inference-net-chat | 262K | $0.8 | $0.13 | – | $3.95 | | `moonshotai/kimi-k3-fast` | inference-net-chat | 1.05M | $4.5 | $0.45 | – | $22.5 | | `meta-llama/llama-3.2-1b-instruct` | inference-net-chat | 60K | $0.027 | – | – | $0.201 | | `meta-llama/llama-3.2-3b-instruct` | inference-net-chat | 131K | $0.05 | – | – | $0.33 | | `meta-llama/llama-3.3-70b-instruct` | inference-net-chat | 131K | $0.13 | – | – | $0.4 | | `nvidia/nemotron-3-super-120b-a12b` | inference-net-chat | 262K | $0.102 | – | – | $0.48 | | `qwen/qwen3-14b` | inference-net-chat | 131K | $0.2275 | – | – | $0.91 | | `qwen/qwen3-30b-a3b` | inference-net-chat | 41K | $0.12 | – | – | $0.5 | | `qwen/qwen3.7-plus` | inference-net-chat | 1M | $0.4 | $0.08 | – | $1.6 | | `inference-net/schematron-v2-small` | inference-net-chat | 128K | $0.05 | $0.05 | – | $0.23 | | `inference-net/schematron-v2-turbo` | inference-net-chat | 128K | $0.03 | $0.03 | – | $0.15 | | `deepseek/deepseek-v4.1-flash-1017` | sail-research-chat, wafer, runinfra-chat, doubleword-chat, particle | 1.05M | $0.15 | $0.01 | – | $0.6 | | `ornith-ai/ornith-1.5-122b` | runinfra-chat | 262K | $0.26 | $0.07 | – | $2.08 | | `ornith-ai/ornith-1.5-35b-a3b` | runinfra-chat | 262K | $0.1 | $0.01 | – | $0.4 | | `google/gemma-4-31b-it-nvfp4` | chutes-chat, sail-research-chat | 262K | $0.14 | $0.07 | – | $0.4 | | `google/gemma-4-12b-it` | sail-research-chat | 16K | $0.3 | $0.15 | – | $2 | | `deepseek/deepseek-v3.2` | chutes-chat | 131K | $1 | $0.1 | – | $1 | | `z-ai/glm-5.1` | chutes-chat | 203K | $0.9 | $0.09 | – | $2.9 | | `qwen/qwen3-vl-235b-a22b-instruct` | doubleword-chat | 262K | $0.21 | $0.1155 | $0.2625 | $1.9 | | `qwen/qwen3.5-35b-a3b-dottxt` | doubleword-chat | 262K | $0.35 | $0.19999 | $0.4375 | $1.2 | | `particle-ai/particle-1` | particle | 1.05M | $0.25 | $0.03 | – | $1 | ### Provider order The Providers column lists, cheapest first, the routes that a request with no provider named can use. It has at most eight routes. [Provider routing](/docs/provider-routing) describes how RouterPlus picks among them. - A provider past those eight can still be chosen with `provider.only` or `provider.order`. These providers are past the eight: `novita-chat`, `streamlake` and `doubleword-chat` on `z-ai/glm-5.3`; `streamlake`, `doubleword-chat` and `particle` on `z-ai/glm-5.3-flash`; `streamlake` on `deepseek/deepseek-v4.1-flash`; `doubleword-chat` on `moonshotai/kimi-k3`. - The column order is not the order RouterPlus tries. By default RouterPlus orders a model's providers by Routing Cost, the lowest cost per good answer, and providers inside the competitive band share traffic. Before a provider has measurements, the order is cheapest first. `provider.order` or `provider.sort` in the request overrides the default. See [Provider routing](/docs/provider-routing#default-order-lowest-cost-per-good-answer). - The Claude models have two providers at the same price: Anthropic and DeepInfra (`deepinfra-labs`). - On the open models that Morph and DeepInfra both serve, Morph comes before DeepInfra when their prices are equal. On GLM 5.3, Mistral follows both at that same price. - Chutes (`chutes-chat`) is first on Gemma 4 31B NVFP4, Kimi K2.6, Kimi K3, Qwen3.5 397B A17B and Qwen3.6 27B. It is second on GLM 5.2 (after `doubleword-chat`) and Qwen3.8 27B (after `runinfra-chat`). A request routed to Chutes is billed Chutes's own price, which on those seven is below the row's list price. - On DeepSeek V4 Flash 0731, Chutes costs more than the row and is sixth, after `streamlake` at the same price. - On DeepSeek V4 Flash, `streamlake` is first, at its own price, below the row's. [`/api/models.json`](https://demo.routerplus.com/api/models.json) lists every provider of a model and its price. ### Model notes - **Featured order.** The first eleven rows lead the Models page and the playground in this order. - **MiniMax on StreamLake.** On StreamLake, `minimax/minimax-m3` and `minimax/minimax-m3.1-flash` accept text only. Those routes also refuse requests above 512,000 bytes of encoded JSON, because larger inputs can need a different rate. MiniMax M3 image requests can use its other providers. M3.1 Flash has no other provider, so an image request returns a 400 before RouterPlus calls the provider. - **Qwen3.6 35B A3B on StreamLake.** StreamLake serves `qwen/qwen3.6-35b-a3b` with internal reasoning off, to keep output within the requested token limit. A requested reasoning effort other than `none` appears in `x-tm-dropped-params`. With `provider.require_parameters: true`, that request cannot use this route. - **Gemma 4 31B NVFP4.** `google/gemma-4-31b-it-nvfp4` has the same weights as `google/gemma-4-31b-it`, quantized to NVFP4 (NVIDIA's 4-bit floating-point format), so it has its own row. Its 262K context is the largest of its routes. Chutes, tried first, holds 131,072 tokens. A longer prompt goes to `sail-research-chat`, which holds 262,144. - **GPT-6 and GPT-5.6.** RouterPlus calls OpenAI's seven GPT-6 and GPT-5.6 models on OpenAI's Responses API, where they take function tools and custom (free-form) tools. You can call them on either chat API, or on [POST /v1/responses](/docs/api-responses). On these seven, the chat APIs drop `stop`, `n`, `seed`, `logprobs`, `logit_bias` and the penalties. An output limit under 16 tokens is sent as 16. - **Default sampling only.** The Claude 5 models (Opus, Fable, Sonnet and Haiku) and OpenAI's GPT-6 and GPT-5.6 models take only their default sampling. Their providers refuse `temperature` other than 1, and any `top_p` or `top_k`, with a 400. RouterPlus leaves these out for those models on every route. `x-tm-dropped-params` names each drop, except `temperature: 1` and `top_p: 1`, which change nothing. Under `require_parameters`, a drop is a 400. - **Long prompts.** OpenAI bills a prompt over 272K input tokens at twice the input rate and 1.5 times the output rate. Anthropic bills a Claude Haiku 5.5 prompt over 100K tokens at five times its rate. The catalog lists the base rate. - **Your deployed models.** A model or mix you deploy from [Optimize](/docs/model-search) gets its own id, `tm/-v`. Call it on `POST /v1/chat/completions` with a key of the organization that owns it. It is not listed in the catalog. ### Fast Apply (code merge) `morph/morph-v3-fast` and `morph/morph-v3-large` merge an edit into a file. They are chat models on `POST /v1/chat/completions` that take one user message with the instruction, the original file and the edit, each in its own tag. The edit can skip unchanged lines with `// ... existing code ...` (or `# ...` in Python). The reply's `content` is the complete merged file. ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \ -d '{"model":"morph/morph-v3-fast", "messages":[{"role":"user","content":"Add an optional third number\ndef total(a, b):\n return a + b\ndef total(a, b, c=0):\n # ... existing code ...\n return a + b + c"}]}' ``` ### Image models Image models bill in tokens and answer only on [POST /v1/images/generations](/docs/api-images). The per-image ceiling is the most output tokens one image can bill. The balance hold is `n` times the ceiling. | Model id | Sizes | Qualities | Per-image ceiling | Text in /1M | Image out /1M | |---|---|---|---|---|---| | `gpt-image-1` | `1024x1024 · 1536x1024 · 1024x1536` | `low · medium · high` | 6,240 | $5 | $40 | | `gpt-image-1-mini` | `1024x1024 · 1536x1024 · 1024x1536` | `low · medium · high` | 6,500 | $2 | $8 | | `gpt-image-1.5` | `1024x1024 · 1536x1024 · 1024x1536` | `low · medium · high` | 6,500 | $5 | $32 | | `gpt-image-2` | `1024x1024 · 1536x1024 · 1024x1536` | `low · medium · high` | 8,232 | $5 | $30 | | `gpt-image-2.5-flare` | `1024x1024 · 1536x1024 · 1024x1536` | `low · medium · high · xhigh · max` | 8,232 | $5 | $30 | | `gpt-image-2.5-sunburst` | `1024x1024 · 1536x1024 · 1024x1536` | `low · medium · high · xhigh · max` | 8,232 | $5 | $30 | `size` and `quality` also accept `auto`, which lets the provider choose. Each model's full descriptor map is in `/api/models.json` under `supported_parameters`. ### Decision models Decision models answer only on [POST /v1/decisions](/docs/api-decisions). They write no text. They return one typed answer for each question about a state. Each model takes its own question format. | Model id | Made by | Providers | Question kinds | Input /1M | Cached /1M | |---|---|---|---|---|---| | `typesafe/jev-1.13` | TypeSafe | `typesafe` | choice, score, yes/no (System One) | $0.042 | – | | `bespokelabs/nimble-v4` | Bespoke Labs | `bespokelabs` | choice, score, yes/no (System One, the same as Jev) | $0.04 | – | | `bespokelabs/nimble-v4-max` | Bespoke Labs | `bespokelabs` | choice, score, yes/no (System One, the same as Jev) | $0.09 | – | | `cloudflare/clef` | Cloudflare | `workers-ai` | choice, score, yes/no (System One, the same as Jev) | $0.24 | – | | `cloudflare/clef-flash` | Cloudflare | `workers-ai` | choice, score, yes/no (System One, the same as Jev) | $0.09 | – | | `routerplus/decider-2b` | RouterPlus | `routerplus` | choice, score, yes/no (System One, the same as Jev) | $0 | $0 | | `routerplus/kev-4b` | RouterPlus | `routerplus` | choice, score, yes/no (System One, the same as Jev) | $0 | $0 | | `perplexity/pplx-decider-v1-27b` | Perplexity | `perplexity-decisions` | choice, score, yes/no (System One, the same as Jev) | $0.04 | – | | `openai/gpt-6-luna-decisions` | OpenAI | `openai-decisions` | choice, score, yes/no (System One, the same as Jev) | $0.10 | – | | `levanto/sage-1.2` | Levanto | `levanto` | choice, score, yes/no (System One, the same as Jev) | $0.05 | – | | `scaledown/classify-1` | ScaleDown | `scaledown` | choice, score, yes/no (System One, the same as Jev) | $0.05 | – | | `fastino/gliner-2.5-decide` | Fastino | `fastino` | classifications (single- and multi-label), entities, structures, relations | $0.03 | – | Output is free on every decision model. A discount can apply to decision models: see [Pricing](/docs/pricing). **Bespoke Nimble v4 and v4 Max** - Nimble v4 is $0.04 per million input tokens, and Nimble v4 Max is $0.09. These are Bespoke's own prices with no markup. - Nimble v4 is the faster, lower-cost model. v4 Max is Bespoke's most accurate model. - Both have a 262,144-token context for each question's prompt, including the state, and take 1 to 64 questions per call. - They share one pool, which takes 7 requests at once (5 per organization). Bespoke allows the RouterPlus account 8, and RouterPlus keeps one for its test environment. - The gateway also accepts `bespokelabs/nimble-latest`, which the table does not list. Bespoke picks the model that answers it (Nimble v4 at the time of writing), at $0.04 per million input tokens. **Clef and Clef-flash** - Cloudflare's own decision models. Cloudflare Workers AI serves them at Cloudflare's own price with no markup: $0.24 and $0.09 per million input tokens. - They take 1 to 64 questions per call, each question id 1 to 100 letters, digits, `_`, `.` or `-`, and a context of 65,536 tokens. - They share one pool: 200 requests a minute (150 per organization) and at most 8 at a time (6 per organization). **Decider 2B and Kev 4B** - RouterPlus's own models, on RouterPlus GPUs in the United States, and free: $0 in and out. - They share one pool. Their contexts are 25,600 and 8,192 tokens. - RouterPlus answers an identical request from its cache for 600 s. **Perplexity Decider v1 27B** - Perplexity's own decision model, served by Perplexity's API at Perplexity's own price with no markup: $0.04 per million input tokens. - Perplexity bills the state once per question, so the cost grows with the number of questions. - It takes 1 to 128 questions per call, and 262,144 tokens for each question's prompt. - Its pool takes 540 requests a minute (405 per organization) and 8 at once (6 per organization). **GPT-6 Luna Decisions** - OpenAI's own decision model, served by OpenAI's API at OpenAI's own price with no markup: $0.10 per million input tokens. - OpenAI's API uses its own format. RouterPlus translates your System One body to it, and translates the answers back. - OpenAI bills the state once per call, and each question adds about 150 input tokens. - It takes 1 to 200 questions per call, 2 to 255 options in a choice, and a context of 272,000 tokens. - Its pool takes 24,000 requests a minute (18,000 per organization) and 64 in flight at once (48 per organization), so your organization's own limits apply first. - OpenAI's own name for it, `gpt-6-luna`, is the RouterPlus id of OpenAI's chat model GPT-6 Luna. OpenAI can decline one question and answer the rest: that answer's `type` is `refusal`. **Sage** bills the state once per question, as Perplexity Decider does. **ScaleDown Classify-1** - ScaleDown's Jev-compatible decision model on its `/v1/scaledown` endpoint. - It costs ScaleDown's own price with no markup: $0.05 per million input tokens, output free, the price of every ScaleDown model. - It runs each question as its own call, so input grows with the number of questions. - Its `state` can also carry `document` (a base64 image or PDF) with `document_mime_type`, which ScaleDown reads by OCR. - Its context is 1,000,000 tokens. ### Cache pricing Most models also have cache pricing: `cached_prompt` (the Cached column above), and `cache_write` where the provider bills it (the Cache write column). Each model's page and `/api/models.json` have the full prices per SKU. A model with no separate cache or reasoning price bills those tokens **at the base rate**. A missing SKU never means free. ### Task models (ScaleDown) ScaleDown's two chat models each do one task. Call them on `POST /v1/chat/completions` (or `POST /v1/messages`). The answer's content is a JSON string with the task's result: | Model id | Task | Input /1M | Cached /1M | Output /1M | Context | |---|---|---|---|---|---| | `scaledown/extract-1` | Fields and entities from the last user message, defined by a `response_format` of type `json_schema` (each property is a field; its `description` is the hint) | $0.05 | – | $0 | 1M | | `scaledown/summarize-1` | An abstractive summary of the last user message; the system message holds instructions, and `max_tokens` is honored | $0.05 | – | $0 | 1M | - Output is free. These models bill input tokens only, and report 0 output tokens. - ScaleDown answers in one piece. With `stream: true` you still get a stream (role and content, the finish, usage with its cost, `[DONE]`). The stream starts when the complete answer is ready. - To classify with ScaleDown, use `scaledown/classify-1` on [POST /v1/decisions](/docs/api-decisions). - Both models also read an image or a PDF. On `/v1/chat/completions`, put one `image_url` part (a base64 `data:` URL) or one `file` part (base64 `file_data`) in the last user message, with a text part next to it. ScaleDown reads the attachment by OCR. A web URL, more than one attachment, or an attachment with no text is a 400 that says which. ### One model, several providers More than one provider can serve a model id. The catalog shows one card. The model page lists every provider with its own uptime and TTFB. **Routing** - By default, RouterPlus orders providers by the estimated cost of one good answer. Until providers have measurements, the cheapest goes first. See [Provider routing](/docs/provider-routing). - `provider.order` comes first. `provider.sort: "price"` orders providers by input price plus output price per million, at each live price, and also overrides a routing policy's saved order. `provider.max_price` leaves out a provider above your limit. - RouterPlus fails over **before the first byte** only, never in the middle of an answer. - A provider that fails twice in a row, or leaves two requests in a row without an answer, rests for 30 seconds (see [Routing](/docs/routing#health-circuits-and-cooldowns)). **What you pay** You pay the price of the provider that serves your request: the one you name with `provider.only`, else the one routing picks, failover included. - Providers that do not set their own price share the model's list price. The catalog build enforces this. - A provider reached through an aggregator is charged at the aggregator's listing price, which is the model's list price. Its row on the model page can also show the host's own published price, labelled "Host's list price". You do not pay that price. **Prices on the pages** - The pages use only the prices that you pay. - The model page shows each provider's live price, the price in force now, without scheduled changes. Its bold cells mark the lowest input, cached input and output price, each in its own column. - The model page's Cheapest sort ranks providers by input price plus output price, the same rule as `provider.sort: "price"`. - `/api/models.json` also lists each provider's scheduled changes in `scheduled_pricing`, at most 20 per provider. A revision that leaves the price unchanged is not listed. - In the directory, a decision model's price is the cheapest price you can get by naming a provider. It is labelled "from" when it is below the price of the provider that the page lists first. The model page's "from" price names the same provider. - The directory's price sort, price filter and "from" price rank providers by input price, then by output price. On one model page, the "from" price can therefore name a provider that the Cheapest sort does not put first. #### Self-serve price changes A self-serve provider sets its own price and can change it without a release. - Every price change starts at a 5-minute mark (10:00, 10:05, 10:10 UTC), at least 2 minutes after the change is written. A change written at 10:02:30 starts at 10:05. All gateways switch at the same time. - A decrease starts at the next mark. An increase starts only after notice. - A turn-off of a model, or a lower limit, applies at once. The pages hide the model at that provider and show the lower limits. Only a limit that the listing declares, on a chat model, can be lowered. - A turn-off has no price, so it is never an increase and needs no notice. - A turn-on is compared with the last price you could pay while the model was on. When it is higher, it is an increase, and the provider must schedule the turn-on at least the notice period ahead. - A model that no provider serves now, but that one provider turns back on later, has a page that says it is not served now. It is not in the directory until it is back. An operator can freeze a self-serve provider. While it is frozen: - You pay its listing's own price from the manifest files, and the pages show that price. - `/api/models.json` lists its scheduled turn-offs and turn-ons, but not its scheduled price changes. - A model that it turned off stays hidden, and lower limits stay in force. - When an operator unfreezes it and accepts its increases, they can apply at once. Other exceptions: - For a provider with a price feed, the site schedules a fed increase at the earliest for the notice period plus 5 minutes after it reads the feed. That can be later than the time the provider publishes. - An operator can apply an increase without notice. Each such change is audited. **How fresh the pages are** | What | Delay | |---|---| | Pushed prices | The site reads them every 30 s | | A page with a self-serve provider | Cached at the edge for up to 60 s (`s-maxage=60`), plus up to 60 s while it refreshes (`stale-while-revalidate=60`), never past the next 5-minute mark | | A pushed change in `/api/models.json` | Listed as scheduled within about 2.5 minutes. The pages switch at its mark | | A pushed increase in `/api/models.json` | Listed for its full notice period before it starts, also with a turn-on | | Manifest files (a release) | Up to 60 s more, because the site reads them once a minute. A changed list price starts at the next 5-minute mark and shows as scheduled until then | | Any other page | Changes only with a release. The edge can serve a copy up to about 11 minutes old | After the site starts, no page is cached until its first read. The gateway charges the price in force when your request arrives, whatever a cached page says. A price change applies at its 5-minute mark. A turn-off or a lower limit applies within about 30 s, when the gateway next reads the prices. #### Who served the request - `x-tm-provider` names the provider that served you. - `x-tm-upstream-model` has the id sent to the provider, when it knows the model under its own id. The response's `model` field still names the model you asked for. - `x-tm-served-by` names the provider that ran the request, when a route reports one. **Inference.net.** `inference-net-chat` is an ordinary provider at its own price, also on models other providers serve. Routing tries it first where it is the cheapest (GLM 5.3 Flash, for example), else as a fallback. Name it in `provider.only` or `provider.order` (`"inference-net"` works too) to send a request there first. You pay Inference.net's price, which the model page shows on its row. ```json { "model": "moonshotai/kimi-k2.6", "messages": [...], "provider": { "only": ["inference-net-chat"] } } ``` ### Date-pinned ids The catalog lists **one id per model** (`claude-haiku-4-5`). Vendors also ship dated snapshots (`claude-haiku-4-5-20251001`, `gpt-4o-2024-08-06`) and `-latest` spellings, and tools such as Claude Code send them. - A snapshot or `-latest` id of a catalog model works without being listed. RouterPlus resolves it to its family **for routing and pricing only**. - RouterPlus forwards your original id to the provider unchanged. It never moves a snapshot pin to a newer version. - The response's `model` field shows the snapshot that served you. - If a vendor retires a pinned snapshot, you get the vendor's own error. RouterPlus does not upgrade the pin. - Unknown ids, dated or not, return a 404 that names the id. - A key bound to your own provider connection matches ids literally, so list the exact ids you send there. ### Claude names for Cursor Cursor sends a model name that starts with `claude-` to Anthropic, never to a custom base URL. Each Claude model therefore also answers to a second name: `rp-` and its id without `claude-`. | Name | Model | |---|---| | `rp-opus-5-5` | `claude-opus-5-5` | | `rp-sonnet-5-5` | `claude-sonnet-5-5` | | `rp-haiku-5-5` | `claude-haiku-5-5` | | `rp-fable-5-1` | `claude-fable-5-1` | | `rp-opus-5` | `claude-opus-5` | | `rp-sonnet-5` | `claude-sonnet-5` | | `rp-fable-5` | `claude-fable-5` | | `rp-sonnet-4-5` | `claude-sonnet-4-5` | | `rp-haiku-4-5` | `claude-haiku-4-5` | - Each name is one model, billed at the price of the provider that serves it, as for the model's own id. - A name takes no date pin. `rp-sonnet-4-5-20250929` is a 404. - The provider receives the model's own id. - The answer's `model` field has the name you sent. A Claude stream on `/v1/messages` is relayed as Anthropic sent it, so its first event names the model's own id. - Billing, `/v1/usage` and `/v1/generation` record the model's own id. - `GET /v1/models` lists each name right after its model, with `canonical_slug` set to the model's id, so model pickers that read it show the names. - A key bound to your own provider connection or to a routing policy takes ids literally, so these names do not work on it. In your own code, use the catalog ids. ### Id grammar Ids match `[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}`: letters, digits, `.` `_` `/` `-`, at most 128 characters. `:` is not allowed, so an id has no suffix. ## Providers are manifests A provider lists its models in a JSON manifest: its endpoint, wire format, settlement terms, data policy, and a model list with prices. [List your models](/docs/for-providers) is the full reference. ```json { "provider": { "id": "anthropic", "name": "Anthropic (direct API)", "prompt_logging": "retained" }, "endpoint": { "base_url": "https://api.anthropic.com/v1", "dialect": "anthropic" }, "models": [{ "id": "claude-haiku-4-5", "context_length": 200000, "max_output_tokens": 64000, "pricing": [ { "type": "prompt", "unit": "token", "cost_usd_per_million": "1" }, { "type": "completion", "unit": "token", "cost_usd_per_million": "5" } ] }] } ``` What this means for you as a buyer: - A model is listed only after it passes a live conformance suite against the provider's endpoint. The suite checks real usage, well-formed streams, non-empty answers, a 4xx with no fallback for an unknown model, and that the response names the model you asked for. A dated snapshot of the same id, or a target the manifest declares in `resolves_to`, passes. `gpt-4o` answered by `gpt-4o-mini` fails. - `is_ready: false` stages a model: it is never routed or listed. After its `deprecation_date`, a model leaves the catalog and the public pages. - Providers that are neither self-serve nor opt-in (`"routing": "opt_in"`) must agree on a shared model id's price and kind, or the catalog build fails. They share the model's list price. Self-serve and opt-in providers each charge their own price. - When other providers also serve a model, an opt-in provider serves it only to a request that names that provider. - Prices are stored as effective-dated, append-only rows. Old requests keep the price they were dispatched at. A changed list price starts at the first 5-minute mark (UTC) at least 2 minutes after the release writes it. A new model's first price applies at once. ## The directory [`https://demo.routerplus.com/`](https://demo.routerplus.com/) lists every live model in three sections: chat, image and decision. Each kind is called on a different route. - A row shows the model's context length and max output (for a chat model), or its sizes and qualities (for an image model). - Directory rows and model pages show no request counts for any model, including newly added models. - The default decision order starts with Jev 1.13, GPT-6 Luna Decisions, Nimble v4 Max, ScaleDown Classify-1 and Nimble v4. - Search with `/?q=haiku`. It matches model, lab and provider names. - Filter by kind, context length, supported parameters, zero data retention, input price, lab and provider. - Sort by use, name, price or context. Each model's page at `/models/` shows the values that depend on the provider. It has one row per provider, with prices per SKU, latency, uptime and the provider's prompt-retention policy, and snippets to copy. See [Data policy](/docs/data-policy) for how RouterPlus handles request data. ### Uptime Uptime and latency come from the last 24 hours of routed traffic. When a provider has fewer than 100 counted attempts in that window, the model page shows a dash (**-**) in place of a percentage. Failures you cause (your 400s, your cancelled streams) do not count against a provider. ## Machine-readable: /api/models.json The catalog as JSON, with no auth: ```bash curl -s https://demo.routerplus.com/api/models.json ``` ```json { "models": [ { "id": "claude-haiku-4-5", "display_name": "Claude Haiku 4.5", "output_modalities": ["text"], "providers": [ { "id": "anthropic", "name": "Anthropic (direct API)", "dialect": "anthropic", "prompt_logging": "retained", "pricing_usd_per_million": { "prompt": "1", "cached_prompt": "0.10", "cache_write": "1.25", "completion": "5" } } ], "served_by": [ { "id": "anthropic", "name": "Anthropic", "zdr": false, "routes": [ { "via": "anthropic", "direct": true } ] } ], "context_length": 200000, "max_output_tokens": 64000, "pricing_usd_per_million": { "prompt": "1", "cached_prompt": "0.10", "cache_write": "1.25", "completion": "5" }, "lab": { "id": "anthropic", "name": "Anthropic" }, "weights": "proprietary" } ], "gateway": "https://demo-api.routerplus.com/v1", "docs": "https://demo.routerplus.com/docs" } ``` | Field | Meaning | |---|---| | `id` | The exact string to put in your request's `model` field | | `output_modalities` | `["text"]`, `["image"]` or `["decisions"]`: which route serves this model. Filter on it. Never guess from the id | | `providers` | Every deployment serving this id, primary first, each with `id`, `name`, `dialect`, `prompt_logging`, its price, and `upstream_id` when the provider knows the model under its own name | | `pricing_usd_per_million` (per provider) | What you pay when this provider serves the request, live: decimal-string USD per 1M tokens, keyed by SKU type. Absent for a `reported_cost` listing (see `price_card`) | | `price_revision` / `price_effective_from` (per provider) | Only for a self-serve provider with a pushed price in force for this model: the revision number and when it took effect (ISO 8601, UTC). Absent when the revision in force does not have this model, and while the provider is frozen: the listing's own price from the manifest files applies then | | `scheduled_pricing` (per provider) | Only when a change is scheduled: `[{effective_from, rises, pricing_usd_per_million}]` in time order. `rises` is `true` when any SKU goes up against the last price you could pay before it. An entry with `enabled: false` turns the model off: it has no `pricing_usd_per_million`, and its `rises` is always `false`. A later entry without `enabled` turns the model on again at its price. Its `rises` compares that price with the last one you could pay while the model was on, and the notice period applies when it rises. While the provider is frozen, only turn-offs and turn-ons are listed, at the price you pay then | | `enabled` (per provider) | Only `false`: the provider has the model off now, and a turn-on is in its `scheduled_pricing`. Such an entry comes after the providers that serve the model, has no price, `price_revision` or `opt_in`, and is not in `served_by`. A provider that has the model off with no turn-on scheduled is not listed | | `enabled` | Only `false`: no provider serves the model now, but one turns it back on later (see its `providers` entries). A request for it now that no BYOK route serves is answered as for an unknown model. Such an entry has no top-level `pricing_usd_per_million`, and `served_by` is empty | | `dialect` (per provider) | The provider's **native** wire format, for information. Every chat model answers on both APIs | | `prompt_logging` (per provider) | Provider's prompt retention: `none` or `retained` | | `served_by` | The providers that run the model, one entry per provider. `routes[].via` is the `providers` entry the request goes to; `direct: true` means the request goes to the provider's own API. `zdr` is `true` when the provider offers Zero Data Retention for this model | | `context_length` / `max_output_tokens` | Token limits; `max_output_tokens` may be `null`, and `context_length` is `null` for image models | | `supported_parameters` | Image models only: the descriptor map the route enforces (`size`, `quality`, `n`, and the rest) | | `pricing_usd_per_million` | The primary provider's live price (what a request that names no provider pays first): decimal-string USD per 1M tokens, keyed by SKU type. Other providers can charge another price: see each `providers[]` entry | | `lab` | Who made the model, which can differ from who serves it. `null` when unknown | | `weights` | `"open"` when the lab publishes the model's weights, `"proprietary"` when it never has, `null` when RouterPlus is not certain | For an image model, `max_output_tokens` is the **per-image ceiling**. It does not cap a completion. Prices are decimal **strings**, so float rounding loses nothing. Parse them with a decimal type. For the authenticated, SDK-shaped list (what `client.models.list()` calls), see [GET /v1/models](/docs/api-models). --- Source: https://demo.routerplus.com/docs/migration.md # Migrate Most migrations change the base URL and the API key. If you come from another gateway, you also change the model ids. You keep your SDK, because RouterPlus serves both common API formats: - OpenAI-compatible: `POST https://demo-api.routerplus.com/v1/chat/completions`, and `POST https://demo-api.routerplus.com/v1/responses` for OpenAI's Responses API - Anthropic-compatible: `POST https://demo-api.routerplus.com/v1/messages` Every chat model works on both APIs. RouterPlus translates requests, streams and errors in both directions. Image models use [POST /v1/images/generations](/docs/api-images). The translation rules are on [Wire compatibility](/docs/compat). Every route accepts the key as `Authorization: Bearer $TM_API_KEY` or `x-api-key: $TM_API_KEY`. > [!NOTE] > To get a key, open [Sign up](https://demo.routerplus.com/signup), complete Clerk authentication > and email verification, and save the first key shown after sign-in. Buy credits > in [Billing](https://demo.routerplus.com/console/billing). Signup does not add free credit. If you have an account, > [sign in](https://demo.routerplus.com/login) with the same verified email and create a key at > [API keys](https://demo.routerplus.com/console/keys). There is no programmatic signup route. ## From the OpenAI SDK Change two lines. `gpt-*` model ids are the provider's own ids (`gpt-4o-mini`, `gpt-4.1-mini`, `gpt-4o`), so your model strings usually stay the same. ```diff from openai import OpenAI client = OpenAI( - api_key=os.environ["OPENAI_API_KEY"], + base_url="https://demo-api.routerplus.com/v1", + api_key=os.environ["TM_API_KEY"], ) ``` ```diff import OpenAI from "openai"; const client = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, + baseURL: "https://demo-api.routerplus.com/v1", + apiKey: process.env.TM_API_KEY, }); ``` The same client can call Claude models: ```python client.chat.completions.create( model="claude-sonnet-4-5", # Anthropic model, OpenAI format stream=True, messages=[{"role": "user", "content": "hello"}], ) ``` > [!NOTE] > Output limits: > - On the chat APIs, a request with no `max_tokens` or `max_completion_tokens` gets the > model's own maximum output. RouterPlus lowers it when needed so the input and output > fit the model's context window. > - On the Responses API, RouterPlus adds no limit, and OpenAI applies the model's own maximum. > - While such a request runs, the balance hold counts at most 32,768 output tokens. You pay > for the real usage. > - A limit that you send (`max_tokens` on the chat APIs, `max_output_tokens` on the > Responses API) is held in full. Code on OpenAI's Responses API (`client.responses.create`) works with the same base URL for OpenAI's models. Claude and the other chat models are a preview there. See [POST /v1/responses](/docs/api-responses). **Codex** (the CLI, the desktop app and the IDE extension) calls the Responses API. Add a provider block to `~/.codex/config.toml`: ```toml model = "gpt-6-luna" model_provider = "routerplus" [model_providers.routerplus] name = "RouterPlus" base_url = "https://demo-api.routerplus.com/v1" env_key = "TM_API_KEY" wire_api = "responses" ``` Then run `codex` with `TM_API_KEY` set. For the desktop app, which needs the key in the file, see [Coding agents](/docs/install#codex). ## From the Anthropic SDK Change the base URL and the key. The Anthropic-compatible API is at the gateway root. Do not add `/v1` to `base_url`: the SDK appends `/v1/messages`. ```diff from anthropic import Anthropic client = Anthropic( - api_key=os.environ["ANTHROPIC_API_KEY"], + base_url="https://demo-api.routerplus.com", + api_key=os.environ["TM_API_KEY"], ) ``` Claude model ids are Anthropic's own (`claude-sonnet-4-5`, `claude-haiku-4-5`, dated snapshots included). This SDK can also call GPT models on the same `/v1/messages` endpoint. **Claude Code** needs environment variables only: ```bash ANTHROPIC_BASE_URL=https://demo-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY claude ``` - RouterPlus forwards your `anthropic-beta` header when an Anthropic-format provider serves the request. - It keeps only allowlisted betas that do not change the price of a token: `prompt-caching`, `claude-code`, `interleaved-thinking`, and the values Claude Code needs on the Claude 5 models. The full list is on [Authentication](/docs/authentication). - The `x-tm-dropped-params` header names each dropped value. - Claude Code's `?beta=true` query string is accepted and routes normally. RouterPlus does not forward query strings to the provider. For Claude Code's output limit, auto mode and advisor, see [Coding agents](/docs/install#output-limit-auto-mode-and-the-advisor). ## From another gateway If your code calls another multi-provider gateway in the OpenAI format, change the client as in [From the OpenAI SDK](#from-the-openai-sdk). Then change the model ids. Usage comes in the last stream chunk with a `cost` field. The optional `HTTP-Referer` and `X-Title` attribution headers also work. ```diff completion = client.chat.completions.create( - model="anthropic/claude-sonnet-4.5", + model="claude-sonnet-4-5", stream=True, messages=[{"role": "user", "content": "hello"}], ) ``` | Before | RouterPlus | |---|---| | `author/model` slugs (`anthropic/claude-sonnet-4.5`) | Provider ids (`claude-sonnet-4-5`). The full list is at [https://demo.routerplus.com/api/models.json](https://demo.routerplus.com/api/models.json) | | Variant suffixes (`model:variant`) | None. An id with a suffix is not in the catalog and returns 404 | | `usage.cost` in credits | `usage.cost` in USD, computed with the ledger's integer math. It covers the full request debit, including attempts that failed over | | A usage lookup by generation id | `GET https://demo-api.routerplus.com/v1/generation?id=REQUEST_ID` returns every attempt with its tokens, outcome and settled cost | | Out of credits: HTTP 402 | HTTP 429 `insufficient_quota`, the response OpenAI's SDKs already handle | | A `models` fallback array with `route: "fallback"` | Supported: up to five models, tried in order. See [Model fallback](/docs/model-fallback) | | `transforms` | Dropped. The `x-tm-dropped-params` header names it. Remove it | | A `provider` object | Supported keys: `order`, `only`, `ignore`, `allow_fallbacks`, `require_parameters`, `sort`, `max_price`, `quantizations`, `preferred_min_throughput`, `preferred_max_latency`, `zdr`, `data_collection`, `enforce_distillable_text`, and the connection controls on [Routing policies](/docs/routing-policies). `sort` takes `"price"`, `"throughput"`, `"latency"` or `"quality"`. Other keys return a 400. See [Provider routing](/docs/provider-routing) | > [!WARNING] > RouterPlus never substitutes a model. An id that is not in the catalog returns a 404 > that names the id you sent. Check every migrated model id against > `https://demo.routerplus.com/api/models.json` before you ship. Failover never switches providers in the middle of an answer. If a provider fails after output starts, you get the tokens streamed so far and one terminal error event. You decide whether to retry. See [Wire compatibility](/docs/compat). ## Migrate with one prompt Paste this into your coding agent: ```text Migrate this project to RouterPlus. 1. If TM_API_KEY is not set, have me open https://demo.routerplus.com/signup and complete Clerk authentication and email verification in the browser. Existing customers sign in at https://demo.routerplus.com/login and create a key at https://demo.routerplus.com/console/keys. Save the key as TM_API_KEY (env var and .env; never commit it). Continue once the key is available; there is no programmatic signup endpoint. 2. Find every LLM client in this codebase and repoint it: - OpenAI SDK / any OpenAI-compatible client: base_url -> "https://demo-api.routerplus.com/v1", api_key -> TM_API_KEY - Anthropic SDK: base_url -> "https://demo-api.routerplus.com", api key -> TM_API_KEY - Claude Code: run with ANTHROPIC_BASE_URL=https://demo-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY - Codex: add a [model_providers.routerplus] block to ~/.codex/config.toml with base_url "https://demo-api.routerplus.com/v1", env_key "TM_API_KEY", wire_api "responses", and set model_provider = "routerplus" and model = "gpt-6-luna". - Clients of another gateway: also strip "author/" prefixes and ":variant" suffixes from model ids, and remove the transforms request field. Keep a "models" fallback list (at most five) with route "fallback". Inside a "provider" object keep only: order, only, ignore, allow_fallbacks, require_parameters, sort ("price", "throughput", "latency" or "quality"), max_price (USD per million), quantizations, preferred_min_throughput, preferred_max_latency, zdr, data_collection; remove the rest. - Raw HTTP: change the host to https://demo-api.routerplus.com and the bearer/x-api-key to TM_API_KEY. 3. Check every model id against https://demo.routerplus.com/api/models.json and substitute the closest listed id where needed. Never invent an id: unlisted models return 404. 4. Run one real streamed test call per repointed client and show me each response's final usage object (it must contain "cost"). 5. Keep the diff minimal. Do not refactor unrelated code. ``` ## Verify the cutover One streamed call tests auth, routing and billing: ```bash curl -N https://demo-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":40,"messages":[{"role":"user","content":"Reply with exactly: MIGRATED"}]}' ``` The last usage chunk must have `cost` (USD). Then check that the ledger recorded the call: ```bash curl -s -H "Authorization: Bearer $TM_API_KEY" https://demo-api.routerplus.com/v1/usage ``` Use the response headers to confirm that traffic moved: - `x-tm-provider` names the deployment that served the request. - `x-tm-attempts` counts the dispatches. Every error body has a stable `error_type`. The fixes are on [Errors](/docs/errors). --- Source: https://demo.routerplus.com/docs/playground.md # Playground The Playground at [`https://demo.routerplus.com/playground`](https://demo.routerplus.com/playground) runs chat, typed decisions and images on the same catalog, routing, prices and ledger as the API. Each answer shows the provider that served it, the token counts and the cost. You can look around without an account. To send, you need one (see [Without an account](#without-an-account)). ![Playground: Cmd+K opens the model picker, a search finds DeepSeek V4.1 Flash, and one message gets a reply with its cost and provider](media/playground.gif) ## What it does - **Any catalog model.** `⌘K` / `Ctrl+K` opens the model picker. - Models of your current mode come first (chat models in Chat, decision models in Decision). - Chat starts with Claude Opus 5.5, then Claude Sonnet 5.5, then the rest, newest first. - Search matches a model's name, id, lab, providers and kind: type `decision` for every decision model, `image` for the image models, `free` for the free ones. - Each row shows the lab's mark and the price per million tokens, in / cached / out (a dash where there is no cached price). An image model shows the most one image can cost. A chat model shows its context length. - To pin a dated snapshot of a catalog model, type its id. An id that is not listed is used as typed. - **Decision order.** Decision mode lists Jev 1.13, GPT-6 Luna Decisions, Bespoke Nimble v4 Max, ScaleDown Classify-1, Bespoke Nimble v4, Clef, Clef-flash, Kev 4B, GLiNER-2.5-Decide, Decider 2B and Sage 1.2, in that order. - **Compare up to three chat models side by side** (Decision mode compares up to four). **Compare** adds a column. One prompt goes to every column. Each column streams into its own card with its own provider, tokens, latency and cost. Each column keeps its own thread: a follow-up sees that model's replies only. Removing a column leaves the rest unchanged. - **Sample prompts** under the composer send a real prompt in one click, until the conversation starts. - The first is always the **strawberry test** (how many times "r" appears in "strawberry"). Each reply says **passed**, **failed** with the count the model gave, or that it gave no clear count. - The others are random: a reasoning trap, a code task, structured JSON, a table, a six-word story and more. - **Streaming replies** with stop, regenerate, a collapsible "thinking" section for reasoning models, and safe rendering of code, lists, links and tables. A table is drawn only for a real markdown table: a header row, then a divider row such as `|---|---|` with the same number of cells. Other text with a pipe shows as written. A wide table scrolls sideways. - **Code** and **Parameters** sit to the left of Send. - **Code** shows the next request as curl, Python or Node. - **Parameters** holds a system prompt, temperature, top_p and max output tokens, per conversation. - **Cost on every reply:** the same `usage.cost` the API returns, the time to first byte, and an `audit` link to the request's ledger rows. - **Organizations:** if you belong to several, the sidebar selector sets which one the playground bills. ## Tetris Royale [Tetris Royale](https://demo.routerplus.com/playground/tetris-royale) compares nine System One decision models on the same block sequence. GPT-6 Luna Decisions plays next to Jev, Bespoke Nimble v4 Max is Bespoke's player, and ScaleDown Classify-1 plays last. Open it from the Playground mode bar. The arrow button at the top left goes back. - It bills the selected organization through its own managed key, **Tetris Royale**, held on the server. Ordinary Playground keys keep their own limit. No API key reaches the browser. - The server validates the board and match counters, then builds the decision prompt and the legal choices. The game relay accepts no custom prompts or questions. - Every request follows the organization's normal credits, limits, billing and routing. Match rules: - A match is 200 pieces by default. Settings offers 20 up to a thousand. - Each model plays on its own, so a slow or rate-limited model does not hold up the others. - A transient 429 cools down only that model. It retries up to three times and respects Retry-After. Other errors offer a retry button. - Matches in one organization share per-model pacing on the server. A busy queue for one model does not pause the others. Gateway quotas still apply. - Pause finishes current moves and stops new calls. Switching tabs pauses the match. Reset starts over. Match state lives only in the open page. Settings offers five **Block sequences**, each a repeatable order of pieces that all models share. The boards keep a fixed order: Jev, GPT-6 Luna Decisions, Perplexity, Bespoke, Clef, Clef-flash, Kev, Decider. **Standings** rank the models by score at each model's current progress. A row moves when its model passes another. Equal scores share a place. A line clear or a change of place shows for a moment. Final standings appear when every board finishes or tops out. Both move modes use a 10 × 20 board, seven-bag pieces, line-clear and level points, hard-drop points, combos, back-to-back clears and all-clear bonuses. Settings → **Moves** picks how a piece reaches its place: - **Drop and slide** (the default): the piece turns at the top, then falls. While it falls it can move left or right, so it can tuck into a gap under an overhang. It never turns while it falls. The model picks any position the piece can reach this way. Each choice says whether it is a straight drop or a tuck, and the points it scores now. The board shows the piece turn, drift toward its column and tuck. - **Drop only**: the model picks a rotation and a column, and the piece drops straight down. Neither mode has hold, wall kicks, T-spins or a gravity deadline. **Rules & scoring** gives the exact rules. ## Decision A *decision model* returns typed answers in place of text. You give it a **state** (a message, a ticket, a list of items) and typed **questions**. It returns a typed answer for each question, with the probability or confidence behind it. Use it for routing, classification and other decision points in an application. The catalog has twelve: | | Jev 1.13 | Bespoke Nimble v4 and v4 Max | Clef and Clef-flash | Decider 2B and Kev 4B | Perplexity Decider v1 27B | GPT-6 Luna Decisions | Sage 1.2 | ScaleDown Classify-1 | GLiNER-2.5-Decide | |---|---|---|---|---|---|---|---|---|---| | Catalog id | `typesafe/jev-1.13` | `bespokelabs/nimble-v4`, `bespokelabs/nimble-v4-max` | `cloudflare/clef`, `cloudflare/clef-flash` | `routerplus/decider-2b`, `routerplus/kev-4b` | `perplexity/pplx-decider-v1-27b` | `openai/gpt-6-luna-decisions` | `levanto/sage-1.2` | `scaledown/classify-1` | `fastino/gliner-2.5-decide` | | Made by | TypeSafe | Bespoke Labs | Cloudflare | RouterPlus, our own models | Perplexity | OpenAI | Levanto | ScaleDown | Fastino | | Numbers it returns | probabilities | probabilities, at full precision | probabilities, to 4 decimals | probabilities | probabilities, at full precision | probabilities, to 2 decimals | probabilities | probabilities, at full precision | confidences, not calibrated probabilities | | Price | $0.042 per million input tokens | $0.04 (v4) or $0.09 (v4 Max) per million input tokens | $0.24 (Clef) or $0.09 (Clef-flash) per million input tokens | free: our own models, at $0 | $0.04 per million input tokens, the state billed once per question | $0.10 per million input tokens, about 150 tokens for each question | $0.05 per million input tokens, the state billed once per question | $0.05 per million input tokens, each question its own call | $0.03 per million input tokens | Jev, the two Nimble models, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage and ScaleDown Classify-1 are the eleven **System One** models. On this page, "System One" means all eleven. - **System One models** use Jev's question format (System One). They take choice, rating (2 to 10 levels) and yes/no questions, and tags as one yes/no per tag. When unsure, they give their best answer, with probabilities. GPT-6 Luna Decisions can also decline a question (**Refused**). - **GLiNER-2.5-Decide** uses GLiNER's schema. It takes yes/no, choice, rating (exactly 5 levels) and tags, all as labels. When unsure, it gives its best label, with a confidence. ### Per-model limits Decision mode checks these limits before a run. It counts the questions in order, and only the ones the column will send. On Nimble, Clef and Clef-flash, a question that would cross 64 shows **Not askable on Bespoke Nimble v4** (or on the other Nimble models, or on Clef, or on Clef-flash) and stays out of that column's request. It takes none of the 64, and a later question that still fits is sent. So the provider never refuses the whole column. Perplexity Decider and GPT-6 Luna Decisions are counted the same way, against their own limits. | Model | Own limits | |---|---| | Nimble, Clef and Clef-flash | A choice needs at least 2 options. One request holds at most 64 questions, each tag counted as one. | | Clef and Clef-flash | Also: every answer key is 1 to 100 letters, digits, `_`, `.` or `-`. A question id in Decision mode always fits. A tag can break the rule: Decision mode asks each tag under the key `__`, and a long question id with a long tag id can pass 100 characters. | | Perplexity Decider v1 27B | One request holds at most 128 questions, each tag counted as one. It takes a one-option choice, as Jev does. A question that would cross 128 shows **Not askable on Perplexity Decider v1 27B**. Perplexity bills the state once for each question and each tag. | | GPT-6 Luna Decisions | A choice needs at least 2 options. One request holds at most 200 questions, each tag counted as one. A question that would cross 200 shows **Not askable on GPT-6 Luna Decisions**. Each question and each tag adds about 150 input tokens. OpenAI can decline one question and answer the rest. The column shows that answer as **Refused**, and the call is still billed. | | Decider 2B and Kev 4B | None. For Decider 2B and Kev 4B, Decision mode checks no limit of their own: probed on 2026-10-02, each answers a one-option choice and a request of 200 questions. Their columns follow the family's rules, as a Jev column does. | | ScaleDown Classify-1 | None in Decision mode. Probed on 2026-10-12, it answers a one-option choice and a request of 65 questions. It reads System One as text, so the gateway sends an option without a description as `""` and a JSON state as its JSON text. The answers mean the same. | ### The decision console In **Decision** mode, the composer becomes a decision console: a box for what the models judge, and up to 20 questions for every column. - Each question is a line of text and an answer type: **Yes / no**, **Pick one** (one of your options), **Rating** (a scale, lowest level first; a new rating starts with five levels) or **Tags** (which of your tags apply). - Options, tags and levels are rows: a short name, which the model answers with, and what it means. - You never write an answer key: Decision mode names each question from its words, and **Code** shows the names it sends. - A yes / no saved by the earlier editor with descriptions of yes and no keeps them, under the question. Only the System One models take them. **Remove descriptions** makes the question plain. An example loads by itself. The **Example** menu picks another, or **Start blank**. The examples cover checking an answer against its sources, reviewing an agent's run, catching a prompt injection, triage, bug severity, lead qualification, code review, moderation, feedback tags and a JSON state. **Run** runs it once. **Run 10 times** runs every column ten times (see [Run 10 times](#run-10-times)). ### One form for every model You write each question once. Each decision model has its own question format, so Decision mode translates the form for each column: | Question | System One (Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage, ScaleDown Classify-1) | GLiNER-2.5-Decide | |---|---|---| | Yes / no | a `noul` question | a classification: the question text is the task, the labels are `yes` and `no` | | Choice, 2 to 20 options, descriptions optional | a `choice` question: each option, with its description or none | a classification: one label per option, `"name: description"` (or `"name"`), read back as the option's name | | Rating, exactly 5 levels, low to high | a `score` question with 5 levels | a classification: the labels `"0: "` to `"4: "`, read back as the level | | Tags, 1 to 20, descriptions optional | one `noul` question per tag, asked as ` Does this apply: ?` (with the tag's description after the tag, when it has one) | a multi-label classification that returns every tag with its confidence; a tag applies at 0.5 or more | - GLiNER reads each label as text, so Decision mode puts an option's or level's description into its label. The description changes GLiNER's answer. - Decision mode asks GLiNER at threshold 0 (`cls_threshold: 0`), so GLiNER always names its top label. At GLiNER's default of 0.5, a task answers `null` when no label reaches it. Decision mode shows a `null` as Not sure. - Each tag is one question in a System One column's request. The column shows the tags together. - Decision mode builds a System One body from the catalog's record of the model's format, never from its name. ### Compare Put up to four models side by side with **Compare**: up to four of the twelve decision models, or three decision models and a chat model. A chat or image conversation stops at three. One turn cannot hold all twelve decision models. A column's chip swaps one model for another. With two or more decision models, the question builder follows the tables above: - Before the run, it marks a question that one of the chosen models cannot take, and names the models that can. Examples: a rating that does not have 5 levels (System One models only), a choice with one option (Jev, Decider 2B, Kev 4B and Perplexity Decider only: Nimble, Clef, Clef-flash and GPT-6 Luna Decisions refuse a one-option choice), more than 20 options, a question past the 64 keys of one Nimble, Clef or Clef-flash request, the 128 keys of one Perplexity Decider request or the 200 keys of one GPT-6 Luna Decisions request, and a tag whose key is longer than Clef's 100 characters. - Models refused for the same reason share one line. A Nimble, Clef, Decider 2B, Perplexity Decider or GPT-6 Luna Decisions column is named as itself, never Jev. - Each column still gets the questions its model can take. A question it cannot take shows **Not askable on** that model and stays out of that column's request. - GLiNER uses the question text as its task name, at most 256 characters. Two questions with the same text, or a question longer than 256 characters, are not askable on GLiNER. Every example uses forms that every decision model takes, the tags example included. ### Run 10 times **Run 10 times** runs the whole comparison ten times with the same state and questions. It shows one ledger in place of a card per column. Each column sends up to two requests at a time, and the ledger fills in as runs come back. The ledger shows: - For each model and question, the most common answer and in how many runs (**10/10** when it never changed). A pick-one question lists every option the runs chose, with its count. - The averages: the mean probability of yes, the mean confidence of a choice, the mean level of a rating (a marker on the track, with a faint dot for each run), and for tags how many runs each tag applied in. Bars and markers move as each run lands. - At the head of each column, the mean time per run and the cost of all runs. Each run's time is a dot on a strip, and the mean is a line. - For each row, whether the decision models agree on their most common answers (for a rating, mean levels within 0.5 of each other). Each column is billed as one request per run: ten runs of four columns are 40 requests. A column that hits a rate limit stops and says so, and the other columns go on. **Stop** stops them all. On Decider 2B and Kev 4B, the ten runs send the same request, so RouterPlus answers the repeats from its cache, which keeps an answer for 600 s. Those runs come back sooner than the first one, cost $0, and repeat the first run's answer exactly. On these models, the ten runs do not show how much the model's answer varies. The first request after a quiet period can take 15 to 20 s while RouterPlus starts the model. ### Compare view Below the columns, the compare view draws each question as one row, with one cell per column: | Question | Each cell shows | |---|---| | Yes / no | The verdict (Yes, No or Not sure) and a bar from 0 to 1: the probability of yes for System One, the confidence in the chosen label for GLiNER (labelled **confidence**), the stated answer for a chat model. | | Choice | The chosen option (or Not sure) and its probability or confidence. Per-option bars where the model returns them: a System One model's add up to 1, and GLiNER returns the chosen label only. | | Rating | One 0 to 4 track, with one marker per model (a System One score or GLiNER's chosen level), and the confidence beside it. | | Tags | A grid of tag by model: applies ✓, does not apply ✗ or not sure ?, with the score. | - Each row says whether the decision models agree: the same verdict, the same option, a level within 0.5, or the same set of tags with every tag answered. A tag that one model is not sure about cannot be compared, and the row says so. - The view shows only numbers a model returned, each labelled with what it means. - **GLiNER's numbers are confidences, not calibrated probabilities**: a GLiNER 0.9 and a Jev 0.9 mean different things. - A Clef confidence and a Jev confidence also mean different things, because Cloudflare computes Clef's in its own way. So compare their probabilities, not their confidences. - GPT-6 Luna Decisions' confidence is OpenAI's own. - A Decider 2B or Kev 4B column costs $0.00, with no list price or discount beside it: the model is free, so there is nothing to take off. ### Answer cards - A System One model's cards (Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions and Sage) show bars: the chosen option, the probability of each alternative, and the model's confidence. - A question that OpenAI declined on a GPT-6 Luna Decisions column shows **Refused**, with no bars. - GLiNER's cards show the chosen label and its confidence, and for tags each tag's confidence. A chat model in a column gets the same questions in words, tags included where the prompt can say them. When it answers in the JSON it was asked for, its answers render as the same cards and in the compare view. ### Billing for decisions A Decision run is a real API call to [`POST /v1/decisions`](/docs/api-decisions) under the playground key. - It is billed to your organization at the model's price. - It counts against the playground key's limits and the organization's. - It appears in `GET /v1/generation`. Each answer card has an `audit` link to its ledger rows. - A discount can make a run free. The cost line shows what you paid and, with a discount, the list price and the percent off. From your code, call [`POST /v1/decisions`](/docs/api-decisions) in the model's own question format. Only Decision mode translates the form. The API does not. ## Images Pick an image model and the composer describes an image. The picker marks image models **image**, with the most one image can cost. ### Size and controls - The toolbar keeps **Aspect ratio**, **Resolution / size** and **Add reference images** in the same place for every model. A control the model does not support stays visible and disabled, with an explanation. Local reference images show a preview and a remove button. - A model with a native `size` menu, such as GPT Image, shows those choices in **Resolution / size**. The aspect ratio follows the size. The request keeps the model's `size` field, with no added resolution parameter. - A model with width and height inputs offers eight canvases within its limits: 1024×1024 and 2048×2048 squares, 1536×1024 landscape, 1024×1536 portrait, 1280×720 HD, 1920×1080 Full HD, and portrait HD and Full HD. - On Krea 3, a fixed size sets the aspect ratio, and a native resolution clears custom width and height. Krea 3 rounds a requested size to a supported 1K or 2K canvas, so the output size can differ. Krea 3 Ultra uses native 2K and takes no size preset. - Other models keep resolution and aspect ratio separate from pixel size. For example, Nano Banana can keep native 4K while a preset sets width and height. - **Model default size** clears explicit dimensions and keeps the native choices. - **Advanced settings** holds the rest: image count, custom dimensions, quality, format, background, seed, thinking mode, and reference URLs or weights. Closing it keeps your values. A hidden field that needs attention opens by itself. - The **Code** dialog waits for reference files to load and asks you to fix invalid settings before it shows a request. It cannot copy an older valid value by mistake. ### Cost The estimate beside the send button is the most the render can cost: the per-image ceiling the gateway reserves. The bill is the actual cost, which is usually less. Receipts show token counts only when the provider reports nonzero counts. Models billed per image, such as Krea, show cost and render time in place of `0 in / 0 out`. ### Compare and render **Compare** puts up to three image models side by side, each with the same description and the options it accepts. A comparison holds image models only. A chat model as the lead starts a new conversation, so text and pictures never share a history. - A render usually takes 10 to 60 seconds. Some models take several minutes. Each tile shows the time elapsed. - You cannot stop a render, and the provider bills it either way. - A render continues while you use another chat. Its chat shows a dot in the list until the picture arrives. - **A render survives a reload** or a sleeping background tab. It runs on the server, and the page picks it up again. - A finished render waits 15 minutes, then is gone. It is still billed. - Each image has a **Download** button. - Saved conversations and exports leave out local reference uploads. After a reload or import, **make again** asks you to reattach them and sends a new request. It keeps the previous result and never turns an edit into a text-only generation. Provider-hosted reference URLs still work. - The sample descriptions include hard cases: exact spelling, counting, hands and a transparent background. A sample fills the description and does not render, because a render costs more than a chat reply. **Images stay in your browser**, in its IndexedDB storage, filed by your sign-in, conversation and reply, so a reload or a visit tomorrow shows them again. They are never uploaded. A render is metered the same as [`POST /v1/images/generations`](/docs/api-images), under the playground key. ## Without an account Without signing in, you can pick and compare models, switch modes, set parameters and read a request as code. - Nothing runs. Send, Generate, Run, Run 10 times and a sample prompt open the sign-in form: email and password, an email link, or Google, and **Create an account**. After it, you are back in the playground. - Your unsent prompt, models, parameters and Decision form come back in the same browser tab, for up to 30 minutes. Nothing is sent until you send it. - A signed-out playground saves no conversations, and does not show those a signed-in person saved in that browser. ## Keyboard shortcuts | Keys | What it does | |:---|:---| | `⌘⇧K` / `Ctrl+Shift+K` | Start a new chat | | `/` | Put the cursor in the message box (in Decision, the box for what the models judge) | | `⌘Enter` / `Ctrl+Enter` | Send the message, or run the Decision form | | `Enter` | Send, from the message box. `Shift+Enter` adds a new line | | `⌘K` / `Ctrl+K` | Open the model picker | | `Esc` | Close the picker or a dialog | `/` does nothing while you type in a field. ## Install as a desktop app The app opens the playground in its own window, with its own dock or taskbar icon. The console is in the same app. - **Chrome and Edge.** Open the playground. Click **Install app** in the top bar, or the install icon at the right of the address bar, and confirm. To remove it, open the app menu and choose **Uninstall**. - **Safari on macOS.** Open the playground. Choose **File** > **Add to Dock**, name it and click **Add**. The app is the same site. It shares the sign-in of the browser profile you installed it from, and updates by itself. **Offline**, the app shows an offline page. It keeps only its static files: icons, styles, scripts and that page. Saved conversations stay in the browser. Sending, the console, sign-in and every API call need the network. The app never stores API answers, console data or sign-in pages. ## How it is billed The playground meters usage the same as an API call. - The console creates one key named **Playground** per organization, tagged **managed** on [`/console/keys`](https://demo.routerplus.com/console/keys). That page hides managed keys until you turn on **Show playground keys**. - Every reply is an attempt under that key. It is charged to the organization's credits, subject to its spend caps, and shows in the console and `GET /v1/generation`. - BYOK connections bound to that key apply. - Nobody sees the key's value. The console holds it in memory and calls the gateway for you. - RouterPlus replaces the key every 24 hours and after a console restart, and retires the old one. If you disable it, the playground creates a new one on the next message. ## What is stored - Your browser's local storage holds up to 100 conversations. **Export** and **Import** move them. **Clear all** deletes them. - Your browser's IndexedDB holds images, up to 1 GB, oldest removed first. Deleting a chat, or **Clear all**, deletes its files. - Export carries conversations, not files. Download the images you want to keep. - RouterPlus handles requests per the [Data policy](/docs/data-policy). Clearing browser history does not delete server-side request logs. ## Limits - No chat attachments. Image models that accept references or editing controls show them in **Advanced settings**. - Up to three image renders in flight per organization (one per comparison column), and up to four images per render. - A request holds at most 400 000 characters, and a conversation 200 messages. A system prompt holds at most 32 000 characters. Start a new chat when a model reports a context overflow. - The managed key has its own rate limit (60 requests per minute) on top of the organization's limits. Each comparison column is one request, with its own credit hold, so on a low balance one column can be refused while another answers. Decision runs count the same way. **Run 10 times** sends ten requests per column, two at a time. - Each model shows its lab's mark in the lab's colours, or the model's own mark. An id the catalog does not list gets a neutral chip. ### Provider pools Columns served by one provider share that provider's pool. A pool has room for a full comparison (three chat models, or four Decision columns) and for other users at the same time. A comparison that exhausts it gets `rate_limit` on the later columns, with **Try again**. | Models | Pool | |---|---| | Nimble | The Nimble models share one pool, which takes 7 requests at once in all, 5 per organization: Bespoke allows our whole account 8, and one is kept for our test environment. | | Clef and Clef-flash | Clef and Clef-flash share one pool: 200 requests a minute and at most 8 at a time in all, 150 and 6 per organization. | | Decider 2B and Kev 4B | Decider 2B and Kev 4B share one RouterPlus pool, which is large (11,400 requests a minute and 59 in flight at once in all, 44 per organization), so your organization's own limits bind first. | | Perplexity Decider | 540 requests a minute and 8 in flight at once in all, 405 and 6 per organization. Perplexity allows our organization 10 requests a second. | | GPT-6 Luna Decisions | A large pool (24,000 requests a minute and 64 in flight at once in all, 18,000 and 48 per organization), so your organization's own limits bind first. | ## Errors Errors show the class and remediation from the API's [error table](/docs/errors). A `rate_limit` asks you to wait, `insufficient_quota` points you at credits, and `content_policy` is never rerouted. Every failed reply carries its request id. --- Source: https://demo.routerplus.com/docs/provider-routing.md # Provider routing Many models have more than one provider. RouterPlus picks the provider for each request. When a provider cannot serve, RouterPlus fails over to the next one before the first byte reaches you. Use the `provider` object to change the order or filter the providers. ## Request ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \ -d '{ "model": "moonshotai/kimi-k3", "messages": [{ "role": "user", "content": "Hello" }], "provider": { "sort": "price", "max_price": { "prompt": 1, "completion": 4 } } }' ``` ## Default order: lowest cost per good answer For each provider of the model, RouterPlus estimates the cost of one good answer: - the price of the model's typical request: uncached input, cached input at that provider's cache hit rate, and output; - plus the cost of waiting: time to first token and output time at the provider's measured speed; - plus the time lost to rate limits; - divided by the share of requests that succeed, with a heavy penalty for provider errors and a penalty for malformed tool calls. RouterPlus splits traffic as follows: - Providers whose cost is close to the best share the traffic. - Every provider keeps a small share, so RouterPlus keeps measuring new and recovering providers. - A provider that fails or rate-limits gets less traffic until it recovers. - Traffic above a provider's learned capacity goes to the next provider. - Until providers have measurements, the cheapest goes first. A conversation stays on the provider that served its first turn while that provider can serve. This keeps the provider's prompt cache warm. Requests that share a long prompt prefix (system prompt and tools) go to the same provider across sessions. The response header `x-tm-routing` names the strategy: `routing_cost` (the default), `price`, `throughput`, `latency` or `quality`. It adds `;pinned` or `;prefix` when a conversation pin or a shared prefix chose the provider. ## The `provider` object | Field | Effect | |---|---| | `order` | Try these providers first, in this order. Turns off the default order. | | `only` | Only these providers. | | `ignore` | Never these providers. | | `allow_fallbacks` | `false`: only the first provider, with no failover. | | `require_parameters` | Refuse the request instead of dropping a parameter a provider does not take. | | `sort` | `"price"`, `"throughput"`, `"latency"` or `"quality"`, or `{ "by": ..., "partition": "model" \| "none" }`. Turns off the default order. See [Sort](#sort). | | `preferred_min_throughput` | Tokens per second (a number means p50, or `{ "p50", "p75", "p90", "p99" }`). Slower providers go last. None is excluded. | | `preferred_max_latency` | Seconds to first token, same forms. Slower providers go last. | | `max_price` | `{ "prompt", "completion" }` in USD per million tokens, plus `request` and `image`. A provider above a cap is left out. | | `quantizations` | Only providers that serve the model at these quantizations (`int4`, `int8`, `fp4`, `mxfp4`, `nvfp4`, `fp6`, `fp8`, `mxfp8`, `fp16`, `bf16`, `fp32`, `unknown`). | | `zdr` | Only providers that keep no prompts or completions. | | `data_collection` | `"deny"`: only providers that do not train on or keep request data. | | `enforce_distillable_text` | Only `false` is accepted for now. | [Guardrails](guardrails.md) on your key apply first. A request can narrow them. It cannot widen them. ### Sort | `sort` | Order | |---|---| | `price` | Cheapest first. | | `throughput` | Highest measured tokens per second first. | | `latency` | Lowest measured time to first token first. | | `quality` | Highest share of well-formed tool calls first. Ties go by the default cost estimate. | In the object form, `partition` is `"model"` (the default) or `"none"`. Both give the same order, because RouterPlus routes one model per request. After the sort, a conversation pin or a shared prompt prefix can still choose the provider. `preferred_min_throughput` and `preferred_max_latency` then move providers that miss them to the end. ### Errors An unknown field in `provider`, or a `sort` or `partition` value outside the lists above, returns `400`. ## Your default An organization or workspace can make price its default order with `PATCH /v1/org {"default_sort": "price"}` or `PATCH /v1/workspaces/` (see the [management API](api-management.md)). A `sort` in the request wins over this default. ## Rankings and benchmarks | Endpoint | Returns | |---|---| | `GET /v1/rankings?period=day\|week\|month` | Models ranked by tokens served on RouterPlus. A model used by fewer than 3 organizations is not listed. | | `GET /v1/rankings?task=code\|reasoning\|extraction\|chat\|creative\|vision\|long_context` | Models ranked for one kind of request. RouterPlus labels each request with its kind (the `task` label). | | `GET /v1/rankings?view=sessions` | Models ranked by sessions, with the median cost and the median number of requests per session. | | `GET /v1/benchmarks/` | What the model takes (context, output limit, inputs, reasoning efforts, response formats) and how it serves now across its providers: the best latency, throughput, error rate and tool-call quality measured. | | `GET /v1/benchmarks?task=coding\|math\|reasoning&limit=20` | Catalog models ranked by a quality index. See below. | | `GET /v1/models//endpoints` | Each provider with its price, data policy, status and measurements. Its `quality` field holds the model's quality scores. | `GET /v1/benchmarks?task=...` ranks `coding` by the coding index, `math` by the math index and `reasoning` by the intelligence index. `limit` is 1 to 100 (default 20). When a model has no coding or math index, the mean of its published coding or math evaluations (out of 100) ranks it. A model with neither is not listed. ### Quality scores `GET /v1/benchmarks/` and `GET /v1/models//endpoints` carry a `quality` object: ```json { "quality": { "intelligence_index": 58.4, "coding_index": 55.0, "math_index": 90.1, "evaluations": { "gpqa": 0.9, "hle": 0.4, "livecodebench": 0.8 }, "aa_model": { "slug": "claude-opus-5-5", "name": "Claude Opus 5.5 (Max)" }, "source": "Artificial Analysis", "source_url": "https://artificialanalysis.ai/", "model_url": "https://artificialanalysis.ai/models/claude-opus-5-5", "fetched_at": "2026-10-31T09:00:00.000Z" }, "scores_status": "ready" } ``` - The scores come from [Artificial Analysis](https://artificialanalysis.ai/). RouterPlus does not run these evaluations. - `evaluations` holds every other evaluation Artificial Analysis publishes for the model, by its name. - `quality` is `null` when Artificial Analysis has no entry for the model. - `scores_status` is `ready`, `loading` or `unavailable`. `loading` means RouterPlus is fetching the scores. Ask again shortly. - Scores refresh every 6 hours. `fetched_at` gives the time of the copy in use. - When you show these scores, credit Artificial Analysis and link to https://artificialanalysis.ai/. Artificial Analysis asks for this attribution. --- Source: https://demo.routerplus.com/docs/routing.md # Routing & failover You name a model. The gateway picks a deployment that serves it, and fails over to another deployment when a provider fails. Three rules apply to every request: 1. **You get the model you name.** An unknown model is a 404 that names the id. The gateway never substitutes another model. 2. **Failover happens only before your answer starts.** Once the first token of real output reaches you, the request stays with that provider. 3. **The gateway never reroutes a content-policy refusal** to another provider. To change the provider order or filter providers, see [Provider routing](/docs/provider-routing). ## How a request picks a provider A **deployment** is a provider endpoint with a wire dialect, the model ids it serves and a routing `priority`. Today each Claude and GPT model has one deployment: the lab's own API (`anthropic` for Claude, `openai` for GPT). Routing runs in memory and makes no remote calls. Billed requests still check the ledger and the shared admission store for authentication, limits and the reserve check. 1. **Model filter.** Keep deployments that list the requested id. The match is exact, with two exceptions: - A dated snapshot of a listed id (`claude-haiku-4-5-20251001`) routes and bills as its family. The provider still gets your original id. - The `rp-` name of a Claude model (`rp-sonnet-5-5`, for Cursor) routes, bills and goes to the provider as that model. See [Models & catalog](/docs/models#claude-names-for-cursor). 2. **Health filter.** Drop deployments whose circuit is open. 3. **Order.** The base order is cheapest first: input plus output price per million tokens, at each provider's live price. - Equal prices keep ascending `priority`, then catalog order. - A route billed at the cost its provider reports (some image and video models) has no price to compare. It comes after the priced routes. - The providers that `provider.order` names go first, in its order. - The [routing strategy](/docs/provider-routing) then sets the final order. - A key under a [routing policy](/docs/routing-policies) keeps the policy's saved order. The gateway tries the list in order. The first deployment to produce real output wins. A failure that can fail over moves to the next deployment with no backoff. The gateway translates requests, streams and errors between dialects, so you can call any listed model from either surface. A key with a BYOK connection or a routing policy runs the same steps over its own routes, with the policy's order, funding rule and attempt limit on top. `POST /v1/route` shows the plan without sending the request. A [dedicated endpoint](/docs/dedicated-endpoints) (`/`) routes first to capacity reserved for your organization, then to public deployments of the same model that meet its constraints. It never routes to a different model. A [closed](/docs/dedicated-endpoints#closed-endpoints) endpoint names RouterPlus as the provider, whichever route served. ## The commit boundary **A request commits to a deployment at its first semantic output:** the first text delta, reasoning delta, tool call, refusal or usage report that reaches you. **Before commit**, failover is possible only while the gateway has written nothing to you. - A provider that fails before its first frame is skipped, and the next one is tried at once. This covers a refused connection, a 5xx, a failure before any bytes, and a miss of the 20-second headers deadline. - The gateway sends your response headers only when a live attempt starts writing. - After the headers and (on the OpenAI surface) the role-priming delta go out, the gateway does not switch providers. A later failure becomes the terminal in-stream error in [Streaming](/docs/streaming), even with no semantic output. **After commit**, the gateway never switches providers. If the provider fails mid-answer, you get the tokens it produced plus one terminal error event inside the 200 stream. You decide whether to retry. A retry starts fresh. > [!NOTE] > You pay for every settled attempt, including attempts that failover discarded before commit. > A provider that fails after it reports prompt usage still bills RouterPlus for that prompt, > and pass-through pricing passes the charge on. The inline `usage.cost` is the **full request > debit** across all attempts, so it matches the ledger. `x-tm-attempts` gives the number of > physical dispatches. ## No model substitution A model that is not in the catalog returns a 404 that names it: ```json { "error": { "code": "model_unavailable", "message": "model \"gpt-5-ultra\" is not in the catalog; GET /v1/models lists what this key can serve", "type": "invalid_request_error", "metadata": { "error_type": "model_unavailable" } }, "request_id": "…" } ``` There is no closest-match fallback and no default model. `n>1` returns a typed 400, because the gateway sends every stream as a single choice. ## What never fails over The gateway decides whether an error can fail over once, when it normalizes the error. Routing never reads raw provider errors. - **`content_policy`** never retries and **never** goes to another provider. This check runs before every status-code rule. Error metadata on this path never echoes your flagged input. To try a different provider, send a new request. - **`context_overflow`** never fails over, because every deployment of a model has the same context window. Shorten the request or choose a model with a larger context. - Other buyer-fault 4xx errors, such as a malformed request, return directly. ## Failover eligibility by error class | Upstream signal | `error_type` | Retryable | Fails over | |---|---|---|---| | 429 (Retry-After honored, capped 60s) | `rate_limit` | yes | yes | | 401 / 402 / 403 from the provider | `auth` | no | **yes** | | Context / length errors | `context_overflow` | no | no | | Content policy / refusal shapes | `content_policy` | no | **never** | | 404 / model_not_found upstream | `model_unavailable` | no | yes | | 5xx / 529 / overloaded | `upstream_error` | yes | yes | | Other 4xx | `upstream_error` | no | no | > [!NOTE] > A 401, 402 or 403 **from a provider** is a problem with RouterPlus's account at that > provider. The gateway routes around the deployment and counts the error against that > provider's health. You see an `auth` error only if every candidate failed. ## Health circuits and cooldowns Each deployment has its own circuit breaker: - **Two failures in a row open the circuit** for a 30-second cooldown. Routing skips the deployment while the circuit is open. - **After the cooldown the circuit goes half-open.** One live request goes through as a probe. Success closes the circuit. Failure opens it again with a new cooldown. - **Only failures that can fail over count.** Buyer-fault errors (400/413-class, content policy, context overflow) never count. - An upstream 429 counts against the circuit. Uptime scoring excludes it as buyer-caused. It also pauses that provider's admission pool for the provider's `retry-after`, 1 to 60 seconds. - A client cancellation does not count. - **A hanging provider counts as a failure.** This applies when the provider sends no response headers for 60 seconds (20 seconds on a stream) and the request then ends, by the gateway's deadline or your cancellation. - The attempt counts on the deployment's circuit and on a second circuit for that model at that deployment. - Two such attempts in a row pause the provider for that model for 30 seconds. Its other models keep answering. Then one request probes it. Public uptime numbers use the same rules. A model page shows no uptime percentage for a provider RouterPlus calls directly until it has 100 counted requests in the window. ## When everything is cooling down If every deployment of your model has an open circuit, the gateway refuses the request at once, with no queue. The response is a `503` with: - `retry-after: 5` - `x-tm-error-code: gateway_error` - `x-tm-limit-kind: health` - the message `all deployments cooling down` A cooldown lasts 30 seconds, then one request goes through as the probe. Retry after a few seconds. ## Reading what happened Every response says how it was served: | Header | Meaning | |---|---| | `x-request-id` | your handle for audit and support (always present) | | `x-tm-provider` | the deployment that served the request; `routerplus` on a closed dedicated endpoint | | `x-tm-served-by` | the provider that ran the request, when a route reports one; absent on a closed dedicated endpoint | | `x-tm-upstream-model` | the id sent to the provider, when it differs from the one you sent; absent on a closed dedicated endpoint | | `x-tm-attempts` | physical dispatches, failovers included | | `x-tm-upstream-status` | the HTTP status the provider returned | | `x-tm-dropped-params` | request fields the gateway removed before dispatch, by name | | `x-tm-route-plan-id` | the plan id, for correlation with `/v1/route` | | `x-tm-dedicated-endpoint`, `x-tm-route-role` | on a dedicated endpoint: its id, and whether your dedicated capacity (`primary`) or the shared pool (`fallback`) served | | `x-tm-queue-ms` | on a dedicated endpoint with a [paced limit](/docs/dedicated-endpoints#the-paced-limit): how long the request waited for its turn, in milliseconds | | `x-tm-remaining-rpm`, `x-tm-remaining-tpm` | what your rate limits had left at admission | | `x-tm-error-code` | canonical error class, on failures | | `x-tm-error-origin` | on failures: `upstream`, `gateway_admission`, `upstream_quota`, `gateway_infrastructure` or `authorization` | | `retry-after` | on rate limits and cooldowns; whole seconds, at most 59 (`usage_limited` sends 60 or 3,600) | On failures the body carries the canonical class: `error.metadata.error_type` on the OpenAI surface, `error.error_type` on the Anthropic surface. Each dialect's native type string is there too, so your SDK's retry rules keep working. The gateway never relays a provider's raw error body. See [Errors & remediation](/docs/errors) for the full table. ### Per-attempt audit The generation audit endpoint returns every physical attempt for a request id: which deployments the gateway tried, in what order, and what each one cost. ```bash curl -s -H "Authorization: Bearer $TM_API_KEY" \ "https://demo-api.routerplus.com/v1/generation?id=" ``` In this trimmed example, a Claude request under a `byok_with_house_fallback` policy failed on your own Anthropic connection, then succeeded on the marketplace `anthropic` deployment. Each attempt also carries its billing source, price snapshot, error origin, admission and route context, and dropped parameters. ```json { "request_id": "2f6d8e3b-…", "admission_events": [], "attempts": [ { "attempt": 1, "deployment": "byok:5f3a9c1e-…:0b7d42e8-…", "model": "claude-sonnet-5", "outcome": "failed", "usage_provenance": "unknown", "tokens": { "input": 0, "cached": 0, "cache_write": 0, "output": 0, "reasoning": 0 }, "reserved_max_usd": 0, "cost_usd": 0, "error_code": "upstream_error", "dispatched_at": "2026-09-04T09:12:44.120Z" }, { "attempt": 2, "deployment": "anthropic", "model": "claude-sonnet-5", "outcome": "completed", "usage_provenance": "observed", "tokens": { "input": 412, "cached": 0, "cache_write": 0, "output": 638, "reasoning": 0 }, "reserved_max_usd": 0.04, "cost_usd": 0.007204, "error_code": "", "dispatched_at": "2026-09-04T09:12:45.010Z" } ] } ``` The response holds metadata only. See [Data policy](/docs/data-policy). | Field | Values | |---|---| | `outcome` | `completed`, `failed`, `cancelled`, `incomplete` (failed after your answer started) or `unknown_after_crash` (settled at the reserved maximum until reconciled) | | `admission_events` | Refusals before any attempt, such as a rate limit | Use this endpoint to recompute an Anthropic-surface stream that failed mid-answer (see [Streaming](/docs/streaming)). --- Source: https://demo.routerplus.com/docs/model-fallback.md # Model fallback Model fallback tries up to five models in your order until one answers. It works on `POST /v1/chat/completions` and `POST /v1/messages`, streamed or not. ## Request Send `models` next to `model`: ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-5-5", "models": ["gpt-6-luna", "z-ai/glm-5.3"], "messages": [{ "role": "user", "content": "Hello" }] }' ``` | Field | Rule | |---|---| | `models` | An array of 1 to 5 model ids. | | `model` | Tried first. Without `model`, the first entry of `models` goes first. | | `route` | Optional. The only value is `"fallback"`. | - The gateway removes duplicates and keeps your order. - Each model gets your request with its own id in `model`. All other fields go as you sent them. - Each model first fails over between its own providers (see [Routing & failover](/docs/routing)). The gateway tries the next model only after every provider of the current model failed. ## When the next model is tried The gateway moves to the next model only before any part of an answer reaches you. | Response from a model | What happens | |---|---| | 2xx | You get this answer. The request stays with this model. | | 429, 404, 5xx, other 4xx | The gateway tries the next model. | | 400 because the model cannot take the request | The gateway tries the next model. This covers context length, a model that is not available, an unsupported parameter, an input type (for example images) and `response_format`. | | Any other 400 | You get this error. The request itself is wrong, so another model would fail the same way. | | 401, 403 | You get this error at once. This covers a bad key and a guardrail refusal. | | 413 | You get this error at once. The request is too large for every model. | When every model fails, you get the last model's error unchanged. The gateway does not switch models after a stream starts. A failure in the stream ends it with the error chunk in [Streaming](/docs/streaming). ## Response The response is the answering model's own answer, and `model` names that model. The usual headers (`x-request-id`, `x-tm-provider`, `x-tm-attempts` and the others) come from that model's call. The gateway adds two headers: | Header | Meaning | |---|---| | `x-tm-fallback-model` | The model that answered. | | `x-tm-fallback-tried` | How many models were tried, the answering model included. Also set on an error. | ## Billing RouterPlus bills each model call as a direct call to that model. A model that failed before it produced output costs what its provider billed for the attempt, as in [Routing & failover](/docs/routing#the-commit-boundary). ## Errors | Status | Cause | |---|---| | 400 `invalid_request` | `models` is not an array of 1 to 5 non-empty strings, or `route` is not `"fallback"`. | --- Source: https://demo.routerplus.com/docs/auto-router.md # Auto router Send the model `routerplus/auto`, and RouterPlus selects a model for each request. The router reads what the request asks for and stays in the cost tier that you select. It keeps one conversation on one model. ## Request ```json { "model": "routerplus/auto", "messages": [{ "role": "user", "content": "Why does this throw?\n```js\nnull.x\n```" }], "auto": { "tier": "economy", "prefer": "cost", "models": ["deepseek/deepseek-v4.1-flash", "claude-haiku-5-5"] } } ``` The `auto` object and all of its fields are optional. | Field | Rule | |---|---| | `auto.tier` | `economy`, `balanced` (default) or `premium`. | | `auto.prefer` | `quality` (default) or `cost`. With `cost`, the router can also select models in cheaper tiers, and price counts for more. | | `auto.models` | The models that the router can select. Leave it out to use the full list below. | ## Tiers | Tier | Models | |---|---| | `premium` | `claude-opus-5-5`, `gpt-6-astra` | | `balanced` | `claude-sonnet-5-5`, `gpt-6-luna`, `moonshotai/kimi-k3`, `z-ai/glm-5.3` | | `economy` | `claude-haiku-5-5`, `deepseek/deepseek-v4.1-flash`, `openai/gpt-oss-120b`, `z-ai/glm-5.3-flash` | The router selects from your tier. When no model in your tier can serve the request, it selects from the next tier up. ## How it selects 1. The router reads the conversation and names the task: - `code`: code fences, stack traces, file paths or SQL. - `reasoning`: proofs, math or requests for step-by-step work. - `extraction`: `response_format` with a JSON schema. - `vision`: images in the latest message. - `long_context`: 120,000 or more characters of input. - `chat` or `creative` otherwise. 2. It removes the models that cannot serve the request: - models without image input, when you send images - models without tools, when you send tools - models with a context window too small for your input 3. It removes the models that your key's guardrails or `auto.models` do not allow. 4. It ranks the remaining models by how well each one does the task, then by price. A short follow-up such as "thanks, now make it faster" keeps the conversation's task. ## One model per conversation The router keeps a conversation on the model that it selected first. The router identifies a conversation by your system prompt plus your first user message. It selects again only in these cases: - The latest message asks for a different task, for example a code conversation that now asks for a poem. - The model can no longer serve the request. ## Fallback The router also ranks up to 4 more models. When the selected model fails before its answer starts, RouterPlus tries the next model, as in [Model fallback](/docs/model-fallback). ## Response The response is the selected model's answer, and `model` names that model. Two headers tell what the router did: | Header | Meaning | |---|---| | `x-tm-auto-model` | The model that the router selected. | | `x-tm-auto-reason` | A short reason, for example `task code; tier economy; strong at code`. | ## Billing RouterPlus bills the request as a direct call to the model that answered. The router adds no charge. ## Custom routers Your organization can store its own router and call it as the model `routerplus/auto@`. A router has a list of models that it can select, rules that it tries in order, and a default model. In the console, open Build > Routers. ```json { "model": "routerplus/auto@support", "messages": [{ "role": "user", "content": "Where is my invoice for October?" }] } ``` The router selects a model in this order: 1. The trained classifier, if the router has one and its prediction is above the threshold. See [Training](#training). 2. The rules, in order. The first rule that matches selects its model. 3. The built-in ranking above, over the router's candidates. 4. The default model. - The router never selects a model that your key's guardrails or `auto.models` do not allow. It goes to the next step instead. - `auto.tier` and `auto.prefer` in the request apply to step 3. - In step 3, one conversation keeps its model, as above. The classifier and the rules decide again on each request. The response has the `x-tm-auto-model` and `x-tm-auto-reason` headers. It also has `x-tm-router` with the router's slug and version, for example `support@3`. The reason names the step: `classifier: claude-haiku-5-5 at 94%`, `rule 2`, a ranking reason, or `default`. An unknown slug returns 404 `model_unavailable`. ### Spec ```json { "candidates": [ { "model": "claude-haiku-5-5", "tier": "economy" }, { "model": "claude-sonnet-5-5", "strengths": ["code", "creative"] } ], "rules": [ { "when": { "contains": ["\\binvoice\\b", "\\brefund\\b"] }, "use": "claude-haiku-5-5" }, { "when": { "task": ["code"], "min_input_chars": 20000 }, "use": "claude-sonnet-5-5" } ], "default": "claude-haiku-5-5", "prefer": "cost" } ``` | Field | Rule | |---|---| | `candidates` | Up to 20 models for step 3. `tier` and `strengths` are optional. A model from the tiers table keeps its tier, strengths and context window. Any other model is `balanced`, and the router assumes that it takes images and tools. | | `rules` | Up to 50 rules. Each rule has `when` and `use` (a model id). | | `default` | The model for step 4. Required. | | `prefer` | `cost` or `quality`, for step 3. `auto.prefer` in the request wins. | Every condition in `when` must be true. A rule with an empty `when` always matches. | Condition | Matches when | |---|---| | `task` | The task is one of these: `code`, `reasoning`, `extraction`, `chat`, `creative`, `vision`, `long_context`. | | `contains` | One of these regular expressions matches the first 20,000 characters of the latest user message. Case is ignored. 1 to 10 patterns of up to 512 characters. | | `min_input_chars` | The input, with the system prompt and the tool definitions, has at least this many characters. | | `has_images` | The conversation has an image (`true`), or has none (`false`). | | `has_tools` | The request sends tools (`true`), or sends none (`false`). | `contains` patterns follow the same safety rules as guardrail regex rules. RouterPlus refuses these patterns with 400 `invalid_router`: - a lookahead or a lookbehind - a backreference - a repeated group that holds a quantifier or an alternation, such as `(a+)+` ### Manage routers Use a management key, under `https://demo-api.routerplus.com/v1` or `https://demo-api.routerplus.com/api/v1`. | Method and path | Body | Result | |---|---|---| | `GET /v1/routers` | | The latest version of each router. | | `POST /v1/routers` | `{ "slug": "support", "spec": { ... } }` | `201`. A slug that exists returns `409`. | | `GET /v1/routers/:slug` | | The latest version. `?version=2` reads an older version. | | `PUT /v1/routers/:slug` | `{ "spec": { ... }, "expected_version": 3 }` | A new version. `expected_version` is optional. When it is not the latest version, the response is `409`. | | `DELETE /v1/routers/:slug` | | The router stops working. | | `POST /v1/routers/:slug/train` | `{ "examples": [ ... ], "threshold": 0.7 }` | A new version with a trained classifier. | A slug is 1 to 80 characters from `a-z`, `0-9`, `_` and `-`, and starts with a letter or a digit. Each change writes a new version. Requests use the latest version within 5 seconds. ```bash curl -s https://demo-api.routerplus.com/v1/routers \ -H "Authorization: Bearer $TM_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{"slug":"support","spec":{"rules":[{"when":{"contains":["\\binvoice\\b"]},"use":"claude-haiku-5-5"}],"default":"claude-sonnet-5-5"}}' ``` ## Training A router can learn from your labeled requests. Send examples of text, each with the model that must answer it: ```bash curl -s https://demo-api.routerplus.com/v1/routers/support/train \ -H "Authorization: Bearer $TM_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{"threshold":0.8,"examples":[ {"text":"I was charged twice this month","model":"claude-haiku-5-5"}, {"text":"The export to CSV crashes with error 500","model":"claude-sonnet-5-5"} ]}' ``` - Send 10 to 5,000 examples. Each `text` has up to 10,000 characters. - Label at least two models. Each label must be a candidate or the router's default. - The gateway fits a naive Bayes text classifier on words and word pairs. It keeps the 20,000 most frequent features. - `threshold` is optional, above 0 and below 1. Default 0.7. At request time, the classifier reads the latest user message. When its top model has a probability above the threshold, that model answers, before the rules. Otherwise the rules decide. The response has the new version and a `training` object with these fields: `examples`, `labels`, `features`, `threshold` and `training_accuracy`. `training_accuracy` is the share of your examples that the classifier labels correctly. Router objects show a summary of the classifier: its labels, feature count, example count, threshold and training time. To replace the classifier, train again. A `PUT` without `model_classifier` keeps the current classifier. To remove it, send `"model_classifier": null`. ## Errors | Status | Cause | |---|---| | 400 `invalid_request` | The `auto` object is invalid. The message names the field. | | 400 `invalid_request` | No allowed model can serve the request. The message names the need: image input, tools or context length. | | 400 `invalid_router` | A custom router's spec or training request is invalid. The message names the field. | | 400 `invalid_request` | No step of a custom router can select an allowed model. | | 404 `model_unavailable` | The custom router slug is unknown. | --- Source: https://demo.routerplus.com/docs/fusion.md # Fusion Fusion sends one prompt to several models at once. A judge model then picks the best answer or writes one answer from all of them. Call `POST /v1/chat/completions` with the model `routerplus/fusion`. ## Request ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \ -d '{ "model": "routerplus/fusion", "messages": [{ "role": "user", "content": "Explain what a mutex is." }], "fusion": { "models": ["claude-sonnet-5-5", "gpt-6-luna", "moonshotai/kimi-k3"], "judge": "claude-opus-5-5", "mode": "synthesize", "max_tokens_each": 1024 } }' ``` | Field | Rule | |---|---| | `fusion.models` | 2 to 5 different model ids. These are the candidates. | | `fusion.judge` | Optional. The model that judges. The default is the first candidate. | | `fusion.mode` | `"synthesize"` (default): the judge writes one answer from the candidates. `"judge"`: the judge picks the best candidate, and you get that answer word for word. | | `fusion.max_tokens_each` | Optional. The output limit for each candidate. Without it, each candidate gets your `max_tokens`. | - Each candidate gets your messages and parameters. Candidates do not stream. - The judge sees the candidate answers as A, B, C and so on, never the model names. - `stream: true` streams the answer. In `synthesize` mode the judge's answer streams as the judge writes it. In `judge` mode the picked answer streams after the judge picks. ## Response The response is an OpenAI chat completion with `model: "routerplus/fusion"`. `usage` is the sum over every candidate and the judge. `usage.cost` is present when the calls report a cost. A streamed response carries `usage` and `fusion` on its last chunk before `[DONE]`. ```json { "model": "routerplus/fusion", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "..." }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 1830, "completion_tokens": 892, "total_tokens": 2722, "cost": 0.0213 }, "fusion": { "mode": "synthesize", "candidates": [ { "model": "claude-sonnet-5-5", "finish_reason": "stop", "usage": { "prompt_tokens": 20, "completion_tokens": 300, "total_tokens": 320 } }, { "model": "gpt-6-luna", "finish_reason": null, "usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }, "error": "the provider is unavailable" }, { "model": "moonshotai/kimi-k3", "finish_reason": "stop", "usage": { "prompt_tokens": 20, "completion_tokens": 280, "total_tokens": 300 } } ], "judge": { "model": "claude-opus-5-5", "usage": { "prompt_tokens": 1790, "completion_tokens": 312, "total_tokens": 2102 } } } } ``` In `judge` mode, `fusion.judge` also has `pick` (the letter) and `selected` (the model whose answer you got). ## When a call fails - RouterPlus leaves out a candidate that fails. Its entry in `fusion.candidates` has `error`. - When only one candidate answers, you get its answer with no judge call. `fusion.judge.skipped` says so. - When every candidate fails, you get the first candidate's error. - When the judge fails, you get the longest candidate answer. `fusion.judge.error` says why and `fusion.judge.fallback` names the candidate. ## Billing RouterPlus bills each candidate call and the judge call as a direct call to its model. A fusion request costs the sum of those calls. ## Errors | Status | Cause | |---|---| | 400 `invalid_request` | The `fusion` object is missing or invalid. The message names the field. | | 400 | `n` is above 1. Fusion returns one choice. | Fusion does not work with [server tools](server-tools.md). --- Source: https://demo.routerplus.com/docs/routing-policies.md # Routing policies and Azure connections A routing policy is a saved rule set for a key. It says which of your connections and which marketplace deployments the key may use, in what order, and who pays. You can bind a policy to an org, a workspace or a key. ## Create and bind a policy Policy management uses the site origin, with the same session cookie, JSON content type and same-origin write protection as [BYOK connection management](/docs/byok). Inference keys cannot manage policies. Viewers can read them. `POST /api/routing-policies` creates a policy revision. A revision never changes. ```json { "models": ["your-exact-model-id"], "connections": ["FIRST_CONNECTION_UUID", "SECOND_CONNECTION_UUID"], "funding": "byok_only", "allow_fallbacks": true, "max_attempts": 4, "timeout_ms": 450000 } ``` The response is `201` with `{"policy_id":"UUID"}`. The connections must belong to your org. | Request | Effect | |---|---| | `POST /api/routing-policies/{id}/bind` with `{"key_id":"KEY_UUID"}` | Binds the policy to an existing key. | | `POST /api/routing-policies/{id}/bind` with `{"scope":"org"}` | Binds the policy to the org. | | `GET /api/routing-policies` | Lists the newest 100 revisions. | | `GET /api/routing-policies/{id}` | Returns one revision. | Binding rules: - An org policy applies to every key in the org. A key policy can narrow it. - A key with no policy and no single-connection binding of its own uses the org policy. - An older single-connection binding stays as an extra restriction when an org policy exists. - Binding a policy to a key replaces the key's single-connection binding. Binding a single connection replaces the key's policy. - To change a policy, create a new revision and bind it. You cannot delete a policy or unbind a key back to marketplace-only routing. - Cached marketplace-only decisions can take up to 5 seconds to see a new binding. Calls already dispatched can finish on their pinned revision. - Workspace bindings use the [identity API](identity.md). ## Policy fields | Field | Behavior | |---|---| | `models` | 1 to 100 exact IDs. Every policy that applies must allow the requested model. | | `connections` | Up to 8 BYOK connection UUIDs that your org owns, in order. | | `house_deployments` | Up to 8 marketplace deployment IDs. | | `funding: "byok_only"` | The default. Needs connections. Forbids marketplace deployments. | | `funding: "house_only"` | Needs marketplace deployments. Forbids connections. | | `funding: "byok_with_house_fallback"` | Needs both. Marketplace deployments are always the last phase. | | `allow_fallbacks` | Default `true`. `false` keeps only the first eligible route, before health filtering. | | `max_attempts` | Integer 1 to 8. Default 4. | | `timeout_ms` | Admission and generation deadline, 1,000 to 600,000 ms. Default 450,000. | | `regions`, `zdr`, `data_collection` | Hard restrictions, checked against provider facts that RouterPlus operators record. A route with no recorded fact is refused. | ### Fallback - Under a policy, automatic fallback stays within the first selected **provider label**. - You can authorize and select both OpenAI and Azure. A failed OpenAI call does not switch to Azure on its own. - RouterPlus does not decide that two models are the same. It rejects `attest_equivalence:true`. - Azure deployment mappings are your declarations. - When fallbacks are off, an unavailable first route does not authorize a fallback. - No retry crosses the [streaming commit boundary](/docs/routing#the-commit-boundary) or a content-policy refusal. ### How restrictions combine - The restrictions in force are the intersection of the org policy, workspace policy, key policy or binding, the server catalog and the request. An empty intersection refuses the request. Lower limits win. - Order: request connection preference, then provider preference, then saved order, then deployment ID as a stable tie-breaker. - With mixed funding, marketplace deployments come after every BYOK route. ### Deadline `timeout_ms` covers admission and generation after planning, including active streams. Request upload, policy and price reads, and the final billing settlement are separate phases. A timeout never skips billing settlement. A reservation that arrives after the deadline is released, and RouterPlus never dispatches it. ## Request controls The inference `provider` object accepts: ```json { "only": ["openai"], "ignore": [], "order": ["openai"], "connections": ["CONNECTION_UUID"], "connection_order": ["CONNECTION_UUID"], "funding": ["byok"], "allow_fallbacks": false, "require_parameters": true } ``` `only`, `ignore` and `order` name route labels: marketplace `anthropic` and `openai`, and a BYOK connection's profile (`openai`, `anthropic`, `azure`, `bedrock`). A route label keeps that meaning even when its route does not serve the model. Any other label matches no route. It selects, orders or skips nothing, and an `only` list with no route label leaves no route. On `POST /v1/decisions` the labels are the decision providers: - `typesafe` for Jev - `bespokelabs` for the Bespoke Nimble models - `workers-ai` for Clef and Clef-flash - `routerplus` for Decider 2B and Kev 4B - `perplexity-decisions` for Perplexity Decider v1 27B - `openai-decisions` for GPT-6 Luna Decisions - `levanto` for Sage - `fastino` for GLiNER-2.5-Decide Clef's label is `workers-ai`, not `cloudflare`: on `POST /v1/decisions`, `cloudflare` names no provider. Perplexity Decider's label is `perplexity-decisions`, not `perplexity`. GPT-6 Luna Decisions' label is `openai-decisions`, not `openai`, the label of the RouterPlus OpenAI chat route. Other controls: - `require_parameters: true` refuses a deployment that would drop one of your request fields. Without it, RouterPlus calls the deployment without that field. - `regions`, `zdr` and `data_collection` work here too, as hard restrictions. - `sort` (`price`, `throughput`, `latency` or `quality`) and `max_price` apply to marketplace routes only. See [Provider routing](/docs/provider-routing). The [Agent integration guide](agent-integration.md) covers how to keep a session's prompt cache warm. - Raw request keys and arbitrary destinations return 400. Request preferences never grant connections, models, providers or funds that the saved policy does not allow. You cannot write the provider facts used for region and privacy restrictions. A restriction is not a residency or ZDR guarantee. ### Explain a route `POST /v1/route` takes `{"model":"ID","provider":{...}}` with the inference key. It returns, with `advisory: true`: - the plan, catalog and policy IDs; - the candidates in order, with their funding and pinned marketplace price IDs; - the excluded routes, each with a reason; - the time and source of the health data. It makes no provider request, decryption or reservation. Health and authorization can change before the real dispatch. Related endpoints: - `GET /v1/models` applies the same restrictions. An optional URL-encoded JSON `provider` query parameter narrows the list. `output_modalities=text,image,decisions` filters by kind. - Token counting uses the same plan but needs an Anthropic route. It sends one count request and does not fall back. - `GET /v1/generation?id=...` shows each attempt's `route_context` and fallback cause. - Policy requests return `x-tm-route-plan-id` for correlation. ### Funding - BYOK needs no marketplace credit. - Marketplace fallback reserves credit only when the request reaches it, and charges under the standard pricing rules. - Earlier marketplace holds stay until the request finishes, so fallback admission is conservative while settlements are pending. - Each attempt pins its own funding and credential. - RouterPlus does not know what a provider charges you on BYOK. Marketplace prices come from the model price list and are fixed before dispatch. ## Azure/Foundry API-key connections Create a connection with `POST /api/connections`. In the console, open Access > Provider connections. ```json { "profile": "azure", "models": ["your-public-model-id"], "api_key": "YOUR_RESOURCE_API_KEY", "endpoint_config": { "resource": "your-resource-name", "host": "foundry", "api_version": "v1", "deployments": {"your-public-model-id": "your-deployment-name"} } } ``` - `host` is `openai` (`{resource}.openai.azure.com`) or `foundry` (`{resource}.services.ai.azure.com`). RouterPlus builds the public hostname. You cannot send a URL. - Every model you declare needs a deployment mapping. - `endpoint_config` cannot change. A change needs a new connection. - Validate the connection, then bind it to a key or include it in a policy. Rotation and revocation follow the [connection lifecycle](/docs/byok). RouterPlus calls `/openai/v1/chat/completions?api-version=v1`, with the deployment name in the model field and the native `api-key` header. Validation calls the model-list endpoint and generates nothing. These shapes follow Microsoft's [chat reference](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/chat), [models reference](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/models) and [v1 lifecycle guide](https://learn.microsoft.com/en-us/azure/foundry/openai/api-version-lifecycle). | Supported | Not supported | |---|---| | API-key authentication, public v1 endpoints | Entra or workload identity, legacy dated APIs, preview versions, sovereign or private endpoints, Azure capacity or residency checks | Validation confirms endpoint authentication only. It does not confirm deployment entitlement, model equivalence, quota or capacity. ## Limits - Shared admission needs declared [quota pools and gateway limits](/docs/admission). - A full quota can skip a route that the policy allows. It never adds providers or marketplace funding. - Model lists and route explanations do not reserve capacity. - The attempt record stores at most 32 exclusion details, with `excludedTotal` for the full count. The catalog hash and the full route explanation keep the rest of the context. --- Source: https://demo.routerplus.com/docs/dedicated-endpoints.md # Dedicated endpoints A dedicated endpoint is provider capacity reserved for your organization on one model, with its own limits, prices and failover rules. RouterPlus sets one up with you under a contract. There is no self-serve setup. You call the endpoint with the model id RouterPlus gives you, in your organization's namespace: `/`, for example `acme/gemma-31b`. ## Calling an endpoint Use the endpoint's model id in place of a catalog model id. Both surfaces (`/v1/chat/completions` and `/v1/messages`), streaming, and every request field the model accepts work as usual. ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "acme/gemma-31b", "messages": [{"role": "user", "content": "Hello"}]}' ``` Only your organization's keys can call the endpoint, and it can be limited to named workspaces. Any other key gets the same 404 `model_unavailable` as for an unknown model. On an open endpoint, the response's `model` is the model that ran, for example `"model": "google/gemma-4-31b-it"`. On a closed endpoint it is the endpoint id (see [Closed endpoints](#closed-endpoints)). Two headers say how the request was served: | Header | Value | |---|---| | `x-tm-dedicated-endpoint` | The endpoint's id, for example `acme/gemma-31b`. | | `x-tm-route-role` | `primary` (your dedicated capacity) or `fallback` (the shared pool). | - In [`/v1/generation`](/docs/api-usage), each attempt's `route_context.dedicated` records the endpoint id, the revision of its configuration in force, and the role. - `GET /v1/models` lists your organization's active endpoints with `owned_by` `routerplus`, on the base URL that serves the endpoint. - The [Endpoints](https://demo.routerplus.com/endpoints) page lists your endpoints before the models you deployed from Optimize. For each endpoint it shows the id, model, base URL, status, provider and limits, and the price per million tokens: input, cached input and output, and the cache write where the rate card has one above $0. ## What serves your request 1. **Your dedicated capacity first.** 2. **Then the shared pool, for the same model only.** The request moves on when the dedicated deployment fails before your answer starts, when its circuit is open, or when its capacity is full. It goes to public deployments of the same model that meet your endpoint's constraints, never to a different model. 3. **The commit boundary holds.** Once the first token reaches you, the request stays with that provider. See [Routing & failover](/docs/routing). Your endpoint's configuration sets the constraints. They apply to every fallback: | Constraint | A fallback host qualifies only if | |---|---| | Precision | It serves one of the listed precisions, for example `bf16`. | | Zero data retention | It keeps no prompts. | | Data collection denied | It neither stores nor trains on requests. | | Maximum price | Its price per million tokens is at or below the limit. This is the price of the provider that serves it (its live price when that provider sets its own). | | Regions | It runs in one of the listed countries. | | Hosts | It is one of the named hosts. | | Require parameters | It accepts every parameter the request sends. | - A host that has not stated a value for a constrained property does not qualify. - A fallback that reaches several hosts through one route sends the constraints with the request. Only hosts that meet them serve it. - A region constraint excludes such a route, because the route makes no promise about where a host runs. An endpoint set to `primary_only` never falls back. When the dedicated deployment cannot serve, you get the error. ### Request-level provider fields For one request you can narrow the routes. You cannot widen them. - `"provider": {"allow_fallbacks": false}` keeps the request on your dedicated capacity. It works on open and closed endpoints. - On an open endpoint, `provider.only` and `provider.ignore` filter by provider name, as on any request. - A closed endpoint selects its providers and hosts through the contract configuration. `provider.only`, `provider.ignore`, `provider.order`, `provider.upstream` and `provider.require_parameters` return `400 invalid_request`, empty selectors included. Omit those fields on closed endpoints. - `provider.sort` and `provider.max_price` return `400 invalid_request` on every dedicated endpoint, open or closed. The message names the field. The endpoint's own constraints, including its maximum price, decide its routes. ## Closed endpoints An endpoint is open unless your contract says otherwise. An open endpoint names the deployment and host of the shared pool that served you, like any request. Your dedicated capacity shows as `routerplus`. A closed endpoint never names a deployment or a host. Every surface names RouterPlus as the provider, whichever route served the request. Only the route role differs between routes: | Surface | On a closed endpoint | |---|---| | `x-tm-provider` | `routerplus` | | `x-tm-served-by`, `x-tm-upstream-model`, `x-tm-upstream-status`, `x-tm-dropped-params`, `x-tm-affinity-wait-ms` | Absent. | | `x-tm-dedicated-endpoint`, `x-tm-route-role` | As on an open endpoint. | | `x-tm-remaining-rpm`, `x-tm-remaining-tpm` | Counted against the endpoint's own limits only. | | Chat completion body | A fixed set of fields, below. `model` is the endpoint id. | | Stream chunks | The same fields in each chunk. A provider's SSE comment lines are dropped. A provider's error in the stream ends it with the gateway's own error event, and nothing follows that event. | | Error messages | They name `routerplus`, never a deployment. A provider's own error body is never relayed. | | Error statuses | A provider's 401, 402, 403 or 404 is a retryable 503 `upstream_error`. A `retry-after` is the gateway's own, never a provider's. | | Limits | A refusal because the capacity behind one route is full has `x-tm-limit-scope: endpoint` and `x-tm-limit-id: endpoint:`. | | `provider.only`, `provider.ignore`, `provider.order`, `provider.upstream` | Refused with 400 `invalid_request`. | | [`/v1/generation`](/docs/api-usage) | `deployment` is `routerplus` and `model` the endpoint id. `route_context` keeps only `dedicated` (the endpoint, its revision and the role). `admission_context` is null, `dropped_params` is empty, and admission events carry no pool scope id. | | [`/v1/usage`](/docs/api-usage) | `deployment` is `routerplus` and `model` the endpoint id. | | Console: Overview, Usage, Logs and a request's details | RouterPlus as the provider, no host, the endpoint id as the model, and "contract rate" as the price. | | The Playground's receipt | Served by RouterPlus. | Records stay masked once an endpoint has been closed. Requests it served before it closed, and after it opens again, read as above. The fields of a closed chat completion: | Level | Fields | |---|---| | Top level | `id` (`chatcmpl-` and the request id without dashes), `object`, `created` (when the gateway received your request), `model`, `choices`, `usage` | | A choice | `index`, `message` (not streamed) or `delta` (streamed), `finish_reason` (`stop`, `length`, `tool_calls`, `content_filter` or `function_call`; null until a stream ends), `logprobs` (only when you ask for them) | | `message` or `delta` | `role`, `content`, `tool_calls` (`id`, `type`, `function.name`, `function.arguments`, and `index` in a stream), `function_call`, `refusal`, `reasoning_content` | | `usage` | `prompt_tokens`, `completion_tokens`, `total_tokens`, `prompt_tokens_details.cached_tokens` and `completion_tokens_details.reasoning_tokens` (both always present, 0 when none), `cost` | - Every other field is dropped, including any field a provider adds later. - A field without a value is left out. A message keeps `content`: null beside tool calls. - A model's thinking arrives as `reasoning_content` on every route. - A tool call's id is the gateway's own: `call_` and 24 hex characters. It is the same on every chunk of the call, and it is the `tool_use` id on `/v1/messages`. - In a stream, the first delta of a choice has the role. The first delta of a tool call has its id, type and name. The finish and the usage each come in a chunk of their own. ## Prices | Attempt | Price | |---|---| | On your dedicated capacity | Your contract rate card, per million tokens. | | On a fallback | The price of the provider that serves it, as for any other request (its live price when that provider sets its own). Your rate card instead, when your contract sets one price on every route. | - A closed endpoint bills your rate card on every route. A second price would tell you which route served. - RouterPlus tries a fallback only when its price is known and at or below the endpoint's maximum price, if one is set. This holds whichever price you then pay. - In `/v1/generation`, an attempt billed at your rate card has `inference_price_source` `customer_rate` and a `price_snapshot_id` of the form `dedicated::r`, which names the rate card that produced the charge. - `usage.cost` on the response is the full debit across all attempts, as on every request. ## Limits For the endpoint's traffic, its own limits replace the default organization, workspace, principal and key limits: | Limit | Counted for | |---|---| | Requests per minute | The endpoint | | Requests per second (optional): a count per second, or a paced rate | The endpoint | | Tokens per minute | The endpoint | | Concurrent requests | The endpoint | A limit your organization set in Billing > Rate limits ([`/console/limits`](https://demo.routerplus.com/console/limits)) still applies on top. You can lower what one key or workspace may use. You cannot raise the endpoint's limits. A refusal is the usual 429 `rate_limit` (see [Rate limits & spend caps](/docs/limits)), with `x-tm-limit-scope: endpoint`: | Case | `x-tm-limit-kind` | `x-tm-limit-id` | |---|---|---| | Over the endpoint's RPM, RPS, TPM or concurrency | `rpm`, `rps`, `tpm` or `concurrency` | `endpoint:` | | Above a paced endpoint's hard rate | `rps` | `endpoint:` | | A paced endpoint's wait would be longer than its bound | `rps_queue` | `endpoint:` | | The dedicated capacity is full and the endpoint does not spill over to the shared pool | the measure that is full | `endpoint:` | | A closed endpoint: the capacity behind every route it could take is full | the measure that is full | `endpoint:` | | Fallback traffic is over the endpoint's fallback cap | `rpm`, `tpm` or `concurrency` | `endpoint-fallback:` | `GET /v1/limits?model=/` returns your key's effective limits on the endpoint and a `dedicated` object: the endpoint, its revision, the model, the output default, the fallback caps and whether it spills over. When the shared counter store is unreachable: - In outage mode `closed`, requests are refused. - In the default `bounded_open` mode, requests are admitted under the endpoint's own limits, divided among the gateways that serve it, for up to 10 minutes. See [Limits and capacity](/docs/admission). ### The paced limit When traffic arrives at random, a limit that counts each calendar second refuses requests in many seconds, even when the average rate is below the limit. A paced limit queues a short burst instead. It has three settings: the paced rate, a hard rate above it, and the longest wait (the bound). - Requests leave admission no faster than the paced rate. A request that must wait waits in the gateway. `x-tm-queue-ms` on the response gives the wait in milliseconds. - A request whose wait would pass the bound gets 429 with `x-tm-limit-kind: rps_queue`. Its `retry-after` says when its wait would fit. - The hard rate refuses a request at once, with `x-tm-limit-kind: rps`. It allows a burst as long as the bound (at least one second). It never refuses a request the queue would hold. With a bound of a second or more, a full queue refuses first. - The wait comes before the request's own deadline starts, so it does not use the endpoint's total time. - A waiting request holds no concurrency slot. Its place in the pace stays used if you disconnect. For example, with a paced rate of 6 requests a second, a hard rate of 6.5 and a 2 s bound: | Average rate, arriving at random | Refused (429) | Typical wait | |---|---|---| | 4 a second | Almost none | About 0.1 s | | 5 a second | About 0.3 % | About 0.3 s | | 6 a second | About 4 % | About 0.9 s | | Steadily above 6 a second | The excess, once the queue is full | Up to 2 s | A queue cannot hold a steady excess. Over time, the endpoint passes its paced rate. ## Timeouts and defaults | Setting | On other requests | On an endpoint | |---|---|---| | Attempts per request | Up to 8 | 1 to 8. Default 3. | | Total time per request | 450 s | 1 to 600 s | | Wait for a streamed attempt's response headers before the next route | 20 s | 1 to 120 s | | Time one attempt may take on a request that is not streamed, before the next route | 450 s | 1 to 600 s, below the total | | `max_tokens` written into a request that sets none | The model's own maximum, fitted to its context window | Set per endpoint | A shorter header wait (streamed) or attempt limit (not streamed) makes failover faster when the dedicated deployment stalls. A stalled request still takes that limit plus the fallback's own time. The attempt limit applies only while another route remains. The last route can run for the rest of the total time. ## Other limits - `POST /v1/route` does not explain dedicated endpoints yet. - Every request to a deployment shares its health circuit. Two failures in a row on the dedicated deployment send all its traffic to the fallbacks for 30 seconds. Per-endpoint circuit settings are not available yet. - RouterPlus changes an endpoint's configuration on request. Each change is a new revision. It reaches every gateway within seconds. --- Source: https://demo.routerplus.com/docs/server-tools.md # Server tools Server tools are tools that RouterPlus runs for your model. Add them to `tools` on `POST /v1/chat/completions`. The model calls them like any function tool. RouterPlus runs the call, gives the result to the model and calls the model again. You get the final answer. | Tool | What the model gets | |---|---| | `routerplus:advisor` | Advice from a second model that sees the conversation. | | `routerplus:subagent` | The answer of a helper model to a task that the model writes. | | `routerplus:web_fetch` | The text of a web page. | ## Request ```json { "model": "moonshotai/kimi-k3", "messages": [{ "role": "user", "content": "Compare the two pricing pages and recommend one plan." }], "tools": [ { "type": "routerplus:advisor", "advisor": { "model": "claude-opus-5-5", "max_calls": 2 } }, { "type": "routerplus:subagent", "subagent": { "models": ["gpt-6-luna", "claude-sonnet-5-5"], "max_calls": 4 } }, { "type": "routerplus:web_fetch", "web_fetch": { "allowed_domains": ["example.com"], "max_bytes": 300000 } }, { "type": "function", "function": { "name": "save_report", "parameters": { "type": "object" } } } ] } ``` | Field | Rule | |---|---| | `advisor.model` | The advisor model. | | `advisor.max_calls` | 1 to 5. The default is 3. | | `subagent.models` | 1 to 5 helper models. The model can pick one. The default is the first. | | `subagent.max_calls` | 1 to 8. The default is 4. | | `web_fetch.allowed_domains` | Optional. Fetch only these domains and their subdomains. | | `web_fetch.max_bytes` | Up to 2000000. The default is 500000. RouterPlus cuts a longer page at this size. | | `web_fetch.max_calls` | 1 to 10. The default is 5. You can send `web_fetch` without its object. | List each tool type once. When a server tool is in the request, your own function tools cannot use its name (`advisor`, `subagent` or `web_fetch`). ## What the model sees RouterPlus gives the model one function tool for each server tool: - `advisor(question)`: the advisor gets the conversation so far as text, plus the question. It answers with advice for the model. - `subagent(task, model?)`: the helper gets the task as its only message. The model gets the helper's final answer. - `web_fetch(url)`: RouterPlus fetches the URL and gives the model the page as plain text. When a tool has used its `max_calls`, the model gets a message that says so and continues without that tool. ## How the loop ends - The model answers without a tool call. You get that answer. - The model calls one of your own function tools. You get that answer with your tool calls. RouterPlus removes server tool calls from the same answer. Run your tools and send the next request as usual. - After 12 model calls, you get the last answer without its tool calls, and `server_tools.stopped` is `"round_limit"`. Your `tool_choice` applies to the first model call only. ## Response The response is an OpenAI chat completion. `model` is your model. `usage` is the sum over every model call: your model, the advisor and the helpers. `usage.cost` is present when the calls report a cost. ```json { "model": "moonshotai/kimi-k3", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "..." }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 5210, "completion_tokens": 940, "total_tokens": 6150, "cost": 0.0182 }, "server_tools": { "rounds": 3, "stopped": "answer", "calls": [ { "type": "web_fetch", "url": "https://example.com/pricing", "status": "ok", "bytes": 48211 }, { "type": "subagent", "model": "gpt-6-luna", "status": "ok", "usage": { "prompt_tokens": 900, "completion_tokens": 300, "total_tokens": 1200 } }, { "type": "advisor", "model": "claude-opus-5-5", "status": "ok", "usage": { "prompt_tokens": 2100, "completion_tokens": 120, "total_tokens": 2220 } } ] } } ``` `server_tools.stopped` is `"answer"`, `"buyer_tools"` or `"round_limit"`. Each entry in `server_tools.calls` has a `status`: | Status | Meaning | |---|---| | `ok` | The tool ran. | | `error` | The tool failed. `error` says why. The model got the error as the tool result. | | `refused` | `web_fetch` did not fetch the URL. `error` says why. | | `limit` | The tool had used its `max_calls`. | A `web_fetch` entry has `truncated: true` when RouterPlus cut the page at `max_bytes`. `stream: true` streams the final answer only. The last chunk before `[DONE]` carries `usage` and `server_tools`. ## Web fetch rules - RouterPlus fetches only `http` and `https` URLs. It refuses URLs with a user name or password. - RouterPlus resolves the host name first. It refuses the URL when any address is private, loopback, link-local (cloud metadata addresses included), shared, multicast or reserved. This applies to IPv4, IPv6 and IPv4 addresses inside IPv6. - The connection goes to the address RouterPlus checked. - RouterPlus follows up to 3 redirects and checks each with the same rules, `allowed_domains` included. - HTML becomes plain text. Other text types come back as they are. Images and other binary types are refused. - A fetch stops after 20 seconds. ## Billing RouterPlus bills each model call as a direct call to its model. Your key's limits and [guardrails](guardrails.md) apply to each call. A request with server tools costs the sum of those calls. `web_fetch` has no charge of its own. ## Errors | Status | Cause | |---|---| | 400 `invalid_request` | A server tool is invalid. The message names the field. | | 400 | `n` is above 1. Server tools answer with one choice. | | 403 `guardrail_blocked` | A guardrail blocks server tools (checked before any model call), or does not allow the model, advisor model or helper model. | ## Limits - Server tools do not work with `routerplus/fusion`, `routerplus/auto` or a `models` list. - When a call of your model fails, you get that error unchanged. --- Source: https://demo.routerplus.com/docs/mcp-gateway.md # MCP gateway The MCP gateway puts your MCP servers behind one RouterPlus endpoint. Your agents call the servers with their RouterPlus inference key. The gateway adds the server's credentials, applies your tool policy and logs each tool call. The gateway uses the MCP Streamable HTTP transport (JSON-RPC 2.0). It accepts JSON and `text/event-stream` responses from the server. In the console, open Build > MCP servers. ## Register a server Send `POST /v1/mcp/servers` with a management key (`Authorization: Bearer tm_mk_...`): ```json { "slug": "github", "url": "https://mcp.example.com/mcp", "auth_header": "Authorization", "auth_value": "Bearer ghp_...", "allowed_tools": ["search_*", "get_issue"], "blocked_tools": ["search_admin*"], "workspace_ids": [] } ``` | Field | Rule | |---|---| | `slug` | Required. 1 to 40 of `a-z`, `0-9` and `-`, starting with a letter or digit. Unique in your org. `servers` and `calls` are reserved. | | `url` | Required. An `https` URL. `http` works only for `localhost`. No user name or password in the URL. | | `auth_header` | Optional. The header name that the server expects, for example `Authorization` or `X-Api-Key`. | | `auth_value` | Required with `auth_header`. Up to 3,000 bytes. RouterPlus stores it encrypted and never returns it. | | `allowed_tools` | Optional. Tool names or globs. Empty (the default) allows every tool. | | `blocked_tools` | Optional. Tool names or globs. A blocked tool stays blocked even when `allowed_tools` matches it. | | `workspace_ids` | Optional. The workspaces whose keys can use the server. Empty (the default) means every workspace. | | `enabled` | Optional. Default `true`. A disabled server returns 404. | In a glob, `*` matches any run of characters. Every other character matches itself. Names are case-sensitive. The response is `201`, with the server. It shows `auth_header` and `has_auth_value`. It never shows the value. ## Manage servers | Route | What it does | |---|---| | `GET /v1/mcp/servers` | Lists your servers. | | `GET /v1/mcp/servers/{id}` | Reads one server. | | `PATCH /v1/mcp/servers/{id}` | Changes the fields you send. To rotate the secret, send `auth_value` alone. To remove the auth header and its value, send `"auth_header": null`. | | `DELETE /v1/mcp/servers/{id}` | Deletes the server. Its call log stays. | | `GET /v1/mcp/calls` | Reads the tool call log. | Every route also works under `/api/v1`. Each change writes a row to the audit log (`GET /v1/audit`). ## Call a server Point your MCP client at `https:///v1/mcp/{slug}` and send your inference key: ``` Authorization: Bearer tm_vk_... ``` | Method | Use | |---|---| | `POST` | Sends a JSON-RPC message or batch. The response is JSON or an SSE stream, in the form that the server sends. | | `GET` | Opens the server's SSE stream. | | `DELETE` | Ends the session. | - The gateway passes `mcp-session-id`, `mcp-protocol-version` and `last-event-id` to the server. It returns the server's `mcp-session-id` to you. - It sends the server's `auth_header` with the stored value. It never sends your RouterPlus key to the server. ## Tool policy - `tools/list`: the gateway removes the tools that your policy does not allow, in JSON and in SSE. - `tools/call` for a tool that your policy does not allow: the gateway returns this error and does not call the server: ```json { "jsonrpc": "2.0", "id": 3, "error": { "code": -32601, "message": "tool not allowed by RouterPlus policy" } } ``` - The gateway checks a batch one message at a time. Allowed messages go to the server. Refused calls get the error above, in the same response. - Other methods (`initialize`, `resources/*`, `prompts/*`, notifications) pass through. ## Tool call log The gateway records each `tools/call`: the tool name, the key, the server, the status and the duration. It does not store arguments or results. | Status | Meaning | |---|---| | `ok` | The server returned a result. | | `error` | The server returned a JSON-RPC error or a result with `isError: true`. | | `blocked` | Your policy refused the call. The gateway did not call the server. | | `upstream_error` | The server was unreachable or returned an HTTP error. | | `timeout` | The server did not answer in time. | | `no_answer` | The server ended its response without an answer to the call. | `GET /v1/mcp/calls` takes these query parameters: `server_id`, `key_id`, `tool`, `status`, `from` and `to` (ISO 8601 times), and `limit` (1 to 500, default 100). The newest calls come first. ## Limits and errors | Limit | Value | |---|---| | Request body | 4 MiB. A larger body gets `413`. | | Server timeout | 120 s until the server starts its response, and 120 s between parts of a stream. When the server does not start in time, the response is `504`. | | JSON response from the server | 16 MiB. | | Status | Cause | |---|---| | `401` | The inference key is missing or not valid. | | `404` | No enabled server has this slug in your org, or the server is not open to your key's workspace. | | `400` with JSON-RPC code `-32700` | The body is not valid JSON. | | `400` `upstream_refused` | The server URL resolves to a private, loopback or link-local address. The gateway does not call it. | | `502` `upstream_auth_failed` | The server refused the stored credentials (HTTP 401 or 403). | | `502` `upstream_error` | The server was unreachable, or sent a redirect or invalid JSON. | Other HTTP errors from the server (for example `404` for an expired session) reach you unchanged. --- Source: https://demo.routerplus.com/docs/streaming.md # Streaming Set `"stream": true` to receive the answer as Server-Sent Events (SSE). Both chat surfaces stream: `POST https://demo-api.routerplus.com/v1/chat/completions` (OpenAI) and `POST https://demo-api.routerplus.com/v1/messages` (Anthropic). ```bash curl -N https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"claude-sonnet-5","stream":true,"max_tokens":100, "messages":[{"role":"user","content":"Say hello"}]}' ``` The images route (`POST https://demo-api.routerplus.com/v1/images/generations`) does not stream. There, `stream: true` and `partial_images` are typed 400s. ## Relayed and translated streams RouterPlus handles a stream in one of two ways. The provider's dialect decides which. **Same dialect: relayed.** The provider speaks the dialect of your surface, for example a Claude model on `/v1/messages` or a GPT model on `/v1/chat/completions`. RouterPlus relays the provider's stream record for record, with the payloads unchanged. - Provider features reach you as they do from the provider: server tool use, web search results, citations, annotations, `pause_turn` and the matched `stop_sequence`. - RouterPlus reads the stream for billing and failover. It adds `usage.cost` to the provider's usage record and makes sure the stream ends with a well-formed terminator. - On the OpenAI surface, each chunk keeps only OpenAI's fields and names the model you asked for. RouterPlus drops fields the provider adds (its cost, its accounting, its host's name). The delta passes as the provider sent it. See [what an answer keeps](/docs/compat#what-an-answer-from-an-openai-dialect-provider-keeps). - An explicit `stream_options: {"include_usage": false}` turns the relay off. RouterPlus must then remove the usage chunk it asked the provider for. **Other dialect: translated.** RouterPlus translates the provider's stream into the framing of your surface. The frame shapes below apply. ## Deadlines | Deadline | Value | |---|---| | First byte (headers) | 20 seconds. A provider that misses it is failed over, and you get no error. See [Routing & failover](/docs/routing). | | After the first byte | None. RouterPlus does not cut a slow stream. | | Whole request | 450 seconds from dispatch, or the `timeout_ms` of a [routing policy](/docs/routing-policies) (1 to 600 s). | A stream that is still open at the request deadline ends with the terminal error event in [When a provider dies mid-stream](#when-a-provider-dies-mid-stream). ## Response headers Every stream response carries `x-request-id`, `x-tm-provider` (the deployment that served you), `x-tm-attempts` and `x-tm-upstream-status`. It carries `x-tm-dropped-params`, `x-tm-upstream-model` and `x-tm-served-by` when they apply. See [Wire compatibility](/docs/compat#response-headers). ## Frame order: OpenAI surface Each frame is a complete `data: {json}\n\n` line in the `chat.completion.chunk` shape. The order is fixed: role-priming delta, content deltas, finish chunk, usage chunk, `data: [DONE]`. ```text data: {"id":"chatcmpl-2f6d8e3b","object":"chat.completion.chunk","created":1757000000,"model":"claude-sonnet-5","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} data: {"id":"chatcmpl-2f6d8e3b","object":"chat.completion.chunk","created":1757000000,"model":"claude-sonnet-5","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]} data: {"id":"chatcmpl-2f6d8e3b","object":"chat.completion.chunk","created":1757000000,"model":"claude-sonnet-5","choices":[{"index":0,"delta":{"content":" there."},"finish_reason":null}]} data: {"id":"chatcmpl-2f6d8e3b","object":"chat.completion.chunk","created":1757000000,"model":"claude-sonnet-5","choices":[{"index":0,"delta":{},"finish_reason":"stop","native_finish_reason":"end_turn"}]} data: {"id":"chatcmpl-2f6d8e3b","object":"chat.completion.chunk","created":1757000000,"model":"claude-sonnet-5","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":9,"total_tokens":21,"prompt_tokens_details":{"cached_tokens":0,"cache_write_tokens":0},"completion_tokens_details":{"reasoning_tokens":0},"cost":0.000114}} data: [DONE] ``` - RouterPlus sends the role-priming delta (`{"role":"assistant","content":""}`) when the upstream responds. It shows that the stream is alive and carries no content. - Tool calls stream as `delta.tool_calls` entries with stable, ascending `index` values. Concatenate the argument fragments. A tool called with no arguments gets one `"{}"` fragment. - Reasoning models stream their thinking as `delta.reasoning_content`. - Refusal text streams as `delta.refusal`. - On a translated stream, the finish chunk also carries `native_finish_reason`, the provider's own stop reason (here `end_turn`). - The usage chunk is always sent, zero-filled if the provider reported nothing. The exception is a request with `stream_options.include_usage: false`. - `[DONE]` comes only after a successful finish. It never follows an error. ## Frame order: Anthropic surface Frames are named SSE events (`event: \ndata: {json}\n\n`) in the Anthropic Messages API order: `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`. ```text event: message_start data: {"type":"message_start","message":{"id":"msg_2f6d8e3b","type":"message","role":"assistant","model":"gpt-4o-mini","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 there."}} 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":9,"cost":0.000007}} event: message_stop data: {"type":"message_stop"} ``` - On a translated stream, the token counts in `message_start` are zeros. Read the final usage and `cost` in the last `message_delta`. On a relayed stream, `message_start` carries the provider's own counts. - Tool calls stream as `tool_use` content blocks with `input_json_delta` fragments. - Thinking streams as `thinking` blocks with `thinking_delta`, plus `signature_delta` where the provider signs it. - `input_tokens` excludes cache reads and writes, per the [billing contract](/docs/compat). `cache_read_input_tokens` and `cache_creation_input_tokens` report cache traffic. ## Usage is always in the stream You do not opt in to stream usage. RouterPlus adds `stream_options.include_usage` to every OpenAI-dialect upstream request, because OpenAI streams carry no usage without it. - RouterPlus honors an explicit `stream_options: {"include_usage": false}`. You are billed the same, and you get no usage chunk. - Every other stream ends with the token counts the provider reported. - Every billed stream carries `usage.cost` in USD. RouterPlus computes it with the integer micro-USD math the ledger settles with. It covers the full request debit, including attempts that failed over before your answer started. > [!TIP] > `cost` on the wire is the amount you are charged. You can recompute it from the token > counts and the public prices at > [https://demo.routerplus.com/api/models.json](https://demo.routerplus.com/api/models.json). Settlement rounds down. ## Keep-alive frames Proxies, load balancers and some HTTP clients close idle TCP connections. Slow reasoning models can be silent for a long time mid-answer. After your stream starts, RouterPlus sends a keep-alive frame after any silence of 15 seconds or more. | Surface | Frame | Who sees it | |---|---|---| | OpenAI | The SSE comment `: processing` | The SSE spec requires clients to ignore it. SDKs never see it. | | Anthropic | `event: ping` / `data: {"type":"ping"}` | The same frame Anthropic's API sends. Anthropic SDKs skip it. | RouterPlus measures the silence from the last real write, so a gap is at most about 15 s. Nothing is sent after a terminal event, keep-alives included. ## Cancelling a stream To cancel, close the connection (abort the HTTP request). RouterPlus then aborts the upstream request, and the provider stops generating. Billing: - You pay for the input and for the output streamed to you before the abort. The attempt has the outcome `cancelled`, visible at `GET /v1/generation?id=`. - If the provider reported no final usage, you pay for the input and the output you received, never the full hold. The input count is the provider's if it sent one, else RouterPlus's estimate. - A cancel before the first token costs the input only. Uptime: - A cancel does not count against the provider's uptime. The public uptime numbers and the health circuits exclude cancelled attempts. - Exception: if the provider sent no response headers for 60 seconds before you closed the connection, the cancel counts as a provider failure. See [Routing & failover](/docs/routing#health-circuits-and-cooldowns). ```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: "claude-sonnet-5", max_tokens: 4096, messages: [{ role: "user", content: "Explain SSE, thoroughly." }], stream: true, }); let chars = 0; for await (const chunk of stream) { chars += chunk.choices[0]?.delta?.content?.length ?? 0; if (chars > 2000) { stream.controller.abort(); // upstream generation stops; billed to here break; } } ``` ## When a provider dies mid-stream If the serving provider fails after your answer started, RouterPlus does not restart the request on another provider (see the commit boundary in [Routing & failover](/docs/routing)). Instead, you get one terminal error event inside the open 200 stream. **OpenAI surface.** RouterPlus first sends any known usage and its `cost`, so you can recompute the billed partial attempt. Then it sends a final chunk with `finish_reason: "error"` and a top-level `error`: ```text data: {"id":"chatcmpl-2f6d8e3b","object":"chat.completion.chunk","created":1757000000,"model":"claude-sonnet-5","choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}],"error":{"code":"upstream_error","message":"upstream failure","type":"gateway_error","metadata":{"error_type":"upstream_error"}}} ``` **Anthropic surface.** RouterPlus sends a native `error` event with the Anthropic type string and the stable `error_type`: ```text event: error data: {"type":"error","error":{"type":"api_error","message":"upstream failure","error_type":"upstream_error"}} ``` Nothing follows a terminal event: no `[DONE]` and no other frames. The same event ends a stream that reaches the request deadline. It also ends a stream whose provider closed the connection without a proper terminator. > [!NOTE] > The Anthropic wire format has no usage slot outside `message_delta`. So an Anthropic-surface > stream that fails mid-answer cannot carry its partial usage. Read the settled numbers from > `GET /v1/generation?id=`. [Errors & remediation](/docs/errors) lists every `error_type` and what to do about it. ## SDK streaming examples Both official SDKs work unmodified. Change the base URL and the key. ```python import os from openai import OpenAI client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"]) stream = client.chat.completions.create( model="claude-sonnet-5", max_tokens=200, messages=[{"role": "user", "content": "Write a haiku about failover."}], 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: # the final usage chunk has an empty choices list print(f"\ncost: ${chunk.usage.cost}") ``` ```python import os import anthropic client = anthropic.Anthropic(base_url="https://demo-api.routerplus.com", api_key=os.environ["TM_API_KEY"]) with client.messages.stream( model="gpt-4o-mini", # a GPT model over the Anthropic wire max_tokens=200, messages=[{"role": "user", "content": "Write a haiku about failover."}], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) final = stream.get_final_message() print(f"\nstop_reason: {final.stop_reason}") ``` --- Source: https://demo.routerplus.com/docs/response-cache.md # Response cache The response cache answers a repeated request from stored results. A cached answer comes back at once and costs nothing. The cache works on `POST /v1/chat/completions` and `POST /v1/messages`. The cache is off by default. Turn it on for one request, or ask RouterPlus support to turn it on for your organization. ## Request ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \ -d '{ "model": "gpt-6-luna", "temperature": 0, "messages": [{ "role": "user", "content": "Summarize our refund policy." }], "cache": { "mode": "normalized", "ttl_seconds": 3600 } }' ``` Use the `x-tm-cache` header or the `cache` field in the body. The header wins over the field. | `x-tm-cache` value | What happens | |---|---| | `on` | Use the cache for this request. | | `off` | Do not use the cache for this request, even when your organization has it on. | | `refresh` | Do not read the cache. Call the model and store the new answer. | | `semantic` | Use [semantic matching](#semantic-matching). | | `cache` field | Rule | |---|---| | `mode` | `"exact"` (default), `"normalized"` or `"semantic"`. | | `ttl_seconds` | How long the answer stays, 1 to 86400. Default 300. | | `shared` | `false` keeps the answer to one user. See [Scope](#scope). | | `allow_nonzero_temperature` | `true` lets a request with `temperature` above 0 use the cache. | | `semantic` | `true` turns on semantic matching. | `"cache": true` turns the cache on with the defaults. `"cache": false` turns it off. The gateway removes the `cache` field before it sends the request to a model. ## Modes - `exact`: a hit needs the same request. The model, the messages, the system prompt, the tools, `tool_choice`, `response_format` and every sampling parameter (`temperature`, `top_p`, `seed`, `max_tokens`, `stop` and the others) must match. - `normalized`: the same as `exact`, but the gateway trims the text of each message and turns each run of spaces, tabs and new lines into one space. Letter case still counts. The match ignores `stream`, `stream_options`, `user`, `metadata`, `session_id` and `prompt_cache_key`. A streamed request can get an answer that a non-streamed request stored. ## Semantic matching Semantic matching returns a stored answer when the last user turn is close in meaning to a cached one, inside the same [scope](#scope). It is off until RouterPlus sets it up for your organization. Ask for it on a request with `x-tm-cache: semantic`, `"cache": { "semantic": true }` or `"mode": "semantic"`. - The response carries `x-tm-cache: hit` and `x-tm-cache-similarity` (cosine similarity, 0.97 or more by default). - The comparison ignores case, punctuation and spacing. - Each lookup and store embeds the last user turn with `text-embedding-3-small`. This is an ordinary `POST /v1/embeddings` request on your key, billed at its price (about $0.02 per million tokens). ## Scope - A stored answer belongs to one organization. Another organization never gets it. - Inside your organization, a stored answer belongs to one user by default. The user is the request's `user` field (on Messages, `metadata.user_id`) or its session. When the request names neither, the API key is the user. - Your organization can turn on sharing between its users. Then any user of your organization can get an answer that another user stored. - With sharing on, a request can keep its answer to its own user with `"shared": false`. - With sharing off, a request cannot share its answer. ## What a hit looks like A hit is your stored answer with: - a new `id`, and on chat completions a new `created`; - `usage` with the stored token counts, `"cost": 0` and `"cached_response": true`; - the headers `x-tm-cache: hit` and `x-tm-cache-age` (seconds since the answer was stored). A hit costs nothing. When you ask to stream, the gateway sends the stored answer as a stream in the live format: chat completion chunks on `/v1/chat/completions`, and Messages events on `/v1/messages`. ## What is not cached The gateway calls the model and stores nothing when a request: - sets `temperature` above 0, unless `allow_nonzero_temperature` is `true`; - asks for more than one choice (`n` above 1); - uses a tool that runs on the provider's side, such as web search, code execution or an MCP server (tools that your code runs, functions, are cached); - gives an image or a file by URL or by file id (images and files sent inline are cached). The gateway stores only a complete answer. It does not store an error, an answer that ended with an error, or a streamed answer. A streamed request can get only an answer that a non-streamed request stored. --- Source: https://demo.routerplus.com/docs/api-batches.md # Batches The Batches API runs many requests from one file and returns the answers in result files. It uses the shapes of the OpenAI Files and Batches API. The OpenAI SDKs work when you change `base_url` and the key. A batch has four steps: 1. Upload a JSONL file of requests: `POST /v1/files`. 2. Create a batch from the file: `POST /v1/batches`. 3. Poll the batch until it ends: `GET /v1/batches/{batch_id}`. 4. Download the output and error files: `GET /v1/files/{file_id}/content`. Every route also works under `/api/v1/`. ## Example ```python import os, time from openai import OpenAI client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"]) batch_file = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch") batch = client.batches.create( input_file_id=batch_file.id, endpoint="/v1/chat/completions", completion_window="24h", ) while batch.status not in ("completed", "failed", "cancelled", "expired"): time.sleep(30) batch = client.batches.retrieve(batch.id) print(batch.request_counts) if batch.output_file_id: print(client.files.content(batch.output_file_id).text) if batch.error_file_id: print(client.files.content(batch.error_file_id).text) ``` To cancel: ```python client.batches.cancel(batch.id) ``` ## Authentication, billing and governance Send your inference key as `Authorization: Bearer `. Files and batches belong to the key's org. A file or batch of another org returns 404. Each line of a batch runs as an ordinary request to the gateway: - RouterPlus bills it to your org at the normal price of the model and provider that serve it. - Your org, workspace, member and key guardrails apply to it. - It has the same rate limit as your key. - Routing, provider failover and a `models` fallback list work as on a direct request. - It shows in your usage and activity, with your key's labels and finance category. When you create a batch, the gateway makes a key for that batch. The batch key copies these settings from the key that created the batch: workspace, member, labels, finance category, rate limit, routing and key guardrail. The batch key stops working when the batch ends. > [!NOTE] > Your key's spending limit and IP allowlist do not apply to the batch key. To limit batch > spend, use a guardrail budget. ## The input file Put one request on each line, as JSON: ```json {"custom_id": "req-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-6-luna", "messages": [{"role": "user", "content": "Hello"}]}} ``` | Field | Rule | |---|---| | `custom_id` | 1 to 64 characters. Unique in the file. The answer line has the same `custom_id`. | | `method` | `"POST"`. | | `url` | `/v1/chat/completions`, `/v1/messages` or `/v1/responses`. The same on every line. | | `body` | The request body, as you send it to that endpoint. | Limits: - A file has at most 50 MiB. - A file has at most 10,000 requests. - The gateway always sends `stream` as `false`. Each line gets one complete answer. The upload checks every line. A bad line returns 400. The message names the first bad line: ```json {"error": {"message": "Line 2: method must be \"POST\".", "type": "invalid_request_error", "param": "file", "code": "invalid_file"}} ``` ## Files ### Upload Send `POST /v1/files` as `multipart/form-data` with the fields `purpose=batch` and `file`: ```bash curl https://demo-api.routerplus.com/v1/files \ -H "Authorization: Bearer $TM_API_KEY" \ -F purpose=batch \ -F file=@requests.jsonl ``` Or send the file as the body, with `content-type: application/jsonl`: ```bash curl "https://demo-api.routerplus.com/v1/files?purpose=batch&filename=requests.jsonl" \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/jsonl" \ --data-binary @requests.jsonl ``` The response is a file object: ```json {"id": "file-5b2e0c7d9a1f4e3b8c6d0a2f", "object": "file", "bytes": 1832, "created_at": 1793577600, "expires_at": 1796169600, "filename": "requests.jsonl", "purpose": "batch", "status": "processed", "status_details": null} ``` RouterPlus keeps a file for 30 days. ### Other file routes | Route | Response | |---|---| | `GET /v1/files/{file_id}` | The file object. | | `GET /v1/files/{file_id}/content` | The file's JSONL content. | | `DELETE /v1/files/{file_id}` | `{"id": "...", "object": "file", "deleted": true}`. You cannot delete the input file of a batch that has not ended (400). | | `GET /v1/files?purpose=&limit=&after=&order=` | A list of file objects, newest first. `purpose` is `batch`, `batch_output` or `batch_error`. | ## Batches ### Create `POST /v1/batches`: | Field | Rule | |---|---| | `input_file_id` | A file with purpose `batch`. | | `endpoint` | The `url` on the file's lines. | | `completion_window` | `"24h"`. | | `metadata` | Optional. Up to 16 pairs. Keys have up to 64 characters. Values are strings of up to 512 characters. | ```bash curl https://demo-api.routerplus.com/v1/batches \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/json" \ -d '{"input_file_id": "file-5b2e0c7d9a1f4e3b8c6d0a2f", "endpoint": "/v1/chat/completions", "completion_window": "24h"}' ``` The response is a batch object: ```json {"id": "batch_8f3a1c2e4b5d6f708192a3b4", "object": "batch", "endpoint": "/v1/chat/completions", "errors": null, "input_file_id": "file-5b2e0c7d9a1f4e3b8c6d0a2f", "completion_window": "24h", "status": "validating", "output_file_id": null, "error_file_id": null, "created_at": 1793577600, "in_progress_at": null, "expires_at": 1793664000, "finalizing_at": null, "completed_at": null, "failed_at": null, "expired_at": null, "cancelling_at": null, "cancelled_at": null, "request_counts": {"total": 3, "completed": 0, "failed": 0}, "metadata": null} ``` ### Other batch routes | Route | Response | |---|---| | `GET /v1/batches/{batch_id}` | The batch object. `request_counts` grows while the batch runs. | | `POST /v1/batches/{batch_id}/cancel` | The batch object, with status `cancelling`. | | `GET /v1/batches?limit=&after=` | A list of batch objects, newest first. `limit` is 1 to 100, default 20. `after` is a batch id. | ### Statuses | Status | Meaning | |---|---| | `validating` | The batch is waiting to start. | | `in_progress` | Lines are running. | | `finalizing` | All lines ran. The gateway is writing the output and error files. | | `completed` | The output and error files are ready. | | `failed` | The batch could not run. `errors` gives the reason. No line ran after the failure. | | `cancelling` | You cancelled. Lines that already started finish. No new lines start. | | `cancelled` | The cancel is done. The files hold the lines that ran. | | `expired` | The batch did not end within 24 hours of creation. The files hold the lines that ran. Every line that did not run is in the error file with code `batch_expired`. | `request_counts.completed` counts the lines with a 2xx answer. `request_counts.failed` counts all other lines. ### Retries The gateway retries a line that gets 429 or 5xx, or that it cannot send. It makes up to 3 attempts in total. Before each retry it waits for the answer's `retry-after`, up to 60 seconds. With no `retry-after`, it waits 1 second, then 2 seconds. The batch records the last answer. ## Output and error files When a batch ends, `output_file_id` and `error_file_id` name its result files. - A file id is `null` when the file has no lines. - The lines follow the order of the input file. Match them to your requests by `custom_id`. A line with a 2xx answer goes to the output file: ```json {"id": "batch_req_1c9e7a0b3d5f2e4a6c8b0d1f", "custom_id": "req-1", "response": {"status_code": 200, "request_id": "8d1c0f6e-2b7a-4e39-9a51-3c0d2e4f6a7b", "body": {"id": "chatcmpl-1", "object": "chat.completion", "choices": []}}, "error": null} ``` A line with any other answer goes to the error file, with that answer's status and body: ```json {"id": "batch_req_7e2d4c6a8b0f1e3d5c7a9b2e", "custom_id": "req-2", "response": {"status_code": 400, "request_id": "4f2a...", "body": {"error": {"message": "..."}}}, "error": null} ``` A line that got no answer has `response: null` and an `error` with a `code` and `message`. `request_id` is the gateway's id for that request. Use it to find the request in your activity. ## Errors Errors use the OpenAI shape: ```json {"error": {"message": "No batch found with id 'batch_...'.", "type": "invalid_request_error", "param": "batch_id", "code": "batch_not_found"}} ``` | Status | When | |---|---| | 400 | A bad file, field or value. `param` names it. | | 401 | No key, or a key that is not valid. | | 404 | No such file or batch in your org. | | 409 | The batch cannot be cancelled in its status. | | 413 | The upload is larger than 50 MiB. | --- Source: https://demo.routerplus.com/docs/evals.md # Evals An eval compares candidate models on your own cases. RouterPlus sends each case to each model. Then a judge can score the answers, compare them with an expected answer, or select the best answer for each case. When the eval ends, you get a summary for each model: quality, cost and latency. 1. Create the eval with your cases and models: `POST /v1/evals`. 2. Poll the eval until it ends: `GET /v1/evals/{eval_id}`. 3. Read the answers for each case: `GET /v1/evals/{eval_id}/results`. Every route also works under `/api/v1/`. In the console, open Build > Evals. ## Authentication, billing and governance Send your inference key as `Authorization: Bearer `. An eval belongs to the key's org. An eval of another org returns 404. Each candidate call and each judge call runs as an ordinary chat completion: - RouterPlus bills it to your org at the normal price of the model and provider that serve it. - Your org, workspace, member and key guardrails apply. Your key's rate limit applies. - Routing and provider failover work as on a direct request. - It shows in your usage and activity, with your key's labels and finance category. When you create an eval, the gateway makes a key for that eval. The eval key copies these settings from your key: workspace, member, labels, finance category, rate limit, routing and key guardrail. The eval key stops working when the eval ends. Your key's spending limit and IP allowlist do not apply to the eval key. To limit eval spend, use a guardrail budget. ## Create an eval `POST /v1/evals`: | Field | Rule | |---|---| | `name` | 1 to 200 characters. | | `cases` | An array of 1 to 2,000 cases. Send one of `cases` and `file_id`. | | `file_id` | A file with purpose `evals`. See [Eval files](#eval-files). | | `models` | 1 to 10 candidate model ids. | | `judge` | Optional. See [Judge modes](#judge-modes). Without a judge, the summary has cost, latency, tokens and errors only. | | `params` | Optional. `temperature` (0 to 2) and `max_tokens` (1 to 128,000). RouterPlus sends them on every candidate call. | A case: | Field | Rule | |---|---| | `id` | Optional. 1 to 64 characters, unique in the eval. Default `case-1`, `case-2` and so on. | | `input` | A string of up to 200,000 characters, sent as one user message. Or an array of chat messages. | | `expected` | Optional. The reference answer, up to 20,000 characters. Mode `match` needs it on every case. The other modes show it to the judge. | | `criteria` | Optional. What a good answer to this case does, up to 4,000 characters. The judge reads it. | ```bash curl https://demo-api.routerplus.com/v1/evals \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/json" \ -d '{ "name": "support answers", "cases": [ {"input": "How do I reset my password?", "criteria": "Names the settings page."}, {"input": [{"role": "system", "content": "Answer in one word."}, {"role": "user", "content": "Capital of France?"}], "expected": "Paris"} ], "models": ["gpt-6-luna", "deepseek/deepseek-v4.1-flash", "moonshotai/kimi-k3"], "judge": {"model": "gpt-6-luna", "mode": "score", "rubric": "Correct, short and polite."}, "params": {"temperature": 0, "max_tokens": 400} }' ``` The response is the eval object, with status `queued`. ## Judge modes | Mode | What happens | Needs `judge.model` | |---|---|---| | `score` | The judge reads each answer and gives a score from 1 to 10. One judge call per answer. | Yes | | `match` | RouterPlus compares each answer to `expected`. No judge call. | No | | `pairwise` | The judge reads all answers to a case and picks the best one. One judge call per case. Needs 2 or more models. | Yes | - `rubric` is optional, up to 4,000 characters. The judge reads it on every call. - The judge sees the answers with the labels A, B, C. It never sees the model ids. In `pairwise` mode the label order changes from case to case. - In `pairwise` mode, when only one model answered a case, that model wins with no judge call. When no model answered a case, the case has no winner. - In `match` mode, an answer is an `exact` match when it equals `expected` after trimming. It is a `normalized` match when both are equal after these changes: lower case, each run of whitespace as one space, and no quotes, brackets or end punctuation. - A judge reply with no score from 1 to 10, or with no named answer, is a judge error. That answer or case has no score or winner. ## Eval files For many cases, upload a JSONL file with one case on each line. Then send its id as `file_id`: ```json {"id": "q1", "input": "Capital of France?", "expected": "Paris"} {"id": "q2", "input": [{"role": "user", "content": "2 + 2?"}], "expected": "4", "criteria": "A number only."} ``` ```bash curl https://demo-api.routerplus.com/v1/files \ -H "Authorization: Bearer $TM_API_KEY" \ -F purpose=evals \ -F file=@cases.jsonl ``` - The upload checks every line and names the first bad line. - A file holds at most 2,000 cases. - The other file routes are the same as for [batches](api-batches.md). - The eval keeps its own copy of the cases. You can delete the file after you create the eval. ## Read an eval `GET /v1/evals/{eval_id}`: ```json { "id": "eval_3f9a0c1e2b4d6f8091a2b3c4", "object": "eval", "name": "support answers", "status": "completed", "models": ["gpt-6-luna", "deepseek/deepseek-v4.1-flash", "moonshotai/kimi-k3"], "judge": {"model": "gpt-6-luna", "rubric": "Correct, short and polite.", "mode": "score"}, "params": {"temperature": 0, "max_tokens": 400}, "file_id": null, "progress": {"cases_total": 2, "cases_done": 2, "calls_done": 12, "call_errors": 0}, "created_at": 1793577600, "started_at": 1793577601, "finished_at": 1793577640, "error": null, "summary": { "mode": "score", "models": [ {"model": "deepseek/deepseek-v4.1-flash", "cases": 2, "answered": 2, "errors": 0, "mean_score": 8.5, "scored": 2, "match_rate": null, "exact_match_rate": null, "win_rate": null, "wins": 0, "cost_usd": 0.000412, "mean_latency_ms": 1840, "p50_latency_ms": 1620, "p95_latency_ms": 2060, "prompt_tokens": 64, "completion_tokens": 190, "total_tokens": 254} ], "judge": {"model": "gpt-6-luna", "calls": 6, "errors": 0, "cost_usd": 0.000951, "prompt_tokens": 2310, "completion_tokens": 140}, "total_cost_usd": 0.002104 } } ``` `summary` is `null` until the eval ends. A cancelled eval has a summary of the calls that ran. Summary fields for each model: | Field | Meaning | |---|---| | `cases`, `answered`, `errors` | Cases sent to the model, answers with text, and calls that failed or came back empty. | | `mean_score`, `scored` | Mode `score`: the mean judge score and the number of scored answers. | | `match_rate`, `exact_match_rate` | Mode `match`: matched cases over all cases. A failed call counts as no match. | | `win_rate`, `wins` | Mode `pairwise`: cases the model won over cases the judge decided. | | `cost_usd` | The billed cost of the model's calls, in USD. Judge calls are in `judge.cost_usd`. | | `mean_latency_ms`, `p50_latency_ms`, `p95_latency_ms` | Time to the full answer, over answered calls. | | `prompt_tokens`, `completion_tokens`, `total_tokens` | Tokens of the model's calls. | `total_cost_usd` is the cost of all candidate and judge calls. It matches your usage. ### Statuses | Status | Meaning | |---|---| | `queued` | The eval is waiting to start. | | `running` | Cases are running. `progress` grows. | | `completed` | All cases ran. `summary` is ready. | | `failed` | The eval could not run. `error` has a `code` and `message`. | | `cancelled` | You cancelled. Cases that already started finish. No new cases start. | An eval that has not ended 7 days after creation fails with code `eval_expired`. ## Results per case `GET /v1/evals/{eval_id}/results?limit=&offset=` lists the cases in order. `limit` is 1 to 500, default 100. `offset` is the first case's index, default 0. ```json { "object": "list", "eval_id": "eval_3f9a0c1e2b4d6f8091a2b3c4", "status": "completed", "offset": 0, "limit": 100, "total": 2, "has_more": false, "data": [ {"case_index": 1, "case_id": "case-2", "expected": "Paris", "results": [ {"model": "deepseek/deepseek-v4.1-flash", "status": "ok", "answer": "Paris", "answer_truncated": false, "error": null, "score": 10, "matched": null, "won": null, "judge_reason": "Correct and short.", "cost_usd": 0.000021, "judge_cost_usd": 0.00016, "latency_ms": 980, "prompt_tokens": 22, "completion_tokens": 2, "request_id": "8d1c0f6e-2b7a-4e39-9a51-3c0d2e4f6a7b"} ]} ] } ``` - `status` is `ok`, `error` or `pending` (not run yet). - `answer` holds the first 4,000 characters. `answer_truncated` is `true` when the answer was longer. - `score` is the judge score in mode `score`, and 1 or 0 in mode `match`. - In mode `pairwise`, `won` tells if the model won the case. Each case has a `judge` object with `best_model`, `reason` and `cost_usd`. - `request_id` is the gateway's id for the call. Use it to find the call in your activity. ## Other routes | Route | Response | |---|---| | `POST /v1/evals/{eval_id}/cancel` | The eval object, with status `cancelled`. An eval that already ended returns 409. | | `GET /v1/evals?limit=&after=` | A list of eval objects, newest first. `limit` is 1 to 100, default 20. `after` is an eval id. | ## Retries RouterPlus retries a call that gets 429 or 5xx, or that it cannot send. It makes up to 3 attempts in total. Before each retry it waits for the response's `retry-after`, up to 60 seconds. With no `retry-after`, it waits 1 second, then 2 seconds. The eval records the last response. ## Errors Errors use this shape: ```json {"error": {"message": "No eval found with id 'eval_...'.", "type": "invalid_request_error", "param": "eval_id", "code": "eval_not_found"}} ``` | Status | When | |---|---| | 400 | A bad field or value, or a bad eval file. `param` names it. | | 401 | No key, or a key that is not valid. | | 404 | No such eval or file in your org. | | 409 | The eval cannot be cancelled in its status. | | 413 | The request body is larger than 32 MiB. Use an eval file. | --- Source: https://demo.routerplus.com/docs/model-search.md # Optimize [Optimize](https://demo.routerplus.com/model-search) finds a cheaper model or mix that scores as well as your model on your own Braintrust evals. It deploys the result as an endpoint: one model id that you call like any other. > [!NOTE] > Optimize is paused. Searches cannot start, continue or deploy for now. Past results stay > available. A deployed endpoint answers only while every model it calls is in the catalog. ## Before you start 1. In the console, open [Access > Integrations](https://demo.routerplus.com/console/integrations) and connect Braintrust with a Braintrust API key. - RouterPlus checks the key with Braintrust and stores it encrypted. The page shows only the last four characters. - Owners and admins can connect, replace or disconnect the key. Viewers can only look. - Langfuse is on the same page, under **Request access**. The RouterPlus team sets it up with you. 2. In Braintrust, make sure that you have one of these sources to search on: | Source | What the search uses | |---|---| | An **experiment** that ran on a dataset | The dataset is the test cases. The experiment's `metadata.model` is the baseline. The prompt comes from `metadata.prompt_id` or `metadata.prompt_slug`, or is the project's only prompt. | | A **dataset** | The rows are the test cases. You select the prompt and, optionally, a baseline model. Without a baseline, the strongest candidate becomes the baseline. | | **Production logs** | The logged calls of the last 1, 7 or 30 days. Each call is a test case, with the production reply as the expected turn. A candidate scores on whether its reply is as good as the production reply. The baseline is the model that most logged calls used, unless you select one. | The baseline must be a catalog model or one of your own deployed endpoints (`tm/...`). A search takes up to 2,000 cases. For an experiment or a dataset, these parts of the rows are used: | Braintrust | Used as | |---|---| | A row's `input.messages` | One test of the next assistant turn. | | The project's scorers | Quality. A case's score is the mean of its scorers. | | A row's `tags` | The slices in a result's detail view. | | A row's `metadata.conversation_id` and `metadata.goal` | Keep the turns of one conversation together. Simulated customers use the goal. | Code scorers run in Braintrust. Prompt scorers (LLM graders) run through the gateway and bill your credits. ## Start a search 1. Click **New search** at the bottom of the searches list. 2. Select the source and name the search. 3. Set the **search budget**: the most that the search spends on model calls, from $5 to $100. Model calls bill your credits while the search runs. They use a managed key that RouterPlus makes for the search. To see the key, turn on **Show playground keys** on `/console/keys`. The key has a **managed** tag. Viewers cannot start or change a search. The search runs in stages. The budget pays for them in this order: 1. **Shortlist.** Public benchmarks select the open models to try: the Pareto frontier of benchmark score against price. The search tries each model on up to three hosts: the cheapest host, the host with the highest precision, and the model maker's own host. 2. **Pilot.** Every candidate answers a sample of the conversations. 3. **Dev.** The leaders answer all the dev conversations. 4. **Mixes.** The search builds cascades, routers and votes from the saved answers. A search agent proposes mixes to try. Mixes cost nothing extra. 5. **Held-out.** The finalists, and a fusion of the best models, answer conversations that the search never saw. The winner comes from these. 6. **Customers.** The finalists talk to a simulated customer who has the goal of a held-out conversation. | Action | Effect | |---|---| | Budget reached | The search stops and keeps its results. | | **Add $25** | Continues the search from where it stopped. | | **Stop** | Ends the search and keeps its results. | If a Braintrust read fails while the search loads its cases, RouterPlus tries 3 more times, after 2 s, 10 s and 30 s. Then it stops the search. A stopped search shows the reason. Fix the cause (for example, connect the key again) and add budget to continue. ## Read the results The chart plots each candidate's cost against its score. A line shows the best-value frontier. The table below the chart has these columns: | Column | Meaning | |---|---| | Model | The model, or `tm-mix-N` for a mix. Open a row to see what the mix does. | | Provider | The host that served the model, with its precision when the host states it. | | Score | The mean scorer score, in percent. | | $ / 1k req | The measured cost of 1,000 requests like your cases. | | p50 | The median time to a full answer. | | TTFT | The median time to the first token. | | Out tok | Output tokens per answer. | | Status | The stage that scored the row: `pilot`, `dev` or held-out. | The ★ row is the winner. It is the cheapest finalist that meets all three rules: - It costs less than the baseline. - It is not worse than the baseline on a paired test over the same held-out cases. - It scores within 2 points of the baseline. When two rows cost the same, the row with the lower median latency wins. Rows marked `dev` or `pilot` scored on fewer conversations and cannot win. If no row meets the rules, no row has a star. Open a row to see the cases where it did worse than the baseline, side by side, and its score for each tag. ## Deploy the result **Deploy** makes a result callable as `tm/-v1`. The name has lowercase letters, digits and dashes, 40 characters at most: ```bash curl https://demo-api.routerplus.com/v1/chat/completions \ -H "Authorization: Bearer $TM_API_KEY" \ -H "content-type: application/json" \ -d '{"model": "tm/billing-agent-v1", "messages": [{"role": "user", "content": "Hi"}]}' ``` - Only keys of your organization can call it. - RouterPlus bills each model call that the endpoint makes as a direct call to that model. `usage.cost` in the response is the sum. - Send it on `POST /v1/chat/completions`. With `"stream": true`, it returns the finished answer as one chunk, then usage. - A second deploy makes a new version (`-v2`). Earlier versions continue to work. [Endpoints](https://demo.routerplus.com/endpoints) lists everything that your organization deployed, with the search that each result came from. **Try it** sends one held-out case of that search to the endpoint, and bills your credits like any call. ## What is stored A search stores its cases and the candidates' answers, for your organization only, until you delete the search. A deployed model id continues to work after you delete its search. --- Source: https://demo.routerplus.com/docs/broadcast.md # Broadcast to storage and warehouses Broadcast sends a trace of each request to the destinations you configure. This page covers the storage and warehouse kinds: `s3`, `azure_blob`, `bigquery` and `snowflake`. In the console, open Build > Exports and alerts. The routes and the `sampling`, `privacy` and `filter` fields are the same for every kind. See [Broadcast destinations](api-management.md#broadcast-destinations). Each trace is one JSON object, after your privacy rules apply. A trace leaves out prompt and completion content unless `privacy.include_content` is `true`. ## Delivery - The gateway sends traces in batches of up to 50, at least every 2 seconds. - Each request has a 10-second timeout. - The gateway retries a failed batch up to 3 times, after 0.5, 1 and 2 seconds. - It retries on a `5xx`, `408` or `429` status, or a network error. It does not retry other errors. - A retry sends the same batch with the same batch id. Storage writes the same object again. BigQuery and Snowflake drop the duplicate rows. See each kind below. `POST /v1/broadcast/destinations/:id/test` sends one sample trace. It returns `{ok, status}`. On a failure it returns `{ok: false, status?, error}`, with the reason in `error`. ## Object layout (`s3` and `azure_blob`) Each batch is one object. Each line of the object is one trace (NDJSON). ``` /dt=YYYY-MM-DD/hour=HH/.ndjson /dt=YYYY-MM-DD/hour=HH/.ndjson.gz (with "gzip": true) ``` - The date and hour are the UTC time at which the gateway sent the batch. - `` is a UUID. The same traces always give the same id. - With no `prefix`, the key starts at `dt=`. - A plain object has `Content-Type: application/x-ndjson`. A gzip object has `Content-Type: application/gzip`. The `dt=` and `hour=` folders are Hive-style partitions. Athena, BigQuery external tables, Snowflake external stages and Spark read them as columns. ## Amazon S3 and S3-compatible storage (`s3`) ```json { "kind": "s3", "config": { "bucket": "acme-llm-traces", "region": "us-east-1", "prefix": "routerplus", "access_key_id": "AKIA...", "secret_access_key": "...", "gzip": true } } ``` | Field | Rule | |---|---| | `bucket` | Required. An S3 bucket name. | | `region` | Required. The bucket's region. Use `auto` for Cloudflare R2. | | `prefix` | Optional. Folders before `dt=`. The gateway removes a leading and a trailing `/`. | | `endpoint` | Optional. The origin of an S3-compatible store, such as `https://.r2.cloudflarestorage.com`. https only, except `http://localhost` for MinIO. No path. | | `access_key_id` | Required. | | `secret_access_key` | Required. Secret. | | `session_token` | Optional. For temporary credentials. Secret. | | `gzip` | Optional. `true` compresses each object. Default `false`. | - Requests use AWS Signature Version 4 with a signed payload hash. - With no `endpoint`, the URL is `https://.s3..amazonaws.com/`. - With an `endpoint`, or a bucket name with a dot, the URL is path style: `//`. - The access key needs `s3:PutObject` on `arn:aws:s3::://*`. It needs no read access. ## Azure Blob Storage (`azure_blob`) ```json { "kind": "azure_blob", "config": { "account": "acmetraces", "container": "routerplus", "prefix": "prod", "sas_token": "sv=2022-11-02&ss=b&srt=o&sp=cw&sig=..." } } ``` | Field | Rule | |---|---| | `account` | Required. The storage account name. | | `container` | Required. The container must exist. | | `prefix` | Optional. Folders before `dt=`. | | `sas_token` | A SAS token with create and write permission. The gateway removes a leading `?`. Secret. | | `account_key` | The storage account key (base64). The gateway signs requests with Shared Key. Secret. | | `gzip` | Optional. `true` compresses each object. Default `false`. | Send one of `sas_token` and `account_key`. Each batch is a Put Blob request with `x-ms-blob-type: BlockBlob`. ## Google BigQuery (`bigquery`) ```json { "kind": "bigquery", "config": { "project_id": "acme-analytics", "dataset": "llm", "table": "routerplus_events", "service_account_json": "{\"client_email\": \"...\", \"private_key\": \"-----BEGIN PRIVATE KEY-----\\n...\", \"token_uri\": \"https://oauth2.googleapis.com/token\"}" } } ``` | Field | Rule | |---|---| | `project_id` | Required. | | `dataset` | Required. | | `table` | Required. The table must exist. | | `service_account_json` | Required. The service account key file, as a string or an object. The gateway keeps `client_email`, `private_key`, `token_uri` and `private_key_id`. Secret. | - The service account needs `bigquery.tables.updateData` on the table. The role `roles/bigquery.dataEditor` on the dataset has it. - The gateway gets an access token with the `https://www.googleapis.com/auth/bigquery.insertdata` scope. It uses the token again until one minute before it expires. - The gateway inserts rows with `tabledata.insertAll`. Each row's `insertId` is the event id, so BigQuery drops a row that arrives twice. - A row that BigQuery rejects fails its batch. The gateway logs the row index and the reason. Create the table: ```sql CREATE TABLE `acme-analytics.llm.routerplus_events` ( event_id STRING NOT NULL, -- the attempt id (generation_id), else the request id received_at TIMESTAMP NOT NULL, -- when the gateway sent the batch event JSON NOT NULL -- the trace ) PARTITION BY DATE(received_at); ``` Query it: ```sql SELECT JSON_VALUE(event, '$.model') AS model, SUM(CAST(JSON_VALUE(event, '$.cost_usd') AS FLOAT64)) AS cost_usd FROM `acme-analytics.llm.routerplus_events` WHERE DATE(received_at) = CURRENT_DATE() GROUP BY model; ``` ## Snowflake (`snowflake`) ```json { "kind": "snowflake", "config": { "account": "xy12345.us-east-1", "user": "ROUTERPLUS_LOADER", "private_key_pem": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", "database": "ANALYTICS", "schema": "LLM", "table": "ROUTERPLUS_EVENTS", "warehouse": "LOAD_WH", "role": "ROUTERPLUS_LOADER" } } ``` | Field | Rule | |---|---| | `account` | Required. The account identifier from your Snowflake URL, such as `xy12345.us-east-1` or `myorg-myaccount`. | | `user` | Required. | | `private_key_pem` | Required. An unencrypted RSA private key in PEM (PKCS#8 or PKCS#1). Secret. | | `database`, `schema`, `table` | Required. Unquoted identifiers: letters, digits, `_` and `$`. | | `warehouse`, `role` | Optional. Default: the user's defaults. | The gateway signs in with key-pair authentication. It sends one SQL API request (`POST /api/v2/statements`) per batch: a parameterized multi-row insert with the traces as bindings. ```sql INSERT INTO ANALYTICS.LLM.ROUTERPLUS_EVENTS (event) SELECT PARSE_JSON(column1) FROM VALUES (?), (?), ... ``` Each request has a `requestId` from the batch id. A retry sets `retry=true`. Snowflake then does not run the same batch twice. Set up the user, the key and the table: ```sql -- Generate the key pair (shell): -- openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt -out routerplus_key.p8 -- openssl rsa -in routerplus_key.p8 -pubout -out routerplus_key.pub CREATE ROLE ROUTERPLUS_LOADER; CREATE USER ROUTERPLUS_LOADER TYPE = SERVICE DEFAULT_ROLE = ROUTERPLUS_LOADER DEFAULT_WAREHOUSE = LOAD_WH RSA_PUBLIC_KEY = 'MIIBIjANBgkqh...'; -- routerplus_key.pub without the BEGIN and END lines GRANT ROLE ROUTERPLUS_LOADER TO USER ROUTERPLUS_LOADER; CREATE TABLE ANALYTICS.LLM.ROUTERPLUS_EVENTS ( event VARIANT NOT NULL, received_at TIMESTAMP_LTZ NOT NULL DEFAULT CURRENT_TIMESTAMP() ); GRANT USAGE ON WAREHOUSE LOAD_WH TO ROLE ROUTERPLUS_LOADER; GRANT USAGE ON DATABASE ANALYTICS TO ROLE ROUTERPLUS_LOADER; GRANT USAGE ON SCHEMA ANALYTICS.LLM TO ROLE ROUTERPLUS_LOADER; GRANT INSERT ON TABLE ANALYTICS.LLM.ROUTERPLUS_EVENTS TO ROLE ROUTERPLUS_LOADER; ``` Query it: ```sql SELECT event:model::string AS model, SUM(event:cost_usd::float) AS cost_usd FROM ANALYTICS.LLM.ROUTERPLUS_EVENTS WHERE received_at >= DATEADD(day, -1, CURRENT_TIMESTAMP()) GROUP BY model; ``` ## Secrets A read returns each secret as `"__secret__"`. To keep a stored secret on an update, send `"__secret__"` in its place. | `kind` | Secret fields | |---|---| | `s3` | `secret_access_key`, `session_token` | | `azure_blob` | `sas_token`, `account_key` | | `bigquery` | `service_account_json` | | `snowflake` | `private_key_pem` | --- Source: https://demo.routerplus.com/docs/slack-agents.md # Slack agents A Slack agent connects your own Slack app to RouterPlus. People mention the app in a channel or send it a direct message. RouterPlus answers in the same thread, with the model you select. Each agent has its own RouterPlus inference key. Every answer is an ordinary request on that key. Your guardrails, budgets, routing and billing apply. ## Set up an agent 1. Create a Slack app at api.slack.com/apps: - Select "From scratch" and install the app in your workspace. - Copy the bot token (`xoxb-...`) from "OAuth & Permissions". - Copy the signing secret from "Basic Information". 2. Create the agent with a management key (`Authorization: Bearer tm_mk_...`): ``` POST /v1/slack/agents ``` ```json { "slug": "helpdesk", "bot_token": "xoxb-...", "signing_secret": "8f74...", "model": "gpt-6-luna", "system_prompt": "You answer questions about our product.", "allowed_channel_ids": ["C0123ABCD"], "allowed_user_ids": [], "tools": { "server": [{ "type": "web_fetch" }], "mcp_servers": ["docs"] }, "max_cost_usd_per_day": 10 } ``` 3. Get the app manifest: ``` GET /v1/slack/agents/{id}/manifest ``` 4. In your Slack app, open "App Manifest", paste the manifest and save. Slack checks the events URL. When Slack asks, install the app again to give it the new scopes. 5. Invite the app to a channel (`/invite @helpdesk`). Mention it, or send it a direct message. `POST` returns `201` with the agent. Slack sends events to the agent's `events_url`: `https:///v1/slack/events/{id}`. ## Fields | Field | Rule | |---|---| | `slug` | Required. 1 to 40 of `a-z`, `0-9` and `-`, starting with a letter or digit. Unique in your org. | | `bot_token` | Required. The bot token, `xoxb-...`. Stored encrypted. Never returned. | | `signing_secret` | Required. The app's signing secret. Stored encrypted. Never returned. | | `model` | Required. A model id, a preset (`@preset/`) or `routerplus/auto`. | | `system_prompt` | Optional. Up to 8,000 characters. | | `allowed_channel_ids` | Optional. Channel ids where the agent answers. Empty (the default) means every channel that the app is in. This list does not limit direct messages. | | `allowed_user_ids` | Optional. User ids that the agent answers, in channels and in direct messages. Empty (the default) means anyone in the workspace. | | `tools.server` | Optional. Server tools: `{ "type": "web_fetch" }` with optional `allowed_domains`, `max_bytes` and `max_calls`, and `{ "type": "advisor", "model": "..." }`. Not with `routerplus/auto`. See [Server tools](server-tools.md). | | `tools.mcp_servers` | Optional. Slugs of your MCP servers. The agent calls them through the [MCP gateway](mcp-gateway.md) with its own key, so their tool policies apply. | | `max_cost_usd_per_day` | Optional. The most that the agent's key spends in one UTC day. `null` (the default) means no daily limit. | | `workspace_id`, `principal_id` | Optional, on create only. The workspace and principal of the agent's key. Default: your default workspace. | | `slack_team_id`, `slack_app_id` | Optional. When set, the agent ignores events from another Slack workspace or app. | | `enabled` | Optional. Default `true`. A disabled agent acknowledges events but does not answer. | ## Manage agents | Route | What it does | |---|---| | `GET /v1/slack/agents` | Lists your agents. | | `POST /v1/slack/agents` | Creates an agent and its key. | | `GET /v1/slack/agents/{id}` | Reads one agent. | | `PATCH /v1/slack/agents/{id}` | Changes the fields you send. Send `bot_token` or `signing_secret` to rotate it. | | `DELETE /v1/slack/agents/{id}` | Deletes the agent and its thread memory, and disables its key. | | `GET /v1/slack/agents/{id}/manifest` | The Slack app manifest for the agent. | - Every route also works under `/api/v1`. - Each change writes a row to the audit log (`GET /v1/audit`). - Responses show `has_bot_token` and `has_signing_secret`, never the values. - The agent's key is a normal inference key with the name `slack:`. It shows in `GET /v1/keys` and in activity. To stop all spend immediately, disable the key or the agent. ## Scopes and events The manifest asks for these bot scopes: | Scope | Use | |---|---| | `app_mentions:read` | Receive mentions of the app. | | `chat:write` | Post answers. | | `im:history`, `im:read`, `im:write` | Receive and answer direct messages. | | `channels:history` | Read messages in threads of public channels. | It subscribes to two bot events: `app_mention` and `message.im`. ## How the agent answers - A mention in a channel gets an answer in the thread of that message. - A direct message gets an answer in the direct message. Direct messages outside a thread are one conversation. - The agent does not answer bots (itself included), edits, deletions, or people who are not on the allow lists. - RouterPlus removes the app mention from the text before the model sees it. - The model sees the system prompt, the thread's earlier messages (up to 30), and the new message. - When the agent's key has reached `max_cost_usd_per_day` today, the agent posts a short notice and does not call the model. - When the model call fails, the agent posts a short message, for example "Sorry, I could not answer that. Try again later." ## Security - RouterPlus checks Slack's signature on every request. The signature is an HMAC-SHA256 of the timestamp and the body, with your signing secret. A request without a valid signature gets `401`. - A request with a timestamp more than 5 minutes from now gets `401`. Thus nobody can send a captured request again. - Slack sends an event again when it gets no acknowledgement in time. RouterPlus answers each event id one time. - RouterPlus acknowledges each event immediately and answers in the background. ## What is stored | Data | How long | |---|---| | Bot token, signing secret, agent key | Encrypted, until you delete the agent. | | Thread memory: the last 30 messages of each thread, from people and from the agent | 7 days after the thread's last message, then deleted. | | Event ids, to answer each event once | 1 day. | RouterPlus keeps the model requests like any other request on your key. See [Data policy](data-policy.md). --- Source: https://demo.routerplus.com/docs/guardrails.md # Guardrails A guardrail is a set of rules for requests. You assign it to the organization, a workspace, a member or a key. Every guardrail that covers a request applies. When two guardrails disagree, the stricter rule wins. Guardrails apply to `POST /v1/chat/completions`, `/v1/messages`, `/v1/responses`, `/v1/images/generations`, `/v1/videos` and `/v1/decisions`. You manage them with the [management API](api-management.md). In the console, open Governance > Guardrails for the rules. Open Governance > Budgets for the spending limits. ## Create and assign a guardrail 1. Create the guardrail with a management key: ```bash curl https://demo-api.routerplus.com/v1/guardrails \ -H "Authorization: Bearer $TM_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "support-bot", "blocked_models": ["openai/gpt-oss-20b"], "content_rules": [{"kind": "pii", "preset": "email", "action": "redact"}]}' ``` The response is `201`. `data` holds the guardrail and its `guardrail_id`. 2. Assign the guardrail. It has no effect until you do. `target_kind` is the organization, a workspace, a member or a key: ```bash curl https://demo-api.routerplus.com/v1/guardrails/$GUARDRAIL_ID/assignments \ -H "Authorization: Bearer $TM_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{"target_kind": "org", "target_id": "'$ORG_ID'"}' ``` A change applies within 5 seconds. With the guardrail above: - A call to `openai/gpt-oss-20b` returns `403` with `error.code` `guardrail_blocked`. - A call that contains an email address goes to the provider with `[REDACTED:email]` in its place. The response carries `x-tm-guardrail-actions: redacted`. ## Rules Unknown fields are refused. | Field | Type | Effect | |---|---|---| | `allowed_models` | string[] | Only these models. `*` matches any run of characters, `?` one character. Empty means every model. | | `blocked_models` | string[] | Never these models. | | `allowed_providers` | string[] | Only these providers. Empty means every provider. | | `blocked_providers` | string[] | Never these providers. | | `zdr` | boolean | Only providers that keep no prompts or completions. | | `data_collection` | `"deny"` | Only providers that do not train on or keep request data. | | `allowed_regions` | string[] | Only providers that process requests in these regions (`us`, `eu`, `in`, ...). `global` allows all. | | `blocked_infrastructure` | string[] | Never providers that run on these clouds (`aws`, `gcp`, `azure`, ...). | | `budgets` | object[] | Spending limits. See [Budgets](#budgets). | | `limit_usd`, `reset_interval`, `include_byok_in_budgets` | | One budget in a short form: a USD limit, its period, and whether BYOK spend counts. | | `rpm`, `tpm` | integer | Requests and tokens per minute for each key the guardrail covers. | | `max_output_tokens` | integer | The most output a request may ask for. RouterPlus lowers a larger limit to this value. | | `server_tools` | `"allow"` or `"block"` | `block` refuses requests that ask the provider to run tools (web search, web fetch, code execution). | | `content_rules` | object[] | Rules on request text. See [Content rules](#content-rules). | | `on_scan_limit` | `"allow"` or `"block"` | What to do with a request too long to scan in full. RouterPlus scans the first 2,000,000 characters. Default `allow`. | | `overridable` | `false` | Makes the guardrail a hard limit. See [Grants](#grants). | A filter on a provider fact leaves out each provider for which RouterPlus does not know that fact. For example, `zdr: true` leaves out a provider that does not publish its retention. ### Regions and infrastructure - RouterPlus records a provider's regions and infrastructure only from the provider's published terms: its privacy policy, data processing terms, subprocessor list or docs. - `allowed_regions` keeps only providers whose terms state where they process requests. - RouterPlus does not record a region that a provider offers only as an option RouterPlus does not use. - `blocked_infrastructure` leaves out a provider that names a blocked cloud for serving or hosting. - `GET /v1/models//endpoints` shows each provider's `regions` and `infrastructure` under `data_policy`. ### Budgets ```json { "budgets": [ { "id": "monthly", "limit_usd": 500, "period": "month" }, { "id": "opus", "limit_usd": 50, "period": "week", "model": "claude-opus-*" }, { "id": "team-pool", "limit_usd": 2000, "period": "month", "shared": true, "include_byok": true } ] } ``` - `period` is `day`, `week` (from Monday), `month` or `lifetime`. Periods start at 00:00 UTC. A budget with no period is `lifetime`. - `model` and `provider` count only the spend on that model or provider. - `include_byok` also counts the provider cost of requests on your own provider keys. - `shared: true` makes one budget for everything the guardrail is assigned to. Without it, each assignment has its own budget. For example, a guardrail on a workspace counts the spend of that workspace. - A key also has its own limit: `limit` with `limit_reset` on the key. RouterPlus refuses a request when its budget is used up. Requests that are already running finish. Spend can go past the limit by the cost of those requests. ### Content rules ```json { "content_rules": [ { "kind": "pii", "preset": "credit_card", "action": "block" }, { "kind": "pii", "preset": "email", "action": "redact" }, { "kind": "regex", "pattern": "PROJ-[0-9]{4}", "action": "redact", "label": "project_code" }, { "kind": "injection", "action": "block" } ] } ``` - `pii` presets: `email`, `phone`, `ssn`, `credit_card`, `iban`, `ip_address`, `credential`, `aws_access_key`, `private_key`, `jwt`, `us_passport`. - `regex`: up to 512 characters, with flags `i`, `m`, `s` and `u`. A pattern with lookarounds, backreferences or nested quantifiers returns `invalid_regex_pattern`. - `injection` finds prompt-injection and jailbreak patterns in user text and tool results. - `action`: - `redact` puts `[REDACTED: