The defect
parseUsage returning ok=false means one of three different things, and nothing distinguishes them:
- the provider genuinely reported no usage (an OpenAI response without
stream_options, for instance);
- the response carried usage in a spelling the parser does not recognise;
- usage was present but every tier was zero.
Only the first is benign. The second is a measurement outage, and it degrades silently: a healthy 200, correct savings counters, correct latency, correct everything else — and fresh_input_tokens, cache_read_tokens, cache_write_tokens quietly at 0.
It has already happened, at full scale, for two iterations
Iteration 024 recorded usage_reported=false on 4,015 of 4,015 requests (1,808 arm A, 2,207 arm B, zero exceptions), with all four token fields at 0, every request provider=anthropic route=/v1/messages sse=False buffered=False.
Nothing surfaced it. It was found in a post-mortem two iterations later, while trying to answer a different question — and only because someone went looking for why a cost figure could not be bounded.
The usage was there. LOCA reported Avg Cost: $2.390637 per arm-seed, and litellm computes cost from the response's usage, so the body that flowed gateway → proxy → harness did carry it. The proxy passed it through and parseUsage found nothing in it.
Why a counter, and why this specific split
The parser handles three spellings and silently fails on others. Measured directly over candidate shapes:
| Shape |
parseUsage |
Anthropic usage.input_tokens |
parses |
OpenAI usage.prompt_tokens |
parses |
Output-only usage.output_tokens |
parses |
Bedrock Converse camelCase usage.inputTokens |
finds nothing |
usage nested (e.g. response.usage) |
finds nothing |
| usage absent |
correctly nothing |
| usage present, all tiers zero |
correctly nothing |
So an unrecognised dialect is indistinguishable from a provider that said nothing — which is the same failure the codebase already refuses to accept in two other places:
This is the third instance of that pattern, and the argument is identical.
Suggested fix
- A counter —
usage_unparsed at /stats and cg_usage_unparsed_total at /metrics: a response arrived, it was not empty, and no recognised usage spelling was found in it. Non-zero means the proxy is not accounting tokens for some route or provider, which is currently invisible.
- A DEBUG record of the shape on the first such response per process — the top-level keys and the keys under
usage, not the body (it carries content). That is the datum that would turn "usage_reported is false" into "the provider is sending camelCase" without needing a body dump at all.
- Optionally distinguish case 3 (present but all-zero) in the log line, since it is legitimate on some responses and would otherwise inflate the counter.
Deliberately not part of this: adding the camelCase or nested dialects. That needs a real response body to get the field names right — guessing between cacheWriteInputTokens and cacheCreationInputTokens produces a parser that looks correct and reads zero, which is the failure this issue is about, reproduced in the code meant to fix it. The counter and the shape record are dialect-agnostic and are what make the next gap visible in one run rather than two iterations.
How it was found
Tracing why iteration 024 had no cache-token measurement, with the coref validation session — which also made the point that a counter here would have surfaced this in iteration 022 rather than in iteration 024's post-mortem. Related: #199 (needs these fields live to bound its cost question), #190 (needs them to check whether refusals suppress the recovery).
The defect
parseUsagereturningok=falsemeans one of three different things, and nothing distinguishes them:stream_options, for instance);Only the first is benign. The second is a measurement outage, and it degrades silently: a healthy 200, correct savings counters, correct latency, correct everything else — and
fresh_input_tokens,cache_read_tokens,cache_write_tokensquietly at 0.It has already happened, at full scale, for two iterations
Iteration 024 recorded
usage_reported=falseon 4,015 of 4,015 requests (1,808 arm A, 2,207 arm B, zero exceptions), with all four token fields at 0, every requestprovider=anthropic route=/v1/messages sse=False buffered=False.Nothing surfaced it. It was found in a post-mortem two iterations later, while trying to answer a different question — and only because someone went looking for why a cost figure could not be bounded.
The usage was there. LOCA reported
Avg Cost: $2.390637per arm-seed, and litellm computes cost from the response's usage, so the body that flowed gateway → proxy → harness did carry it. The proxy passed it through andparseUsagefound nothing in it.Why a counter, and why this specific split
The parser handles three spellings and silently fails on others. Measured directly over candidate shapes:
parseUsageusage.input_tokensusage.prompt_tokensusage.output_tokensusage.inputTokensusagenested (e.g.response.usage)So an unrecognised dialect is indistinguishable from a provider that said nothing — which is the same failure the codebase already refuses to accept in two other places:
expand/unresolved.gosplits malformed from missing, because a bare miss cannot tell them apart and they call for opposite responses;stash_missingfromstash_refusedfor exactly the same reason — one counter was reporting both "nothing broke" and "something broke".This is the third instance of that pattern, and the argument is identical.
Suggested fix
usage_unparsedat/statsandcg_usage_unparsed_totalat/metrics: a response arrived, it was not empty, and no recognised usage spelling was found in it. Non-zero means the proxy is not accounting tokens for some route or provider, which is currently invisible.usage, not the body (it carries content). That is the datum that would turn "usage_reported is false" into "the provider is sending camelCase" without needing a body dump at all.Deliberately not part of this: adding the camelCase or nested dialects. That needs a real response body to get the field names right — guessing between
cacheWriteInputTokensandcacheCreationInputTokensproduces a parser that looks correct and reads zero, which is the failure this issue is about, reproduced in the code meant to fix it. The counter and the shape record are dialect-agnostic and are what make the next gap visible in one run rather than two iterations.How it was found
Tracing why iteration 024 had no cache-token measurement, with the coref validation session — which also made the point that a counter here would have surfaced this in iteration 022 rather than in iteration 024's post-mortem. Related: #199 (needs these fields live to bound its cost question), #190 (needs them to check whether refusals suppress the recovery).