# Migrate from Portkey

Map Portkey x-portkey-* headers, virtual keys, and config IDs to TokenTriage — a base-URL swap plus a header diff.

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

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

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 |

<Callout type="warning">
"Virtual key" is a **false friend** between the two products. On Portkey it is a stored
reference to *your provider credential*. On TokenTriage a `ttv_` key is a **governance
bearer** bound to a tenancy node (org / team / user) with optional scopes and budgets —
it carries no provider secret. Don't try to port Portkey virtual keys one-for-one: keep
your provider keys where they are (TokenTriage is BYO-key), and mint `ttv_` keys fresh when
you want per-tenant attribution and spend caps. See
[Governance, virtual keys &amp; budgets](/docs/build/governance).
</Callout>

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

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

**curl**

```console
$ # 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:

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

<Callout type="tip">
All `x-tt-*` headers are **optional**. A request with none of them is still fully costed —
it just lands as a standalone ledger row. You can migrate the base URL first and add trace /
dimension headers later. See [HTTP headers](/docs/reference/headers) for every field.
</Callout>

## 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 `model` values, 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 `ledger` mode 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.

<Callout type="key">
The migration is reversible at the base URL. If anything looks off, point `base_url` back at
your previous endpoint and you're exactly where you started — TokenTriage never became a new
way for your traffic to break.
</Callout>

## 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_usd` is
  `{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-id` is 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+json`
  with a registered `type`, a stable code, and a `request_id`. See
  [Errors &amp; problem+json](/docs/reference/errors).
- **A trust ladder, not a switch.** You start in `ledger` (observe + cost only), climb to
  `shadow` (compute the would-route decision and its counterfactual saving without touching a
  response), and enforce in `route` only once the evidence says the cheaper path is genuinely
  as good.

<Callout type="note">
On cost, TokenTriage does not overclaim: it adds a thin proxy hop and does **not** claim a
cost win over other gateways. The differentiator is the cost number you read back: priced
when it can be, marked UNPRICED when it cannot, and never silently zeroed.
</Callout>

<NextSteps items={[
  { title: "Governance, virtual keys & budgets", href: "/docs/build/governance", description: "Mint ttv_ keys bound to a tenancy node and cap spend — the real analogue to Portkey's per-key controls." },
  { title: "HTTP headers", href: "/docs/reference/headers", description: "The complete x-tt-* request and response header contract, verified against the proxy source." },
  { title: "The release workflow", href: "/product/", description: "See how gateway configuration, candidate evidence, and deliberate deployment fit together." },
]} />
