Agent frameworks
Point LangChain at TokenTriage
Two nodes, two rows, one trace.
One kwarg on ChatOpenAI prices every call. The adapter is a pair, and half of it fails silently.
import tokentriage as tt
from langchain_openai import ChatOpenAI
from tokentriage.adapters.langchain import TTCallbackHandler
TT = "http://127.0.0.1:8787"
tt.init(base_url=TT)
llm = ChatOpenAI(
model="gpt-4o-mini", api_key="sk-lab",
base_url=f"{TT}/v1", # the kwarg, never the env var
http_async_client=tt.async_httpx_client(base_url=TT), # the transport takes the root
)
await graph.ainvoke(state, config={"callbacks": [TTCallbackHandler()]})- The setting
ChatOpenAI(base_url=…) — the kwarg, not an env var- The base URL
http://127.0.0.1:8787/v1 — the transport takes the bare root instead
How to tell it worked
Run the graph once. Then read the ledger, not your app output. The wrap case returned ok and wrote nothing, so a successful call proves nothing on its own.
tokentriage top --by agentWhat you should see
AGENT REQS … COST · planner · coder · a row under (none) means no x-tt headers reached the wire
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 langchain-openai 1.6.0 on
# LAB RUN · MOCK UPSTREAM · 2026-08-25
# langchain-core 1.6.0 · langchain-openai 1.6.0 · langgraph 1.2.11 · Python 3.14.5
$ python -m pytest sdk/python/tests/test_adapter_langchain.py \
sdk/python/tests/test_adapter_langchain_e2e.py -v
collected 12 items
test_adapter_langchain_e2e.py::test_async_two_node_graph_per_node_identity PASSED [100%]
============================= 12 passed in 20.49s =============================
$ python -m pytest <the same two files> -rs # venv built from sdk/python[dev], as CI does
collected 0 items / 2 skipped
SKIPPED [1] test_adapter_langchain.py:14: could not import 'langchain_core'
SKIPPED [1] test_adapter_langchain_e2e.py:14: could not import 'httpx'
============================= 2 skipped in 1.04s ==============================
# the two-node LangGraph run, from the probe ledger — other fields elided
{"trace.agent":"planner","trace.step_id":"8aa8684a4c79d33c","trace.trace_id":"4dd786cd…","trace.source":"x-tt"}
{"trace.agent":"coder","trace.step_id":"b038ea9d60ff307f","trace.trace_id":"4dd786cd…","trace.source":"x-tt"}The gotcha
Two stale variables silently outrank the one wrap sets
The base URL resolves in four rungs. A kwarg wins, then OPENAI_API_BASE, then LANGSMITH_GATEWAY, and last OPENAI_BASE_URL — the only one tokentriage wrap sets. In that case the call returned ok: true from a decoy sink and the ledger stayed empty.
# The fix: pass the kwarg. It outranks all three variables.
llm = ChatOpenAI(model="gpt-4o-mini", base_url="http://127.0.0.1:8787/v1")
# The check: ask the client which base URL won, not the environment.
print(llm.root_client.base_url) # -> http://127.0.0.1:8787/v1/“Resolution order (first match wins): 1. Explicit `base_url` (or `openai_api_base`) kwarg. 2. Env var `OPENAI_API_BASE` (read by LangChain at init). 3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).”
langchain_openai/chat_models/base.py:794-798 · langchain-openai 1.6.0 · read 2026-08-25
What you get now
What the ledger holds once the graph runs.
Three things a costed row carries here. Each links the page that evidences it. The agent and step ids need both halves of the adapter.
Per-node rows under one trace
Two nodes produced two rows. Each carried its own agent and step id, planner and coder, under one shared trace id, with trace.source x-tt so the identity came off the wire.
Traces, agents and stepsA priced row without touching your graph
The base-URL swap alone gives priced rows: dialect openai, status 200, usage from the provider, a real pricing key, and estimated false. No agent, no trace, no step.
Every field on a rowDollars per trace, agent and step
TokenTriage rolls the rows up by trace, agent, model, step class or tool. A graph spend splits by node in the same rollup. The identity comes from headers only.
Rolling cost up by agent
- 4 rungs
- kwarg, then OPENAI_API_BASE, then LANGSMITH_GATEWAY, then OPENAI_BASE_URL
- 2 lines
- the handler and the transport, together or not at all
- 12 tests passed
- and both files skipped in the environment CI installs
- 1 trace id
- shared by every step of one graph run
Where this stops
Six things to know before you trust it.
Three are LangChain behaviours, not ours. Three are about our adapter and our evidence. Read the first one before you cite this page anywhere.
The tests behind this page skip in CI
Both LangChain test files skip in the environment CI installs, sdk/python[dev]. A green check there is compatible with zero coverage. Installing the langchain and httpx extras would make it a gate. The skip is measured, not assumed.
The vendor docstring lists three rungs, not four
LangChain's own resolution note names the kwarg and two variables. The LangSmith gateway rung sits in a langchain-core helper it does not mention. We found it by running it.
LANGSMITH_GATEWAY is not a workaround
Pointed at the gateway root, LangChain appends /openai/v1. That matches no dialect. The call relays to your upstream with the path as written. The row carries status 404, an empty pricing key, and cost 0 beside estimated true.
Which paths are registeredStreaming turns off LangChain’s own token counts
Setting base_url, or passing a custom HTTP client, turns off the stream_usage default. Your ledger row is priced either way. Your application sees no token counts on a stream until you pass stream_usage=True.
A callback alone attributes nothing
With the handler attached and a stock HTTP client, both rows landed fully costed and carried no agent, no trace and no step. Nothing errors. A callback cannot set headers.
Why the handler pairs with a clientWhat this page did not run
ChatAnthropic and LangChain.js were not driven here. The documented adapter downgrade could not be triggered in any shipped configuration, only by forcing run_inline off. With no trace open, the fallback is no x-tt headers at all.
Point it, then read the ledger.
The kwarg takes one line. The pair takes two more, and it is the only way a node name reaches a row. Check the ledger before you trust the app.
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.