SDKs
Point the OpenAI Python SDK at TokenTriage
The /v1 is the whole change.
One argument on the client, and the /v1 is part of it. Nothing else changes. Then the row tells you it was costed.
from openai import OpenAI
# The /v1 belongs to the address. OPENAI_BASE_URL takes the same string.
# The key is forwarded upstream as-is, unless the upstream names api_key_env.
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="sk-lab")
client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}],
)- The setting
OpenAI(base_url=…), or the OPENAI_BASE_URL env var- The base URL
http://127.0.0.1:8787/v1 — /v1 or /v1/, never the bare root
How to tell it worked
Ask the SDK for the raw response and print the request id. Then find that id in decisions.jsonl. The header proves arrival. The row proves costing.
r = client.chat.completions.with_raw_response.create(
model="gpt-4o-mini", messages=[{"role": "user", "content": "hi"}]
)
print(r.headers["x-tt-request-id"])What you should see
tt_pibe5apkzccashrmhqnfyzuzki — and that row reads "dialect":"openai", "pricing_key":"openai/gpt-4o-mini"
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 openai 3.3.1 on Python 3.14.5 on
# LAB RUN · MOCK UPSTREAM · openai 3.3.1 · Python 3.14.5 · go1.25.6 · 2026-08-25
# venv A — pip install openai, then pip install -e "sdk/python[dev,httpx]"
$ venv/Scripts/python.exe -m pytest sdk/python/tests/test_adapter_httpx_e2e.py
============================= test session starts =============================
platform win32 -- Python 3.14.5, pytest-9.1.1, pluggy-1.6.0
rootdir: D:\tokentriage\sdk\python
configfile: pyproject.toml
plugins: anyio-4.14.2, hypothesis-6.165.10
collected 2 items
sdk\python\tests\test_adapter_httpx_e2e.py .. [100%]
============================== 2 passed in 9.21s ==============================
# venv B — pip install -e "sdk/python[dev]", the string CI runs at sdk-python.yml:285
$ venv-ci/Scripts/python.exe -m pytest sdk/python/tests/test_adapter_httpx_e2e.py -rs
plugins: hypothesis-6.165.10
collected 0 items / 1 skipped
=========================== short test summary info ===========================
SKIPPED [1] sdk\python\tests\test_adapter_httpx_e2e.py:12: could not import 'httpx': No module named 'httpx'
============================= 1 skipped in 0.27s ==============================The gotcha
Your provider gets a body you did not send
TokenTriage splices stream_options.include_usage into the outbound body, then strips the extra frame out of your copy. Your chunks carry no usage object. The row still reads usage.source provider. A provider that rejects unknown fields, or signs the request body, sees the difference.
# Stop the injection. Output tokens are estimated instead.
ledger:
usage_injection: false“If set, an additional chunk will be streamed before the `data: [DONE]` message.”
openai 3.3.1 · chat_completion_stream_options_param.py · read 2026-08-25
What you get now
What one argument buys you.
Three things that start the first time a request reaches the gateway. Each links the page that evidences it. Read those first.
A row you can find from Python
TokenTriage stamps x-tt-request-id on every response. with_raw_response hands it to you. That id is the row id in the decision log, and the row carries the provider counts priced against a dated snapshot.
What a row recordsPer-step attribution without rewriting your calls
Pass http_client=tt.httpx_client() and each call inside a tt.trace scope carries your agent, user and step id. The SDK is optional. pip install "tokentriage[httpx]" pulls the httpx that openai 3.3.1 no longer installs.
Which extra each adapter needsThe provider key stops living in your app
Where an upstream names api_key_env, TokenTriage strips the credential your client sent and replaces it. The provider key lives in the gateway. Budgets and rate limits attach to the key, not to your code.
How governance works
- 1 argument
- base_url on the constructor, or OPENAI_BASE_URL carrying the same string
- 2 tests, neither in CI
- both passed by hand on 2026-08-25; the CI install collects zero of them
- 6 row fields emptied
- dialect, model, tier, usage source, pricing key, estimated: the passthrough shape
- 0 chunks carrying usage
- the injected frame is stripped before your client sees it, and the row is priced anyway
What does not carry across
Five things this swap will not do.
Four are about your setup. The fifth is about our evidence. The first one is how this goes wrong quietly.
A base URL without /v1 still reaches your upstream
TokenTriage matches /v1/chat/completions exactly. A bare root sends /chat/completions, which matches no dialect and is relayed with the path as written. Our mock answered 404. An upstream that serves that path answers 200, and the row is still unpriced.
What the path has to beA trailing slash does not match, and it is not yours
/v1/chat/completions/ matches nothing. The OpenAI Python SDK cannot produce it: a base_url ending /v1 and one ending /v1/ build the same path, checked on 3.3.1. The slash arrives from a rewriting proxy or an ingress rule in front.
The evidence on this page is not one level
Levels are not uniform here. The chip is Reproduced because the Python test skips in CI. The injection gotcha is gated instead: go test ./test/e2e runs TestRealOpenAIStreamStrip on every push.
openai 3.3.1 installs httpx2, and not httpx
The adapter tt.httpx_client() rides the httpx package, which pip install openai stopped pulling at 3.0.0. Install the extra. openai 3.3.1 accepts a client from either package, so the adapter still works.
Every exported name in the Python SDKWhat we ran was chat completions, and only that
TokenTriage registers /v1/responses, /v1/completions and /v1/embeddings in the same OpenAI driver. We sent none of them. So the rows behind this page describe chat.completions.create and nothing else.
Set it, then read one row.
The change is one argument and it is reversible. Read one row first. Point base_url back and the SDK is talking to OpenAI again.
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.