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"}],
)- 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 -1What 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.'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.
Streaming and non-streaming both priced
The run made both calls. Each produced a row with usage source provider, in=900 out=220, and pricing key anthropic/claude-haiku-4-5. Streaming did not change the shape of the row.
What a priced row holdsYour upstream key replaces the client’s
Where an upstream names api_key_env, TokenTriage replaces the client’s x-api-key. Every x-tt-* request header is stripped. anthropic-version, anthropic-beta, User-Agent and ten x-stainless-* headers reached the upstream unchanged.
How keys and tenancy workBoth dialects, one row shape
TokenTriage serves the Anthropic messages endpoint at /v1/messages and the OpenAI chat endpoint at /v1/chat/completions. Two dialects, one row shape. Routing rules match on the model your client sent.
Tiers, rules and modes
- 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 kindsAn 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 isThe 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 fromA 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 whenThe 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.
Names and trademarks
These are independent integrations. No product named here has endorsed, reviewed or sponsored TokenTriage; every name is its owner's trademark, used only to say what works with what.