Console
Guides/Migrate

Migrate

From OpenAI, Anthropic or another gateway — copy-paste diffs.

/llms.txt

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://api.routerplus.com/v1/chat/completions, and POST https://api.routerplus.com/v1/responses for OpenAI's Responses API
  • 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

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.

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://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.

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 values Claude Code needs on the Claude 5 models — the full list is on 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. 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, 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:

BeforeHere
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 (model:variant)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
A usage lookup by generation idGET 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, a models fallback array, transformsDropped 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. Keys such as sort and max_price 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://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: add a [model_providers.routerplus] block to ~/.codex/config.toml with
       base_url "https://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; 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