Migrate
From OpenAI, Anthropic, or OpenRouter — copy-paste diffs.
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.
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.
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"],
) 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:
client.chat.completions.create(
model="claude-sonnet-4-5", # Anthropic model, OpenAI wire — the gateway translates
stream=True,
messages=[{"role": "user", "content": "hello"}],
)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:
[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).
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:
ANTHROPIC_BASE_URL=https://api.routerplus.com ANTHROPIC_AUTH_TOKEN=$TM_API_KEY claudeYour 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.
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 |
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. Any other key inside provider — sort, max_price — is a 400. Remove or translate it |
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:
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:
curl -s -H "Authorization: Bearer $TM_API_KEY" https://api.routerplus.com/v1/usageEvery 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