# Migrate from OpenAI or Anthropic SDKs

Point the official OpenAI or Anthropic SDK at TokenTriage with one base-URL change — your code, calls, and keys stay put.

Source: https://tokentriage.com/docs/migrate/openai-anthropic

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

If your app already calls the model provider through the **official OpenAI or Anthropic
SDK**, this is the smallest possible migration: change the SDK's base URL to point at a
TokenTriage proxy. You keep the same SDK, the same calls, the same API keys — the proxy
speaks each provider's native dialect, so the request and response bytes are unchanged.

<Callout type="key">
The only edit is the base URL. In the default `ledger` mode the proxied bytes are identical
to going direct — streaming included — and any internal error fails open to a verbatim
passthrough. Swapping the base URL cannot become a new way for your app to break.
</Callout>

## The one change

TokenTriage's data plane listens on `127.0.0.1:8787` by default. Each SDK gets a different
base URL because each speaks a different dialect:

- **OpenAI SDK** → the proxy's `/v1` prefix (`http://127.0.0.1:8787/v1`). The SDK appends
  `/chat/completions`, so requests land on `/v1/chat/completions`, which the OpenAI dialect
  owns.
- **Anthropic SDK** → the proxy **root** (`http://127.0.0.1:8787`). The Anthropic SDK
  appends `/v1/messages` itself, and the Anthropic dialect owns `/v1/messages` — so you give
  it the root, **not** `/v1`.

## Migrate the OpenAI SDK

First run a ledger proxy with an OpenAI upstream. Drop this into `tokentriage.yaml`:

```yaml
version: 1
mode: ledger                    # observe and cost every request; mutate nothing
upstreams:
  openai:
    base_url: https://api.openai.com   # host only — your client already sends /v1
    dialect: openai
    api_key_env: OPENAI_API_KEY
```

```console
$ tokentriage run -c tokentriage.yaml
```

Then change one line in your app.

<CodeTabs>
<Fragment slot="python">
```python
from openai import OpenAI

# BEFORE: client = OpenAI()                                  # talks straight to api.openai.com
# AFTER  — the only change: base_url points at the proxy's /v1 dialect prefix.
client = OpenAI(base_url="http://127.0.0.1:8787/v1")

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: const client = new OpenAI();                      // talks straight to api.openai.com
// AFTER  — the only change: baseURL points at the proxy's /v1 dialect prefix.
const client = new OpenAI({ baseURL: "http://127.0.0.1:8787/v1" });

const resp = await client.chat.completions.create({
  model: "gpt-5",
  messages: [{ role: "user", content: "Summarize this ticket." }],
});
```
</Fragment>
</CodeTabs>

**curl**

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

<Callout type="note">
Your key stays where it is. With `api_key_env: OPENAI_API_KEY` set on the upstream, the proxy
attaches that key and your app can send anything (or nothing) as the bearer. Omit `api_key_env`
and the inbound `Authorization` header is forwarded as-is — your app's existing `OPENAI_API_KEY`
still works unchanged.
</Callout>

## Migrate the Anthropic SDK

For an Anthropic upstream (ideal for Claude Code), the zero-config quickstart is enough — no
config file needed:

```console
$ tokentriage run --anthropic
```

Then point the Anthropic SDK at the proxy **root**:

<CodeTabs>
<Fragment slot="python">
```python
from anthropic import Anthropic

# BEFORE: client = Anthropic()                               # talks straight to api.anthropic.com
# AFTER  — the only change: base_url is the proxy ROOT, not /v1. The SDK appends /v1/messages.
client = Anthropic(base_url="http://127.0.0.1:8787")

resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=256,
    messages=[{"role": "user", "content": "Summarize this ticket."}],
)
```
</Fragment>
<Fragment slot="typescript">
```typescript
import Anthropic from "@anthropic-ai/sdk";

// BEFORE: const client = new Anthropic();                   // talks straight to api.anthropic.com
// AFTER  — the only change: baseURL is the proxy ROOT, not /v1.
const client = new Anthropic({ baseURL: "http://127.0.0.1:8787" });

const resp = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 256,
  messages: [{ role: "user", content: "Summarize this ticket." }],
});
```
</Fragment>
</CodeTabs>

**curl**

```console
$ curl http://127.0.0.1:8787/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-sonnet-4-6","max_tokens":256,"messages":[{"role":"user","content":"Hello"}]}'
```

<Callout type="tip">
For Claude Code specifically, `tokentriage wrap` sets `ANTHROPIC_BASE_URL` for you and injects
a fresh `x-tt-trace-id` (plus `x-tt-agent`) so every request in the session groups into one
trace: `tokentriage wrap --agent claude -- claude`.
</Callout>

## What you get for the one-line change

Nothing about your responses changed — but now every request is on the ledger:

- **A costed row per request.** TokenTriage stamps an `x-tt-request-id` response header on
  every proxied call; that's the join key into a ledger row with the real dollar cost,
  computed from the compiled-in pricing snapshot. Roll it up from the terminal:

  ```console
  $ tokentriage top --by model
  ```

  A model the server can't price shows a `-` in the `COST` column, with a footnote stating how
  many rows were excluded because no price resolved — never a fabricated `$0`.

- **Optional trace grouping.** Add `x-tt-trace-id` / `x-tt-agent` headers later to group a run
  or split spend by sub-agent. They're optional — a request with no `x-tt-*` headers is still
  fully costed; it just lands as a standalone row.

- **A path to savings with zero further code change.** Everything above is `ledger` mode:
  pure observation. When the evidence says a cheaper model is genuinely as good, you climb to
  `shadow` and then `route` — enforcing routing decisions **in the proxy config**, without
  touching your application again.

<NextSteps items={[
  {
    title: "Quickstart",
    href: "/docs/get-started/quickstart",
    description: "The five-minute path from install to a costed request you can read the dollar for."
  },
  {
    title: "Python SDK",
    href: "/docs/sdks/python",
    description: "Read the costed receipt back in code with the zero-dependency instrumentation SDK."
  },
  {
    title: "Observability",
    href: "/docs/build/observability",
    description: "Turn the per-request ledger into rollups, traces, and dashboards."
  }
]} />
