# Migrate

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

- OpenAI-compatible: `POST https://api.routerplus.com/v1/chat/completions`
- Anthropic-compatible: `POST https://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://app.routerplus.com/signup), complete Clerk authentication
> and email verification, and save the first key shown after sign-in. Buy credits
> in [Billing](https://app.routerplus.com/console/billing); signup does not add free credit. Existing customers can
> [sign in](https://app.routerplus.com/login) with the same verified email and create a key at
> [API keys](https://app.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://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://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]
> When a request omits `max_tokens` (or `max_completion_tokens`), the gateway
> writes the default of 4096 into it before dispatch, on every route. Send
> `max_tokens` explicitly if you want a different ceiling; 32,768 is the most a
> request may ask for.

**Codex CLI** — current releases do not work: since February 2026 Codex calls only the
OpenAI Responses API, which the gateway does not implement (`POST /v1/responses` is a
404). A Codex release from before February 2026 takes a provider block in
`~/.codex/config.toml`:

```toml
[model_providers.tm]
name = "RouterPlus"
base_url = "https://api.routerplus.com/v1"
env_key = "TM_API_KEY"
wire_api = "chat"

[profiles.tm]
model_provider = "tm"
model = "gpt-4o-mini"
```

Then run `TM_API_KEY=$TM_API_KEY codex --profile tm`.

## 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://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://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
like — the full list is on [Authentication](/docs/authentication)); a dropped
value is named in `x-tm-dropped-params`. Claude Code's `?beta=true` query
string is tolerated and routes normally (query strings are not forwarded
upstream).

## From OpenRouter

The wire is deliberately close: same OpenAI-compatible endpoint shape, usage in
the final stream chunk, a `cost` field in `usage`, and the same optional
`HTTP-Referer` / `X-Title` attribution headers.

```diff
 client = OpenAI(
-    base_url="https://openrouter.ai/api/v1",
-    api_key=os.environ["OPENROUTER_API_KEY"],
+    base_url="https://api.routerplus.com/v1",
+    api_key=os.environ["TM_API_KEY"],
 )

 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:

| OpenRouter | Here |
|---|---|
| `author/model` slugs (`anthropic/claude-sonnet-4.5`) | Bare provider ids (`claude-sonnet-4-5`) — the full list is [https://app.routerplus.com/api/models.json](https://app.routerplus.com/api/models.json) |
| Variant suffixes (`:free`, `:floor`, `:nitro`) | 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 |
| `GET /api/v1/generation?id=` for post-hoc usage | `GET https://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`, the `models` fallback array, `transforms` | Dropped on every route, and the drop is named in the `x-tm-dropped-params` header. Remove them |
| The `provider` object (`sort`, `max_price`, `allow_fallbacks`, …) | A different shape here: `only`, `ignore`, `order`, `upstream`, `allow_fallbacks`, `require_parameters` and the connection controls on [Routing policies](/docs/routing-policies). Any other key inside `provider` — `sort`, `max_price` — is a 400. Remove or translate it |

> [!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://app.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://app.routerplus.com/signup and complete
   Clerk authentication and email verification in the browser. Existing customers
   sign in at https://app.routerplus.com/login and create a key at https://app.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://api.routerplus.com/v1", api_key -> TM_API_KEY
   - Anthropic SDK:
       base_url -> "https://api.routerplus.com", api key -> TM_API_KEY
   - Claude Code:
       run with ANTHROPIC_BASE_URL=https://api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY
   - Codex CLI (only a release from before February 2026; current Codex calls only
     the Responses API, which the gateway does not implement):
       add a [model_providers.tm] block with base_url "https://api.routerplus.com/v1",
       env_key "TM_API_KEY", wire_api "chat", and a [profiles.tm] using it.
   - OpenRouter clients: 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, upstream, allow_fallbacks,
     require_parameters; remove the rest (sort, max_price, ...).
   - Raw HTTP: change the host to https://api.routerplus.com and the bearer/x-api-key to TM_API_KEY.

3. Check every model id against https://app.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://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://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).
