fix(dash): price removed tokens as the provider counts them — closes #240 - #255
OsherElhadad wants to merge 1 commit into
Conversation
…240 `saved_usd`, `baseline_cost_usd` and every figure derived from them took OUR tokenizer's count of the removed content and priced it at the PROVIDER's rate. A quantity in one unit, a price in another. `cost_usd` was never affected — it is built from the four tiers the provider reports — so only the counterfactual moves. Measured on the deployed service, 281,421 requests over 2026-08-17 → 09-15: gross saved $103.0428 -> $173.6241 (+68.5%) net saved $86.5609 -> $157.1422 (+81.5%, 0.269% -> 0.488% of spend) The correction is a factor on a DIFFERENCE, which is why the 3.4x EstimatorDivergence is not a substitute: that ratio is of levels, and a level carries the system prompt, tool declarations and envelope that no component removes and that cancel in a difference. Measured by billing real captured turns twice, uncached, and reading `usage` off each reply: f = 1.3686 on haiku-4-5 and 1.6857 on the sonnet-5/opus-5 tokenizer, which returned byte-identical deltas on every pair. Corroborated to within 4-7% by an independent per-content-class measurement weighted by this corpus's own component mix. The class where our tokenizer is worst is grep output, which is what searchfold removes, and searchfold is 52% of all savings. Issue #240 asserted 12.4% from a single cross-turn substitution; the real gap is 26.9%/40.7%, so it understated its own finding. Two of its supporting arguments do not hold and the report says which. Also fixed, found on the way: - A fabricated cache-write premium. Both price paths filled a missing cache-write rate with `Input * 1.25` — Anthropic's published premium — for every model on earth. No other family charges to CREATE a cache entry, so every OpenAI/Gemini row had its savings priced 25% above anything billable. Worth at most $0.15 here, but it is an OVER-report, which is why "the error is always conservative" was never safe to believe. Both directions are now pinned by a test; a one-sided test is how that belief survived. - `TestLiteLLMPrices` required a creation premium on every model, encoding the defect as an invariant. Its stated intent is kept, the universal claim is not. - The comment that kept this alive: "Dollars are unaffected: they come from the provider's own reported usage" — true of cost_usd, false of the savings figure beside it. Deleted. - EstimatorDivergence was computed only on rows where nothing was compacted, the strict complement of the rows the savings come from. Both populations are now published so neither reads as the other. Not backfilled. History keeps its figures; `requests.billed_token_factor` defaults to 0 to MARK a pre-fix row, which is a different fact from "measured, correction 1.0", and the Overview says how much of a window is still on the old arithmetic. Additive column, so no schema bump and no history discarded. The token counts are unchanged. Component gates are evaluated against them, so moving those would change what gets compacted rather than what gets reported. Full before/after, per component, with the measurement method, the instrument that had to be thrown away, and what was deliberately left alone: docs/results/saved-usd-correction-2026-09.md Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
ReviewI checked the arithmetic by hand, read the accounting paths, ran the suite on the eval box, and replicated one of the two live instruments. Splitting findings by what each rests on, because that matters here. What I ran: What holds upEvery figure in §1 and §3 is internally consistent: baseline deltas, net/gross, share of spend, the The per-component "corrected" column was genuinely recomputed per row rather than scalar-multiplied — the ratios differ by component (1.6716 The central argument is right, and right for the reason given. I checked the quantity alignment specifically, since that is where this class of correction usually goes wrong: the test's denominator is
1. The read paths retroactively correct history, so "no backfill" is not true where the per-component numbers come from
Two consequences. (a) The write-path/read-path cross-check is now broken by exactly the factor. (b) The Components tab is backfilled; the Overview is not. Per The column added here is the right instrument and is not used for this. 2.
|
Closes #240.
request_components.saved_usd,baseline_cost_usdand everything derived from them took our own tokenizer's count of the content a component removed and priced it at the provider's rate. A quantity in one unit, a price in another.cost_usdwas never affected — it is built from the four token tiers the provider itself reports — so spend does not move. Only the counterfactual does.The numbers
Deployed service, snapshotted read-only 2026-09-15 11:58 UTC: 281,421 requests, 29,346 sessions, 19 tenants, 2026-08-17 → 09-15. The service was never written to or restarted.
SUM(cost_usd)Month-to-date, the window
/metricsand Grafana publish:cg_saved_usd$53.6004 → $90.3410,cg_net_saved_usd$44.2255 → $80.9661.Per component (deduplicated — the table has no primary key, see below):
Shares barely move — 98.4% of the saving is on one tokenizer. This makes no component profitable that was not;
extract_llmstill spends real money on its own calls and a larger gross does not settle its verdict.Why the correction is what it is
schema.MessagesTokenscounts message text witho200k_base— an OpenAI BPE — applied to every model. No system prompt, no tool declarations, no envelope.baselineDeltaUSDthen priced that count at the provider'sCacheWrite/repeatRate.The factor is on a difference:
Everything our counter cannot see is in both terms of the numerator and cancels. That is why the 3.4×
EstimatorDivergenceis not a substitute — that is a ratio of levels, and using it would over-report by roughly 2×. Issue #240 reached for it, and so did its review.Measured by
apply.TestBilledPairColdReplay: run the production pipeline, then send both bodies to the gateway with everycache_controlstripped so each bills its whole prompt as fresh input, and readusageoff each reply.Sonnet 5 and Opus 5 gave byte-identical deltas on every pair, so they share a tokenizer and haiku-4-5 does not — hence a per-family factor, not one constant.
Those 11 pairs are successive turns of two lineages, so a second, independent instrument was needed.
tokens.TestBilledFactorByContentClassmeasuresfper content class on generated text: it spans 1.04 to 1.76, and the worst class is grep output — exactly whatsearchfoldremoves, andsearchfoldis 52% of all savings. Weighted by this corpus's own component mix it gives 1.3150 / 1.5774, agreeing with the end-to-end figures to 4–7% with no shared machinery.Pre-registered kill criterion (if
f's interval spans 1.0, nothing moves): all 33 pairs and all 12 class measurements came back above 1.0. The direction is settled; the magnitude is stated as "roughly this" in the helper's own doc comment, because the effective n is a handful of lineages.An instrument that had to be thrown away
/v1/messages/count_tokensfirst gavef = 0.9961— which would have refuted the issue. It is unreliable in a size-dependent way, which is worse than plainly broken because a small-body check certifies it:So 35,221 tokens vanish at exactly the sizes compaction happens at, and what survives is a tiktoken count over messages — our own tokenizer, agreeing with itself. The same body billed through
/v1/messagesreturned 93,375. No number from that endpoint is used anywhere here;TestCountTokensIsNotABillingOraclecarries the finding.Where #240 was wrong
FilterUSDprecedent does not exist.filtered_decl_tokensistokens.Count(t.Raw)— the same estimator. What is measured there is the tier, never the quantity. There is no billed-count precedent in this repo.Also fixed
Input × 1.25, Anthropic's published premium. No other family charges to create an entry.cost_usdwas unaffected (those providers reportcache_write = 0) butbaselineDeltaUSDapplies the rate to our saved-token count regardless — so it reached the savings on traffic that could never pay it. Worth at most $0.15 here, and reported that way; its significance is that it is an over-report, which is why "the error is always conservative" was never safe. Both directions are pinned by a test.cost_usd, false ofbaseline_cost_usdbeside it. A reader auditing exactly this was told not to. Deleted.EstimatorDivergencemeasured on the wrong population — only rows where nothing was compacted, the strict complement of where savings come from. Both are now published.Deliberately not done
requests.billed_token_factordefaults to 0 to mark a pre-fix row — a different fact from "measured, correction 1.0" — and the Overview shows how much of a window is still on the old arithmetic. Additive column, no schema bump, no history discarded (verified against an existing DB).saved_usdis per component, andrequest_componentshas no billed columns and cannot have any.extract_econ.go:132still has the same* 1.25, in a gate that decides whether a call is made. Flagged, not touched here.cachehistory.go/toolsuggest.gohave the same root cause on different content classes (cache-boundary offsets, raw tool JSON). Applying a factor measured on removed message text to those would be the extrapolation this exercise is about avoiding. $0.0914 and $0.00 on this corpus.request_componentsrows inflate the raw sum by $0.9515 (0.99%). All from one day, 2026-08-22, none since. AUNIQUEindex cannot be created while they exist and removing them is a migration this change avoids. Recorded with its exact cost.event.go:642adds the premium to cost, never to the baseline) is real and inert:cache_write_1h = 0on all 281,421 rows.Verification
go test ./...— 29/29 test packages pass; the other 4 have no tests.go vetandgofmtclean.go test -vnaming them,IgnoredGoFilesempty for the touched packages) rather than trusting an exit code.73 tokens × $2.85/MTok × 1.6857 = $0.000351, exactly the stored delta.Full report, with every calculation and the reproduction commands:
docs/results/saved-usd-correction-2026-09.md.After merge
cg_saved_usdis month-to-date, so it will not jump to the corrected figure at restart — it drifts up as corrected rows accumulate and pre-fix rows age out. The Overview's "Savings on the OLD arithmetic" tile reports how much of the window is still on the old definition; when it disappears, the window is fully corrected.