Skip to content

saved_usd prices our own token count at the provider's rate, under-reporting savings by ~12% (measured) #240

Description

@amiddavid

request_components.saved_usd — and every figure derived from it — is computed by pricing our own message-text token count at the provider's rate, rather than the tokens the provider actually billed. On a measured live turn it under-reports by 12.4%.

This is the same root cause as two defects fixed in #234, at a third site. It predates that PR and affects every saved_usd consumer, so it gets its own issue.

The measured pair

From the acceptance run on #234 (full data) — a real Claude Code session on claude-haiku-4-5, where a cold turn arrived after the transcript had been summarized:

The full prefix that turn would have re-created 149,363 tokens
The compacted prefix it did re-create 32,123 tokens
Tokens the compaction removed, as the provider billed them 117,240
At haiku's 5-minute cache-write rate (1.25 × $1/MTok) $0.14655
What saved_usd reported for the same removal $0.12834875
Implied token count behind that figure 102,679
Under-report 12.42%

The savings pipeline counted 102,679 tokens where the provider billed 117,240 for the same removed content — a ratio of 0.876.

Why the gap exists

Event.baselineDeltaUSD prices SavedUnique and Saved, which come from schema.MessagesTokens: this repo's own tokenizer, over message text only. The provider bills the whole serialized prompt. dash/overview.go's EstimatorDivergence already measures the general form of this — a median 3.38x across all traffic — but that figure is dominated by small transcripts where the fixed overhead (system prompt, tool declarations, JSON envelope) is most of the request. On a large transcript, which is where compaction happens, the divergence narrows to roughly 1.1-1.5x, and 12.4% is what it comes to on this turn.

Why it is worth filing rather than leaving

The direction is the interesting part. It under-reports, so:

It is the third site of one root cause. #234 fixed the same class of error twice:

  1. Trigger.Fires compared frac × window against MessagesTokens — a gate that could never fire, because a window is stated in billed tokens.
  2. The compaction-episode span was sized in one quantity and evaluated against another, twice over.
  3. This: saved_usd prices our token count at the provider's rate.

The first two were unreachable-gate and empty-measurement bugs. This one is a quiet 12% understatement, which is why it survived both.

Scope

Everything that reads saved_usd or baseline_cost_usd: the Components tab's per-component economics, NetSavedUSD/TotalSavedUSD, the savings waterfall, the compaction-episode panel's credit buckets, and /stats. So this is not a small change, and a fix wants:

  • a decision on what to price — the provider's billed delta is available per request (fresh_input + cache_read + cache_write before and after is not, but the written and read columns are), so this may be answerable from stored data rather than by improving the tokenizer;
  • the before/after figures published together, because the headline will move up;
  • a test pinning the relationship on a known pair, so the next tokenizer change does not move it silently.

Not on #234's branch

#234's own two instances of this root cause are fixed there. This third one predates it and affects consumers that PR does not touch, so per the repo rule it gets its own issue and branch off main.

Found by the reviewing session on #234, which re-derived the headline from the raw token counts rather than reading the reported figure — which is how the 12.4% gap surfaced at all.

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