ask the gateway for usage on streaming requests, in all five engines - #179
Merged
Conversation
The OpenAI streaming API OMITS usage unless it is explicitly requested, and
nothing here ever requested it. The missing usage chunk was never the
gateway losing data — it was the gateway correctly honouring a request that
never asked.
Verified against llm.smoo.ai (LiteLLM 1.95.0, groq-gpt-oss-120b), same
prompt both ways:
without stream_options -> 7 chunks, 0 carrying usage
with stream_options -> 8 chunks, 1 carrying
"prompt_tokens": 73, "completion_tokens": 8
73 real prompt tokens where the streaming estimator hardcodes 0.
Everything built on top of the misattribution was compensating for one unset
request parameter: two char-count estimators, prompt_tokens pinned to 0,
completion_tokens = content.len()/4, the usage_estimated and cost_estimated
flags, and a cross-language "is this measured?" convention. The core comment
blaming "LiteLLM at llm.smoo.ai currently drops it for smooth-* aliases"
cited pearl th-eff0d0, which does not exist in the pearl store.
Sent only when streaming — meaningless otherwise, and leaving it off keeps a
non-streaming request byte-identical to before. Python honours an explicit
caller-supplied stream_options rather than overriding it.
Each engine has a test asserting the field reaches the wire (and is absent
when not streaming). Rust and Go verified to fail without the change:
`a streaming request must ask for usage or the gateway sends none; got: POST
/chat/completions HTTP/1.1`.
Rust 649 · Go 2 packages ok · Python 439 · TypeScript 423 · .NET 412 across
2 assemblies. clippy -D warnings, fmt --check, go vet, tsc --noEmit clean.
SEPARATE finding from the same experiment, NOT fixed here: the per-request
cost headers ARE present on a streamed response but all read 0.0, because at
header-flush time the completion is not priced yet. parse_gateway_cost
already maps 0 to None, so gateway cost is structurally unavailable on the
streaming path and the response_id -> LiteLLM_SpendLogs.request_id join
remains the only authoritative per-turn cost there.
Pearl th-5e59a5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkCz96UfUxcai5RU4LwPnG
🦋 Changeset detectedLatest commit: a1bbab9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
`uv sync --locked` fails in python/temporal: its lock records the editable `../core` dependency at 1.8.10 while core is now 1.10.0. The 1.10.0 release bumped core's version without re-locking the sibling package. Pre-existing on main and unrelated to this PR's change — but py-checks.yml runs a `[core, temporal]` matrix on any `python/core/**` touch, so it reds every PR that goes near the engine until someone re-locks. Deterministic one-line regeneration; `uv sync --locked` then passes and the temporal suite runs 10 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkCz96UfUxcai5RU4LwPnG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pearl th-5e59a5. One request field, five engines — and it invalidates the premise a lot of today's work was built on.
The gateway was never dropping usage
The OpenAI streaming API omits usage unless it is explicitly requested. Nothing here ever requested it. So the missing usage chunk was the gateway correctly honouring a request that never asked.
Verified against llm.smoo.ai (LiteLLM 1.95.0,
groq-gpt-oss-120b), same prompt both ways:stream_optionsstream_optionsThe usage-bearing chunk:
{"id":"chatcmpl-0c33d398-…","choices":[{"index":0,"delta":{}}], "usage":{"completion_tokens":8,"prompt_tokens":73,…}}73 real prompt tokens, where the streaming estimator hardcodes
0.What was built on the misattribution
Two char-count estimators,
prompt_tokenspinned to0,completion_tokens = content.len() / 4, theusage_estimatedandcost_estimatedflags, and a cross-language "is this measured?" convention — all compensating for one unset request parameter that nobody had tested.The comment driving it blamed "LiteLLM at llm.smoo.ai currently drops it for smooth-* aliases" and cited pearl th-eff0d0, which does not exist in the pearl store.
Scope
One field per gateway client, sent only when streaming — meaningless otherwise, and leaving it off keeps a non-streaming request byte-identical to before. Python honours an explicit caller-supplied
stream_optionsrather than overriding it.The estimators are deliberately left in place: they are now a rare fallback rather than the common path, and deleting them is a separate call once real usage is confirmed flowing in prod.
A second finding from the same experiment — NOT fixed here
I captured the response headers while I was in there. The per-request cost headers ARE present on a streamed response, and all read
0.0:So both halves of the open dispute were half right: the headers do survive streaming (they arrive), and the cost is unusable there (nothing is priced at header-flush time).
parse_gateway_costalready maps0→None, so gateway cost is structurally unavailable on the streaming path, and theresponse_id→LiteLLM_SpendLogs.request_idjoin remains the only authoritative per-turn cost there. That makesresponse_idmore valuable, not less.Verification
Each engine has a test asserting the field reaches the wire, and that it is absent on a non-streaming request. Rust and Go confirmed to fail without the change:
clippy -D warnings+fmt --checkcleanok·go vetcleantsc --noEmitcleanNext
Worth running a real turn after release and checking whether
gen_ai_events.usage_input_tokensis finally non-zero — that's the experiment that closes th-5e59a5 and decides how much the provenance work in the other four cores is actually worth.🤖 Generated with Claude Code
https://claude.ai/code/session_01LkCz96UfUxcai5RU4LwPnG