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.

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 kwarg is the setting. tt.init and the transport share the root; ChatOpenAI takes the /v1 path. Without the handler and the transport you still get costed rows, with no agent on them.
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 agent

What 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"}
LAB RUN · MOCK UPSTREAM. The real proxy engine in front of a mock upstream, on loopback: langchain-core 1.6.0, langchain-openai 1.6.0, langgraph 1.2.11, Python 3.14.5. The second run is the environment CI installs. Full session: docs/integration-facts/transcripts/W2-langchain.md

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.

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 registered
  • Streaming 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 client
  • What 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.

Also: What the ledger recordsEvery integration