Repository navigation
Description
What this is
Surface the cached and reasoning token counts the proxy already returns,
on both Budget and the OTel span — so a developer can see cache effectiveness
and reasoning overhead without parsing the response body themselves.
Why it is needed
The FinOps roadmap lists "Support for Cached and Reasoning Tokens" (accurate cost
tracking for cached input and reasoning output). Unusually for a roadmap item,
the data is already live — no verification row needed.
python/tests/fixtures/anypoint/llm_proxy/responses.success.body.json, captured
against the real proxy, carries:
"usage": {
"input_tokens": 17,
"input_tokens_details": { "cache_write_tokens": 0, "cached_tokens": 0 },
"output_tokens": 51,
"output_tokens_details": { "reasoning_tokens": 0 },
"total_tokens": 68
}Meanwhile core/budget.py models only the three x-token-* headers —
limit, remaining, reset. It never looks at the response body, so these
fields are invisible to the SDK today.
This matters beyond curiosity. A reasoning model can spend the majority of its
output tokens on reasoning the developer never sees, and cached input tokens are
typically billed at a fraction of the normal rate. A developer reading only
total_tokens draws the wrong conclusion about both cost and latency, and has no
signal at all about whether their prompt-caching strategy is working.
Scope
- Parse
usage.input_tokens_details.cached_tokens,
usage.input_tokens_details.cache_write_tokensand
usage.output_tokens_details.reasoning_tokensfrom the response body. - Expose them as last-call fields alongside the existing budget window. They are
per-call, not a window — do not fold them intoremaining, which is a
gateway-owned quantity. - Emit them as span attributes so they land in whatever backend consumes telemetry: OTel GenAI spans at a pinned semconv version, dual-emitted with a stable donkey.* namespace #192.
- Absent fields stay
None, never0— the same honesty ruleBudget.observe()
already applies to an unobserved window.
Sketch
from donkey_kit import Donkey
donkey = Donkey.from_env()
model = donkey.langgraph.chat_model("gpt-5.1") # deep adapter
await model.ainvoke("...")
u = donkey.budget.last_usage
u.input_tokens # 17
u.cached_tokens # 0 -- billed at the cached rate
u.reasoning_tokens # 0 -- output tokens you never see
u.total_tokens # 68
donkey.budget.remaining # unchanged: still the gateway's windowRaw client — the parse lives in the shared transport's _on_response hook, so
the no-framework path gets it identically:
client = donkey.llm.client()
await client.responses.create(model="gpt-5.1", input="...")
donkey.budget.last_usage.reasoning_tokensTypeScript parity (planned, Phase 5):
const usage = donkey.budget.lastUsage;
usage.cachedTokens;
usage.reasoningTokens;Acceptance
- The three fields parse from the captured live fixture, with the fixture as the test input.
- Absent fields report
None, not0. - A response with no
usageobject is a defined no-op, exactly as a response
with nox-token-*headers is today. - Span attributes emitted under the pinned semconv names where they exist.
- Both the async and sync transports carry the behaviour.
Cross-surface impact
- core:
budget.py(a per-call usage record beside the window),
transport.py(_on_responsealready fires once on the final response — the
right seam),telemetry.py(the new attributes). - llm: streaming responses deliver usage differently; confirm the streaming path.
- registry / tools / integrations: not affected.
- TypeScript parity (planned): flag.
- Provisioning CLI: not affected.
- Nextra docs site: the budget and telemetry pages.
- docs/verified-apis.md: the usage shape is already live-captured; add a row
pointing at the fixture so nobody re-opens this as a verification question.
Related
- budget: Budget as a first-class object parsed from x-token-* headers #185 / budget: wait_for_reset() and pace(reserve=) raising BudgetReserveReached #186 — the
Budgetobject and pacing this sits beside. - telemetry: OTel GenAI spans at a pinned semconv version, dual-emitted with a stable donkey.* namespace #192 / telemetry: validated cost-attribution tags (team, project, env, enduser.id) #196 — the span and cost-tag consumers.
- verify: model-wallet spend headers and the threshold-warning signal #301 — model wallets; monetary cost is a separate, unverified question.
Activity
amirkhan-ak-sf commentedon Sep 11, 2026
donkey.budget.last_usagedoes not exist yet, and #309 independently sketchesdonkey.last_routing— a different name on a different parent, describing the same HTTP response. Filed #362 to define the last-call record once so these per-call usage fields have a settled home; the cached/reasoning parsing itself stays scoped here.feat(skeleton): define the last-call record once at donkey.last_call (#…
docs: regenerate llms artifacts for usage-token page edits (#307)
feat(budget): surface cached/cache-write/reasoning token counts on la…
tommasobolis commentedon Sep 15, 2026
Delivered on
developvia #377 (squash commit 7155dc2). Auto-close didn't fire because the PR merged intodevelop, not the default branch; closing manually. Ships with the Phase 1 (0.1.0) promotion.