Console
Guides/Migrate

Migrate

From OpenAI, Anthropic, or OpenRouter — copy-paste diffs.

/llms.txt

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) — the gateway translates requests, streams, and errors in both directions. The exact rules are on Wire compatibility.

Note

No key yet? Open Sign up, complete Clerk authentication and email verification, and save the first key shown after sign-in. Buy credits in Billing; signup does not add free credit. Existing customers can sign in with the same verified email and create a key at API 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); 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:

OpenRouterHere
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
Variant suffixes (:free, :floor, :nitro)None. A suffixed id is just an unlisted id and returns 404
usage.cost in creditsusage.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 usageGET 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 402HTTP 429 insufficient_quota — byte-compatible with what OpenAI's own SDKs already handle
route, the models fallback array, transformsDropped 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. 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.

Migrate with one prompt

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

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.

Markdown source for agents: /docs/migration.md · index at /llms.txt