Streaming asks the provider for usage and never extracts it, so every streamed call records zeros #139
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agent-proxy#139
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Filed from the ops seat with a live repro, 2026-08-19.
50af3deis deployed on ser8 and the streaming path still records no usage.Symptom
Post-roll spans on
agent-proxy,upstream.chat_stream, routesirens-echo/deepseek:gen_ai.usage.input_tokensmax = 0, so the attribute is written with a zero rather than omittedgen_ai.usage.cache_read_input_tokens= absentThe same route non-streaming records both correctly, including
cache_read_input_tokens = 15616on a repeated prefix. Soparse_cache_usageand the attribute plumbing are fine. Only the streaming feed into them is not.The two obvious causes are ruled out
The upstream does return the chunk. Called LiteLLM directly on ser8 with the agent-proxy key:
So LiteLLM honors
stream_options.include_usageand emits a usage-only final chunk carryingprompt_tokens_details.The dialect is right. The deploy chart renders the litellm backend with
"dialect" "openai", and_build_bodyinapp/upstream.pysetsstream_optionsfor every non-ollama dialect whenstream=true, which is what50af3deadded. The request is being made correctly.Where it looks like it goes wrong
50af3deis four lines and all of them are on the request side:parse_cache_usageis called insidechat_streamatapp/upstream.py:655, but the usage it receives is evidently empty. A zero rather than a null oninput_tokensis what an empty dict parsed into defaults looks like.The usage chunk from an OpenAI-compatible stream is unusual in shape: it arrives last, after
[DONE]-adjacent chunks, and carries"choices": []with only ausageobject. A normalizer that requires a choice, or that stops at the first chunk without a delta, would drop it silently. That is a guess about the mechanism. The measurements above are not a guess.A caller-visible symptom that may be the same bug: a client streaming through agent-proxy receives no usage chunk either, 14 chunks and none carrying usage, so whatever drops it does so before the client as well as before the span.
Repro, about two minutes
Step 2 returns a usage chunk. Step 1 does not, and its span records
input_tokens = 0.Why it matters beyond tidiness
coilyco-bridge/deploy#699exists because the demo lane appeared to have a dead prompt cache. It does not: the cache reads at 99.3% on that route, measured non-streaming with a control. The lane is 230 of 233 streaming, so every token and cache number it produces is dark, and an entire incident review was built on a zero that only meant "not reported".Until this is fixed, no token, cost, or cache question about any streaming consumer is answerable, and the zeros actively mislead.
Suggested acceptance
gen_ai.usage.input_tokensandoutput_tokens.gen_ai.usage.cache_read_input_tokens, matching what the non-streaming path reports for the same prefix.choicesarray, since that shape is what the current code appears to drop.tests/test_prompt_cache.pyalready covers the request side from50af3deand asserts nothing about the response side.Refs
coilyco-bridge/deploy#699,50af3deFixed, and the fix landed 15 minutes after this was filed.
f4a59d3is onmainand adds_fold_stream_usage, plus anelif payload.get("usage")branch beside thefinish_reasonone. That is exactly the mechanism this issue identified: the request was asking correctly and LiteLLM was answering correctly, and the normalizer read usage only in the branch that never sees it.Verified deployed rather than assumed. SigNoz, agent-proxy, 3h window ending ~07:00Z:
This issue measured
input_tokensmax = 0 andcache_read_input_tokensabsent. Both are now populated. The live repro and the ruling-out of the two obvious causes are what made the second half findable quickly - the direct LiteLLM call provingstream_optionswas honoured is what pointed at the normalizer rather than the request.Closing.
Withdrawing this. It is wrong.
50af3deworks and streaming usage extraction is fine. Olaf (ops seat), 2026-08-19.I claimed agent-proxy asks the provider for usage on streaming and never extracts it, on the strength of spans showing
input_tokens = 0and no cache attribute after the roll.Real lane traffic disproves it. Over 30 minutes on
sirens-echo/deepseek, 49 streaming calls, all post-roll:upstream.chat_streamcarries both attributes. Theparse_cache_usagecall atapp/upstream.py:655is doing its job.Where I went wrong: I probed with two hand-written streaming requests immediately after the roll, saw no usage on their spans, and generalised from a sample of two synthetic calls to the entire code path. I then went looking for a mechanism to explain a defect that was not there, and found a plausible-sounding one in the shape of the usage-only final chunk. Plausible is not measured.
Why my specific probe produced no usage is unresolved. Candidates are the tiny
max_tokens, the client closing early, or something about a direct caller versus the harness's path. None of it is worth chasing, and none of it is evidence about agent-proxy.Nothing in this issue should inform work. The repro steps do not reproduce a defect. Closing.
Refs
coilyco-bridge/deploy#699