Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

SDKs

Point the Anthropic SDK at TokenTriage
The bare root, not the /v1 root.

Anthropic clients take the bare root. OpenAI clients take root plus /v1. TokenTriage serves the Anthropic dialect at one path only.

# 1 · tokentriage demo — the real proxy over a mock Anthropic upstream, no API key.
#     Its banner prints the base URL to use. It is a ROOT, with no /v1 on the end.
import anthropic

client = anthropic.Anthropic(base_url="http://127.0.0.1:8787", api_key="sk-ant-…")

client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=256,
    messages=[{"role": "user", "content": "ping"}],
)
tokentriage demo boots the real proxy in front of a mock Anthropic upstream. No API key. Point the SDK at the root it prints, and never append /v1.
The setting
anthropic.Anthropic(base_url=…) or ANTHROPIC_BASE_URL
The base URL
http://127.0.0.1:8787 — the bare root. OpenAI clients take root + /v1

How to tell it worked

Send one message, then read the last line of the decision log. Three fields settle it. A response header will not, and the limits below say why.

# tokentriage demo prints its own ledger path; run's default is below.
jq -r '[.http.path, .http.dialect, .cost.pricing_key] | join(" ")' \
  ~/.tokentriage/decisions.jsonl | tail -1

What you should see

/v1/messages anthropic anthropic/claude-haiku-4-5

How sure we are

Reproduced

A person ran it by hand against a real gateway on a stated date, and the transcript is on the page.

Verified against anthropic 1.0.0 · Python 3.14.5 on

LAB RUN · MOCK UPSTREAM — anthropic 1.0.0 · Python 3.14.5 · 2026-08-25

$ tokentriage demo
tokentriage dev · mode: ledger
proxy: http://127.0.0.1:10017   (point your app's base_url here)

--- A · base_url = the bare root · demo, real Anthropic upstream ---
  http.path         '/v1/messages'
  http.status       200
  http.dialect      'anthropic'  <- PARSED
  request.model     'claude-haiku-4-5'
  usage             in=900 out=220 source='provider'
  cost.pricing_key  'anthropic/claude-haiku-4-5'
  cost.unpriced     None   reason=None

--- D · base_url = the root plus /v1 · same process -----------------
  http.path         '/v1/v1/messages'
  http.status       404
  http.dialect      ''  <- EMPTY: no dialect matched this path
  request.model     ''
  usage             in=0 out=0 source='absent'
  cost.pricing_key  ''
  cost.unpriced     None   reason=None

--- B · base_url = the bare root · a config with no Anthropic upstream ---
  http.path         '/v1/messages'
  http.status       502
  http.dialect      'anthropic'  <- PARSED
  request.model     'claude-haiku-4-5'
  usage             in=0 out=0 source='absent'
  cost.pricing_key  ''
  cost.unpriced     True   reason='model_unknown'
  decision          mode='ledger' tier='' upstream='openai-a'
  outcome.error     'dial tcp 127.0.0.1:1: connectex: No connection could be made because the target machine actively refused it.'
anthropic 1.0.0 on Python 3.14.5, driven against the real proxy engine over mock upstreams. Three rows, two processes. Python only: @anthropic-ai/sdk was not run.

The gotcha

A /v1 base URL becomes /v1/v1/messages

The SDK stores http://127.0.0.1:8787/v1/ and posts /v1/v1/messages. TokenTriage relays that path as written and the upstream answers it. You get anthropic.NotFoundError, status 404, body 404 page not found. The row is the tell. Its http.dialect is empty.

# The SDK stores this as ".../v1/" and posts /v1/v1/messages.
anthropic.Anthropic(base_url="http://127.0.0.1:8787/v1")

# The bare root. OpenAI clients are the ones that take root + /v1.
anthropic.Anthropic(base_url="http://127.0.0.1:8787")

What you get now

Three things the row already carries.

What the ledger held after the run, beyond the fact that the call returned. Every item links the platform page that evidences it. Read those first.

1 path
the Anthropic dialect is served at /v1/messages and nowhere else
0 segments to append
the SDK writes /v1/messages itself, so it takes the bare root
10 x-stainless headers
every one reached the upstream, in both credential arms
3 rows from one call
what the SDK’s default of two retries wrote against a 502

What the first call does not show

Seven things to know before you trust the number.

Six came out of the run. The last is about our evidence rather than your code.

  • count_tokens is not a registered path

    client.messages.count_tokens posts /v1/messages/count_tokens. TokenTriage relays it and your upstream answers. The row carries no dialect, no model and no pricing key, so the call is not costed.

    The registered endpoint kinds
  • An unregistered path can also return 200

    The mistake 404s only because the upstream refuses that path. Not every upstream does. In the run an unregistered path the upstream serves returned 200, and its row still held no dialect and no price.

    What a ledgered passthrough is
  • The pre-count can land on another upstream

    The pick is Go map order. With no dialect there is nothing to match on, and in the run one count_tokens call went to the openai upstream while an anthropic one sat beside it.

  • A 502 is retried and a 404 is not

    The SDK ships DEFAULT_MAX_RETRIES = 2. One client call against an unreachable upstream wrote three ledger rows. The doubled-prefix mistake wrote one. Count requests, not rows, when you debug.

  • Without api_key_env the client’s own key goes upstream

    The control arm forwarded the client’s x-api-key exactly as it was sent. Name api_key_env on the upstream when the client should not hold the provider credential. The x-tt-* strip happens either way.

    Where the upstream credential comes from
  • A response header will not tell you

    x-tt-decision and x-tt-mode are set in shadow and route mode only. A ledger-mode run is header-indistinguishable from a passthrough. x-tt-request-id is on everything, including a call nothing parsed. Read the row.

    Which headers are set when
  • The chip covers Python, not TypeScript

    The run drove anthropic 1.0.0 on Python. @anthropic-ai/sdk was not run here, and no test in the product repo drives either package, so the TypeScript half rests on the wire contract alone.

Run the demo, then read one row.

tokentriage demo needs no API key and no config. Send one message at the root it prints, then open the ledger it names. The row is the whole verification.

Also: Where the row shows upEvery integration