GitHubBook a demoStart routing
Browse docs

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",
    },
)

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
)

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:

from anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787")   # root, no /v1

curl, 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_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:

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 $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.

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.