# Migrate from OpenRouter

Move off OpenRouter with a one-line base-URL swap, then map its slug suffixes and provider object onto TokenTriage routing.

Source: https://tokentriage.com/docs/migrate/openrouter

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import CodeTabs from '../../../components/CodeTabs.astro';

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.

<Callout type="note">
OpenRouter behavior below was re-read from **openrouter.ai/docs as of 2026-07**;
field names and defaults are quoted from OpenRouter's own docs. TokenTriage behavior
is verified against its shipped config and routing-policy reference. Where the two
tools genuinely differ, this guide says so rather than forcing a false one-to-one.
</Callout>

## 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):**

<CodeTabs>
<Fragment slot="python">
```python
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",
    },
)
```
</Fragment>
<Fragment slot="typescript">
```typescript
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",
  },
});
```
</Fragment>
</CodeTabs>

**After — TokenTriage (same SDK, one changed line):**

<CodeTabs>
<Fragment slot="python">
```python
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
)
```
</Fragment>
<Fragment slot="typescript">
```typescript
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
});
```
</Fragment>
</CodeTabs>

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 sets `api_key_env` (e.g.
  `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`), TokenTriage replaces the inbound credential
  with that env value, so the SDK's `api_key` argument is ignored upstream.
- **The `HTTP-Referer` / `X-Title` ranking headers are dropped.** They existed for
  OpenRouter's public leaderboard. Attribution moves into your own ledger via
  `x-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:

<CodeTabs>
<Fragment slot="python">
```python
from anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787")   # root, no /v1
```
</Fragment>
<Fragment slot="typescript">
```typescript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "http://127.0.0.1:8787" });   // root, no /v1
```
</Fragment>
</CodeTabs>

**curl, before and after:**

```console
# 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"}]}'
```

<Callout type="warning">
**Model slugs change.** OpenRouter names models in its own `vendor/model` namespace
(`anthropic/claude-3.5-sonnet`). TokenTriage forwards the `model` field to your
configured upstream, so use that upstream's **own** model id (`claude-haiku-4-5`,
`gpt-4o-mini`, …) — or point a routing tier at a model and let TokenTriage rewrite the
request to it. There is no `vendor/` prefix and no `:nitro`/`:floor` suffix on the wire.
</Callout>

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](/docs/get-started/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_effort`** and 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:

```yaml
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.

```yaml
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 rather than pretending to be free:

```yaml
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):

```yaml
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 }
```

<Callout type="note">
Honest caveat: `least_latency` ranks on **request latency percentiles**, not the
tokens-per-second "throughput" OpenRouter's `:nitro` optimizes. A cold upstream with no
measurement is **disclosed and deprioritized** behind any measured candidate (it carries a
`{cold_start:1}` term and a sentinel-worst score, so it loses to any data-bearing candidate,
and if every candidate is cold the first is chosen deterministically) — never guessed. We
publish no latency comparison against OpenRouter. The gateway-overhead figures we do publish
cover LiteLLM and Bifrost only.
</Callout>

### `provider.order` — ordered fallback

An OpenRouter `order: ["anthropic","openai"]` becomes a `first_eligible` selection over
an ordered candidate list:

```yaml
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.

```yaml
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 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 `$0`
  folded silently into your spend.
- **Per-step, per-agent attribution in your own ledger.** Set `x-tt-trace-id` per run,
  `x-tt-agent` per role, `x-tt-step-id` per call — all stripped before the request
  reaches the provider — and `tokentriage 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.

<Callout type="key">
We do **not** claim TokenTriage is cheaper or faster than OpenRouter, and we publish no cost
or latency comparison against it. The one benchmark we publish covers LiteLLM and Bifrost, and
only for gateway overhead. What changes when you migrate is honesty and control: your keys,
your upstreams, an auditable ledger — not a headline number.
</Callout>

## 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:

```text
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.

<NextSteps items={[
  { title: "Routing", href: "/docs/build/routing", description: "Tiers, selectors, and the routing-policy language in full — the home of every mapping in section 3." },
  { title: "Providers & BYO keys", href: "/docs/build/providers", description: "Configure upstreams with your own keys — the direct-to-provider setup that replaces the OpenRouter marketplace." },
  { title: "Quickstart", href: "/docs/get-started/quickstart", description: "Boot the proxy against built-in mocks and read a costed table before you wire anything up." },
  { title: "The honesty model", href: "/docs/concepts/honesty-model", description: "Why UNPRICED, excluded totals, and explained decisions are the point of the migration." },
]} />
