Migrate from Portkey
Map Portkey's x-portkey-* headers, virtual keys, and config IDs to TokenTriage — a base-URL swap plus a header diff, what stays byte-identical, and the honest measurement you get on the first request.
Last updated
On this page
Moving off Portkey is mostly a header diff. Portkey layers its features onto an
OpenAI-compatible endpoint through the x-portkey-* header family; TokenTriage layers
its features onto the same OpenAI-compatible endpoint through the x-tt-* family. So the
migration is: point at a new base URL, drop the x-portkey-* headers, and — only if you
want them — add the smaller set of x-tt-* headers you actually use.
This guide leads with the full header map (verified against Portkey’s current docs, as of 2026-07, and TokenTriage’s proxy source), then the minimal drop-in, what stays identical, and what you get for free the moment traffic flows through.
Header & concept map
Portkey is a hosted gateway: x-portkey-api-key authenticates you to Portkey’s SaaS, a
virtual key references a provider credential Portkey stores for you, and a config ID
names a saved routing/caching config in Portkey’s UI. TokenTriage is a proxy you run
yourself, so several of those concepts move from a per-request header to server-side config —
and one word (“virtual key”) means something genuinely different on each side. The map
below is honest about all three: direct equivalents, config-side moves, and headers with no
TokenTriage analogue.
| Portkey (as of 2026-07) | What it does on Portkey | TokenTriage equivalent |
|---|---|---|
Base URL https://api.portkey.ai/v1 |
Hosted gateway endpoint (OpenAI dialect) | http://<your-tt-host>:8787/v1 for the OpenAI dialect; Anthropic SDK takes the proxy root and appends /v1/messages itself |
x-portkey-api-key |
Authenticates to your Portkey SaaS account | No equivalent — TokenTriage isn’t a SaaS account. You run the proxy; auth to it is either your upstream provider key (passed through in ledger mode) or a TokenTriage virtual key |
Authorization: Bearer <provider token> |
The upstream provider credential | Unchanged in ledger mode — pass the same provider bearer through. Swap to Bearer ttv_… once you turn governance on |
x-portkey-virtual-key (legacy) |
References a provider credential Portkey stores | Different concept — no drop-in map. A TokenTriage virtual key (ttv_) is a governance bearer bound to a tenancy node, not a stored provider secret. TokenTriage never holds your provider key; you keep it (BYO). See the callout below |
x-portkey-config |
A saved config ID (caching / fallback / load-balance) resolved in Portkey’s UI | Moves to server-side config on the host (routing tiers, cache). Per request you can override the tier with x-tt-tier and cache behavior with x-tt-cache; there is no per-request config-ID header |
x-portkey-provider (openai, @openai-prod) |
Selects the upstream provider per request | No per-request provider header. The upstream is bound by run mode / config (e.g. run --anthropic) and the model string. All x-tt-* headers are stripped before forwarding, so TokenTriage never passes a provider selector upstream |
x-portkey-trace-id |
Correlates one or more requests | x-tt-trace-id — opaque, ≤128 B; the one header that groups rows into a single agent trace (top --trace <ID>) |
x-portkey-metadata (JSON, _user) |
Custom metadata for analytics filtering | First-class dimension headers: x-tt-user, x-tt-tenant, x-tt-feature, plus free-form x-tt-tag-<k>. Require ledger.dimensions.enabled: true |
x-portkey-cache-force-refresh: true |
Bypass the cache read, write a fresh entry | x-tt-cache: refresh (bypass read, allow write). Also off (bypass read + write) and no-store (serve hits, skip write) |
x-portkey-cache-namespace |
Partition cache storage per request | No per-request namespace header — cache partitioning is config-side in v1 |
x-portkey-request-timeout (ms) |
Per-request timeout | No per-request timeout header in v1 |
x-portkey-custom-host |
Point at a private/custom upstream deployment | No equivalent — the upstream host is set in TokenTriage config, not per request |
The minimal drop-in
Two edits carry the traffic: change the base URL, and drop the x-portkey-* default
headers. In ledger mode the Authorization header stays exactly as it was — your
provider key, passed straight through.
from openai import OpenAI
# Before (Portkey):
# client = OpenAI(
# base_url="https://api.portkey.ai/v1",
# default_headers={
# "x-portkey-api-key": PORTKEY_API_KEY,
# "x-portkey-virtual-key": "openai-prod",
# "x-portkey-config": "cfg-abc123",
# },
# )
# After (TokenTriage) — one base_url, no x-portkey-* headers:
client = OpenAI(
base_url="http://127.0.0.1:8787/v1", # the only required change
api_key=OPENAI_API_KEY, # your provider key, unchanged in ledger mode
)
resp = client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": "Summarize this ticket."}],
)import OpenAI from "openai";
// Before (Portkey):
// const client = new OpenAI({
// baseURL: "https://api.portkey.ai/v1",
// defaultHeaders: {
// "x-portkey-api-key": process.env.PORTKEY_API_KEY,
// "x-portkey-virtual-key": "openai-prod",
// "x-portkey-config": "cfg-abc123",
// },
// });
// After (TokenTriage) — one baseURL, no x-portkey-* headers:
const client = new OpenAI({
baseURL: "http://127.0.0.1:8787/v1", // the only required change
apiKey: process.env.OPENAI_API_KEY, // your provider key, unchanged in ledger mode
});
const resp = await client.chat.completions.create({
model: "gpt-5",
messages: [{ role: "user", content: "Summarize this ticket." }],
});curl
$ # Before (Portkey):
$ # curl https://api.portkey.ai/v1/chat/completions \
$ # -H "x-portkey-api-key: $PORTKEY_API_KEY" \
$ # -H "x-portkey-virtual-key: openai-prod" \
$ # -H "x-portkey-config: cfg-abc123" \
$ # -H "Authorization: Bearer $OPENAI_API_KEY" ...
$
$ # After (TokenTriage) — new base URL, drop every x-portkey-* header:
$ curl http://127.0.0.1:8787/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{"model":"gpt-5","messages":[{"role":"user","content":"Hello"}]}'
Optionally re-add just the x-tt-* headers you were actually using — most Portkey
migrations map x-portkey-trace-id → x-tt-trace-id and x-portkey-metadata →
x-tt-user / x-tt-tenant / x-tt-feature, and nothing else:
$ curl http://127.0.0.1:8787/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "x-tt-trace-id: run-a1b2c3" \
-H "x-tt-tenant: acme" \
-d '{"model":"gpt-5","messages":[{"role":"user","content":"Hello"}]}'
What stays identical
The drop-in is small because the request and response contract does not change:
- Your SDK and model strings. The OpenAI and Anthropic SDKs, your
modelvalues, and your request/response shapes are untouched. The OpenAI dialect stays at/v1/chat/completions; the Anthropic dialect stays at/v1/messages. - The proxied bytes. In
ledgermode TokenTriage observes and costs every request and mutates nothing — streaming included — and any internal error fails open to a verbatim passthrough. The response you get is byte-for-byte what going direct would return. - Your provider keys. TokenTriage is BYO-key: your OpenAI / Anthropic credentials stay yours and keep working as-is. You are not moving secrets into a new vault to switch.
- Your provider accounts and rate limits. Because the upstream call is unchanged, your provider-side quotas, billing, and org settings are exactly as before.
What you get on the first request
The moment traffic flows through the proxy — no config, still in ledger mode — you gain
TokenTriage’s measurement surface:
- Every request costed with a typed dollar.
cost_usdis{usd, estimated, unpriced}, not a bare float. A request the server can’t price renders as UNPRICED and is excluded from totals and counted in the open, never silently added as$0.00. - A join key on every response.
x-tt-request-idis stamped on every proxied request and is the join into the costed ledger row and the SDK receipt. - Attribution built in. Roll spend up by model, agent, tool, trace, or step-class from
the terminal (
tokentriage top --by agent) — no dashboard required. - Honest errors. Governance and proxy errors are RFC 9457
application/problem+jsonwith a registeredtype, a stable code, and arequest_id. See Errors & problem+json. - A trust ladder, not a switch. You start in
ledger(observe + cost only), climb toshadow(compute the would-route decision and its counterfactual saving without touching a response), and enforce inrouteonly once the evidence says the cheaper path is genuinely as good.