Skip to content

proxy: usage_unreadable is structurally unreachable for a streamed response, which is where a spliced window is most likely to hide the block #207

Description

@amiddavid

The gap

usage_unreadable (added in #205) is reachable for a sniffed JSON response and structurally unreachable for a sniffed stream — which is the one place a truncated window is most likely to hide a usage block.

parseSSEUsageWhy only calls parseUsageWhy with a block usagePresent has already accepted, and parseUsageWhy reaches its ValidBytes branch only when no block was found anywhere. So worst ranges over absent / all_zero / unparsed_dialect and nothing else. A data: line cut mid-JSON yields no present block, so worst stays absent and the response reads as "the provider streamed no usage" — no counter, no shape record.

That is #200's own defect in the corner #205's classification does not cover: a measurement outage indistinguishable from a provider that said nothing.

Why it is narrow, and why it is still worth closing

Narrow because Anthropic puts input_tokens in message_start, at the front of the head window — so found is true and the classification is parsed. The reachable case needs a stream whose only usage block lands in the spliced middle.

Worth closing because the current detection is inferred rather than known. The buffered path decides "the bytes were not a whole document" from gjson.ValidBytes, which is a proxy for the fact it actually wants. The sniffer knows: sniffer.bytes() already computes s.total > len(s.head) to decide whether to splice at all.

Suggested fix

Give sniffer a spliced() bool and pass it into responseUsageWhy. Then:

  • an absent classification on a spliced window becomes unreadable_body, closing the streamed gap;
  • the buffered case stops inferring from ValidBytes and becomes exact;
  • valid_json in the shape record keeps its current meaning and gains a companion that says the window was cut.

It is a small change, but it threads a transport fact into a parser, which is a boundary #205 deliberately did not cross — the classification there is dialect-agnostic and reads only the bytes it is given. That is the decision this issue is for, not the code.

Not part of this

Adding the camelCase or nested dialects. Still blocked on a real response body or a shape record from a deployment that hits the gap, for the reason #200 gives: guessing between cacheWriteInputTokens and cacheCreationInputTokens produces a parser that looks correct and reads zero.

How it was found

Review of #205 (round 3), by the reviewer noticing that the two parsers are not symmetric and that a reader would assume they are. A comment at parseSSEUsageWhy's worst initialiser now states the asymmetry so the next reader does not have to rediscover it; this issue is the closure.

Related: #200 (the rule), #205 (the classification), #199 (waits on the shape record).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions