# Migrate

Most migrations are three changes: the base URL, the API key, and — if you are
coming from another gateway — the model id. The gateway speaks both major wire
dialects, so your SDK stays:

- OpenAI-compatible: `POST https://dev-api.routerplus.com/v1/chat/completions`, and
  `POST https://dev-api.routerplus.com/v1/responses` for OpenAI's Responses API
- Anthropic-compatible: `POST https://dev-api.routerplus.com/v1/messages`

Every chat model is callable from both surfaces (image models use [POST /v1/images/generations](/docs/api-images)) — the gateway translates requests,
streams, and errors in both directions. The exact rules are on
[Wire compatibility](/docs/compat).

> [!NOTE]
> No key yet? Open [Sign up](https://dev.routerplus.com/signup), complete Clerk authentication
> and email verification, and save the first key shown after sign-in. Buy credits
> in [Billing](https://dev.routerplus.com/console/billing); signup does not add free credit. Existing customers can
> [sign in](https://dev.routerplus.com/login) with the same verified email and create a key at
> [API keys](https://dev.routerplus.com/console/keys). Browser signup replaces the retired
> programmatic signup route.

Auth is forgiving on purpose: every route accepts the key as either
`Authorization: Bearer $TM_API_KEY` or `x-api-key: $TM_API_KEY`, so whichever
header your SDK sends, it works.

## From the OpenAI SDK

Two lines. `gpt-*` model ids are the same bare ids the provider uses
(`gpt-4o-mini`, `gpt-4.1-mini`, `gpt-4o`), so model strings usually survive
untouched.

```diff
 from openai import OpenAI

 client = OpenAI(
-    api_key=os.environ["OPENAI_API_KEY"],
+    base_url="https://dev-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://dev-api.routerplus.com/v1",
+  apiKey: process.env.TM_API_KEY,
 });
```

The same client now reaches Claude models too — no second SDK:

```python
client.chat.completions.create(
    model="claude-sonnet-4-5",  # Anthropic model, OpenAI wire — the gateway translates
    stream=True,
    messages=[{"role": "user", "content": "hello"}],
)
```

> [!NOTE]
> On the chat surfaces, when a request sends no output limit (`max_tokens` or
> `max_completion_tokens`), the gateway sets the model's own maximum output,
> lowered when needed so that the input and the output fit the model's context
> window. On the Responses API it adds no limit, and OpenAI applies the model's
> own maximum. Either way, long answers and long tool calls are not cut short.
> While such a request runs, the balance hold counts at most 32,768 output
> tokens, and you pay for the real usage. A limit that you send (`max_tokens` on
> the chat surfaces, `max_output_tokens` on the Responses API) is held in full.

Code on OpenAI's Responses API (`client.responses.create`) works the same way,
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.
It takes a provider block in `~/.codex/config.toml`:

```toml
model = "gpt-6-luna"
model_provider = "routerplus"

[model_providers.routerplus]
name = "RouterPlus"
base_url = "https://dev-api.routerplus.com/v1"
env_key = "TM_API_KEY"
wire_api = "responses"
```

Then run `codex` with `TM_API_KEY` set. The full setup, with the key in the file
for the desktop app, is in [Coding agents](/docs/install#codex).

## From the Anthropic SDK

Same shape: base URL plus key. The Anthropic-compatible surface lives at the
gateway root (no `/v1` suffix in `base_url` — the SDK appends `/v1/messages`
itself).

```diff
 from anthropic import Anthropic

 client = Anthropic(
-    api_key=os.environ["ANTHROPIC_API_KEY"],
+    base_url="https://dev-api.routerplus.com",
+    api_key=os.environ["TM_API_KEY"],
 )
```

Claude model ids match Anthropic's own (`claude-sonnet-4-5`,
`claude-haiku-4-5`, dated snapshots included) — and GPT models are callable
from this SDK too, over the same `/v1/messages` wire.

**Claude Code** — environment variables only, no config file:

```bash
ANTHROPIC_BASE_URL=https://dev-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY claude
```

Your `anthropic-beta` header is forwarded when an Anthropic-dialect provider
serves the request, filtered to an allowlist of betas that do not change what a
token costs (`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)); a dropped value is named in
`x-tm-dropped-params`. Claude Code's output limit, auto mode and advisor notes
are in [Coding agents](/docs/install#output-limit-auto-mode-and-the-advisor). Claude Code's `?beta=true` query
string is tolerated and routes normally (query strings are not forwarded
upstream).

## From another gateway

If your code calls another multi-provider gateway over the OpenAI wire, repoint
the client as in [From the OpenAI SDK](#from-the-openai-sdk), then change the
model ids. Usage arrives in the final stream chunk with a `cost` field, and the
optional `HTTP-Referer` / `X-Title` attribution headers work here too.

```diff
 completion = client.chat.completions.create(
-    model="anthropic/claude-sonnet-4.5",
+    model="claude-sonnet-4-5",
     stream=True,
     messages=[{"role": "user", "content": "hello"}],
 )
```

What changes, honestly:

| Before | Here |
|---|---|
| `author/model` slugs (`anthropic/claude-sonnet-4.5`) | Bare provider ids (`claude-sonnet-4-5`) — the full list is [https://dev.routerplus.com/api/models.json](https://dev.routerplus.com/api/models.json) |
| Variant suffixes (`model:variant`) | None. A suffixed id is just an unlisted id and returns 404 |
| `usage.cost` in credits | `usage.cost` in USD, computed with the exact integer math the ledger settles with, covering the full request debit — failed-over attempts included |
| A usage lookup by generation id | `GET https://dev-api.routerplus.com/v1/generation?id=REQUEST_ID` — returns every physical attempt with per-attempt tokens, outcome, and settled cost |
| Out of credits → HTTP 402 | HTTP 429 `insufficient_quota` — byte-compatible with what OpenAI's own SDKs already handle |
| `route`, a `models` fallback array, `transforms` | Dropped on every route, and the drop is named in the `x-tm-dropped-params` header. Remove them |
| A `provider` object (`sort`, `max_price`, `allow_fallbacks`, …) | A different shape here: `only`, `ignore`, `order`, `allow_fallbacks`, `require_parameters` and the connection controls on [Routing policies](/docs/routing-policies). `sort` takes only `"price"`, and `max_price` takes `prompt` and `completion` in USD per million. Other keys are a 400. Remove or translate them |

> [!WARNING]
> There is no silent model substitution here, ever. An id not in the catalog is
> an honest 404 naming what you asked for — that is a design guarantee, not a
> limitation. Check every migrated model string against
> `https://dev.routerplus.com/api/models.json` before you ship.

Failover exists, but it never switches providers mid-answer: if a provider dies
after output started, you get the tokens streamed so far plus one clean
terminal error event, and retry semantics stay yours. Details on
[Wire compatibility](/docs/compat).

## Migrate with one prompt

Paste this into your coding agent and let it do the whole thing:

```text
Migrate this project to RouterPlus.

1. If TM_API_KEY is not set, have me open https://dev.routerplus.com/signup and complete
   Clerk authentication and email verification in the browser. Existing customers
   sign in at https://dev.routerplus.com/login and create a key at https://dev.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://dev-api.routerplus.com/v1", api_key -> TM_API_KEY
   - Anthropic SDK:
       base_url -> "https://dev-api.routerplus.com", api key -> TM_API_KEY
   - Claude Code:
       run with ANTHROPIC_BASE_URL=https://dev-api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY
   - Codex: add a [model_providers.routerplus] block to ~/.codex/config.toml with
       base_url "https://dev-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 route/models/transforms request fields.
     Inside a "provider" object keep only: only, ignore, order, allow_fallbacks,
     require_parameters, sort ("price" only), max_price (prompt, completion in USD
     per million); remove the rest.
   - Raw HTTP: change the host to https://dev-api.routerplus.com and the bearer/x-api-key to TM_API_KEY.

3. Check every model id against https://dev.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 proves the whole path — auth, routing, billing:

```bash
curl -N https://dev-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 final usage chunk must carry `cost` (USD). Then confirm the ledger saw it:

```bash
curl -s -H "Authorization: Bearer $TM_API_KEY" https://dev-api.routerplus.com/v1/usage
```

Every served response also carries an `x-tm-provider` header naming the deployment
that served it and `x-tm-attempts` counting physical dispatches — useful when
verifying that traffic really moved. If anything fails, every error body has a
stable `error_type`; the remediation table is on [Errors](/docs/errors).
