Migrate from OpenRouter
The one-line base-URL swap off OpenRouter, then an honest map of OpenRouter's routing controls — the :nitro / :floor slug suffixes and the provider{} JSON object — onto TokenTriage's tiers, selectors, and routing policy.
Last updated
On this page
Moving off OpenRouter is a one-line data-plane diff: point your OpenAI-compatible
client at TokenTriage and keep the SDK, the request shape, and your code exactly as
they are. The second half of this guide maps OpenRouter’s routing controls — the
:nitro / :floor model-slug suffixes and the provider{} routing object — onto
TokenTriage’s tiers, selectors, and routing policy, and states plainly where the two
models are the same and where they are not.
1. The drop-in swap
OpenRouter is OpenAI-compatible at https://openrouter.ai/api/v1. TokenTriage exposes
the same OpenAI surface at <proxy>/v1 (the boot banner prints your bound address,
e.g. http://127.0.0.1:8787). Only the base URL changes.
Before — OpenRouter (OpenAI SDK):
import os
from openai import OpenAI
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.environ["OPENROUTER_API_KEY"],
default_headers={ # OpenRouter leaderboard attribution
"HTTP-Referer": "https://your.app",
"X-Title": "Your App",
},
)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
defaultHeaders: { // OpenRouter leaderboard attribution
"HTTP-Referer": "https://your.app",
"X-Title": "Your App",
},
});After — TokenTriage (same SDK, one changed line):
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8787/v1", # ← the only required change
api_key="unused", # TokenTriage swaps in the upstream key
# from the upstream's api_key_env
)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "http://127.0.0.1:8787/v1", // ← the only required change
apiKey: "unused", // TokenTriage swaps in the upstream key
// from the upstream's api_key_env
});Two things fall away when you leave the marketplace, and both are simplifications:
- The key goes back into your environment. OpenRouter fronted every provider with
one
OPENROUTER_API_KEY. TokenTriage forwards to the upstreams you configure with your provider keys — when an upstream setsapi_key_env(e.g.ANTHROPIC_API_KEY,OPENAI_API_KEY), TokenTriage replaces the inbound credential with that env value, so the SDK’sapi_keyargument is ignored upstream. - The
HTTP-Referer/X-Titleranking headers are dropped. They existed for OpenRouter’s public leaderboard. Attribution moves into your own ledger viax-tt-agent/x-tt-trace-id(below) — per-step, private, and exact.
Anthropic-SDK clients use the proxy root, not /v1 — the SDK appends
/v1/messages itself:
from anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787") # root, no /v1import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "http://127.0.0.1:8787" }); // root, no /v1curl, before and after:
# before — OpenRouter (vendor/model slug, OpenRouter key)
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"anthropic/claude-3.5-sonnet","messages":[{"role":"user","content":"hi"}]}'
# after — TokenTriage (a model your upstream serves; key resolves from env)
curl http://127.0.0.1:8787/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"claude-haiku-4-5","messages":[{"role":"user","content":"hi"}]}'
Start in ledger mode (the default). Nothing about routing changes yet — every
request flows through unchanged and is costed in a local ledger. You climb the trust
ladder ledger → shadow → route only when you’re ready to enforce decisions. See the
Quickstart.
2. What stays identical
- Your SDK and request/response bytes. Same OpenAI (or Anthropic) client, same message schema, same streaming. TokenTriage does not translate dialects — a request stays in its own dialect end to end.
stream,tools,max_tokens,reasoning_effortand the rest of the body pass through untouched.- Your provider accounts. You were paying the providers via OpenRouter (plus its fees); now you call them directly with your own keys.
3. Map OpenRouter’s routing onto TokenTriage
Here the two tools stop being interchangeable, and it’s worth being precise about why.
OpenRouter’s provider routing picks which third-party provider endpoint serves one
requested model — many providers host the same open model, and provider{} /
:nitro / :floor choose among those endpoints. TokenTriage’s routing picks which
of the tiers you configured serves each request — where a tier is a {model, upstream} pair pointed at an upstream you brought with your own key. OpenRouter fans
one key out to a marketplace; TokenTriage steers your own traffic across your own
upstreams by request signal (complexity, cost, semantics, budget). The controls map
conceptually, not byte-for-byte.
A TokenTriage tier list is ordered cheapest → most capable:
tiers:
- { name: cheap, upstream: anthropic, model: claude-haiku-4-5 }
- { name: frontier, upstream: anthropic, model: claude-opus-4-8 }
Control-by-control mapping (OpenRouter as of 2026-07)
| OpenRouter control | What it does | TokenTriage equivalent |
|---|---|---|
:floor suffix = provider: {sort: "price"} |
Rank endpoints cheapest-first | Order tiers: cheapest→capable and select on cost: the cheapest_meeting selector, or complexity routing to a cheaper tier |
:nitro suffix = provider: {sort: "throughput"} |
Rank endpoints by throughput | The least_latency selector over a pool (TokenTriage ranks on its own measured p50/p95 percentiles (a bounded sliding-window reservoir of recent samples) — request latency, not tokens/sec) |
provider: {sort: "latency"} |
Rank endpoints by latency | The least_latency selector |
provider: {order: [...]} |
Try providers in a fixed order | The first_eligible selector over an ordered candidate list |
provider: {allow_fallbacks: false} |
No backup provider | A single-tier target, or omit the on_empty escalation |
provider: {only: [...]} / {ignore: [...]} |
Allow / deny provider slugs | select … where 'candidate.provider in [ … ]' (hard filter) / not in |
provider: {max_price: { … }} |
Cap the per-request price (OpenRouter’s max_price sets per-million-token rates) | where 'candidate.price.input_per_tok <= <n>' — an unpriced tier is excluded with a disclosed reason, never a fabricated 0 |
provider: {require_parameters: true} |
Only providers supporting all params | Nearest analogue is a candidate where filter (e.g. candidate.max_context >= <n>); no generic param-capability gate in v1 |
provider: {data_collection: "deny"} / {zdr: true} |
Exclude providers that may store/train on data | Not applicable — TokenTriage brokers no third-party endpoints; each request goes to the upstream you configured, exactly as if you called it direct |
provider: {quantizations: [ … ]} |
Filter by weight precision | Not applicable — you point a tier at an exact upstream model |
models: [ … ] array / openrouter/auto |
Cross-model fallback / auto-pick | TokenTriage chooses among your tiers by policy (complexity, semantic, cost, cascade); a cascade escalates a cheap tier to a capable one on a graded failure |
HTTP-Referer / X-Title headers |
App attribution on the leaderboard | Drop them; attribute with x-tt-agent / x-tt-trace-id in your own ledger |
:floor — cheapest tier that qualifies
:floor says “cheapest endpoint.” In TokenTriage that’s the cheapest_meeting
selector: it picks the lowest-cost candidate that passes your hard filter.
routing:
policy:
version: 1
root:
select:
from: { tiers: all }
where: 'candidate.healthy'
by: { selector: cheapest_meeting }
on_empty: { tier: frontier }
To enforce an OpenRouter-style max_price ceiling, filter on the per-token rate — an
unpriced tier drops out honestly rather than pretending to be free:
root:
select:
from: { tiers: all }
where: 'candidate.price.input_per_tok <= 0.000002 and candidate.healthy'
by: { selector: cheapest_meeting }
on_empty: { refuse: { status: 429, reason: no_tier_under_price } }
:nitro — steer on measured latency
:nitro sorts by throughput. TokenTriage’s closest control is the least_latency
selector, which ranks live candidates on its own off-path p50/p95 percentiles (a bounded sliding-window reservoir of recent samples):
routing:
policy:
version: 1
pools:
fast: { members: [ { tier: cheap }, { tier: cheap-b } ] }
root:
select:
from: { pool: fast }
where: 'candidate.healthy'
by: { selector: least_latency }
on_empty: { tier: frontier }
provider.order — ordered fallback
An OpenRouter order: ["anthropic","openai"] becomes a first_eligible selection over
an ordered candidate list:
routing:
policy:
version: 1
root:
select:
from: { tiers: [primary, backup] }
where: 'candidate.healthy'
by: { selector: first_eligible }
on_empty: { tier: frontier }
To reproduce only / ignore, add a provider filter to the where clause —
where: 'candidate.provider in ["anthropic"] and candidate.healthy'.
Beyond a re-mapping — where the model differs
openrouter/auto routes your request to a model chosen by a third-party model-selection
service (NotDiamond), as of 2026-07. TokenTriage’s analogue is
its own complexity classifier: a fast, in-process lexical score
(signal.complexity) that routes simple requests to a cheap tier and hard ones to a
frontier tier — driven by your policy, calibratable against your own decision log, and
fully explained per request. It’s a different mechanism (your traffic, your tiers, an
auditable score) — not a community leaderboard.
routing:
policy:
version: 1
params: { simple_cut: { value: 0.35, calibrate: { signal: complexity, direction: below } } }
root:
rules:
- { id: simple-to-cheap, when: 'signal.complexity < $simple_cut', to: { tier: cheap } }
default: { tier: frontier }
4. The honest payoff
The reason to make the move is measurement, stated without spin:
- Costs you can trust. Every row carries typed money; an unpriceable model reads
UNPRICED— a stated reason excluded from totals, never a fabricated$0folded silently into your spend. - Per-step, per-agent attribution in your own ledger. Set
x-tt-trace-idper run,x-tt-agentper role,x-tt-step-idper call — all stripped before the request reaches the provider — andtokentriage top --trace <id>renders the exact per-step cost tree. No leaderboard headers, no third party. - Every routing decision is explained. Each routed row records the inputs read (in first-read order), the selector’s score table over every candidate, and why the losers were excluded.
5. Migrate in one prompt
Hand this to your coding agent to do the swap in your repo. It matches OpenRouter’s own “one prompt” migration ergonomics, adapted to the facts above:
Migrate this codebase off OpenRouter to a local TokenTriage proxy. Do exactly this:
1. Find every OpenAI/Anthropic client whose base_url is "https://openrouter.ai/api/v1".
- For OpenAI-style clients, set base_url to "http://127.0.0.1:8787/v1".
- For Anthropic SDK clients, set base_url to "http://127.0.0.1:8787" (root; the SDK
appends /v1/messages). Do NOT add /v1 for Anthropic.
2. Remove the OpenRouter-only pieces:
- Delete the HTTP-Referer and X-Title default headers.
- Stop reading OPENROUTER_API_KEY. Keys now come from the provider env vars the
TokenTriage upstreams reference (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY); the
client's api_key argument is ignored upstream, so a placeholder is fine.
3. Rewrite model ids: strip the "vendor/" prefix and any ":nitro"/":floor" suffix.
Replace each with a concrete model id my configured upstream serves, or leave the
model as-is if a routing tier will rewrite it. List every model string you changed.
4. Do NOT translate request/response shapes — the OpenAI/Anthropic API is unchanged.
5. Print a summary: every file touched, every base_url changed, and every model id
rewritten, so I can review before running.
Then bring your OpenRouter provider{} routing across using the tables and policy
snippets in section 3.