fix(pr-cost): price the claude lane by model family and cache TTL, and allow a corrected figure - #35
Merged
Conversation
Two pricing defects in the claude reader, both producing a plausible number rather than an error. 1. The rate table matched by longest prefix but only carried a "claude-opus-4" row, so it swallowed claude-opus-4-6/4-7/4-8 and charged them Opus 4.0's 15/75. Those models cost 5/25 -- a 3x overestimate. The table also had no row for the current families at all, so claude-opus-5 fell through to the flat defaults. Those defaults are 5/25, which happen to equal Opus 5's real rates, so the dollar figure looked right while usd_basis said it was not priced from the model. Added rows: fable-5-1 (with its 0.025x cache read), fable-5, opus-5, opus-4-8, opus-4-7, opus-4-6, sonnet-5. claude-opus-4 keeps 15/75 for genuine 4.0/4.1; longest-prefix matching separates them. sonnet-4 and haiku-4 already resolved correctly for 4-6 and 4-5. 2. Every cache write was priced at 1.25x input, the 5-minute TTL rate. Claude Code sessions run the 1-hour TTL, which costs 2x. usage.cache_creation says which is which; the flat cache_creation_input_tokens total does not. The reader now reads the split and emits cache_write_5m_input_tokens and cache_write_1h_input_tokens. This was a systematic undercharge. Transcripts predating cache_creation carry no breakdown; those writes bill at the cheaper 5-minute rate, so a missing split understates rather than inflates, and the two fields still sum to the total. Rates come from a Rates NamedTuple built by one helper that states Anthropic's published multipliers once -- reads 0.1x input, writes 1.25x (5-minute) and 2x (1-hour) -- so a hand-typed write rate cannot drift away from its input rate. Every pre-existing table value is reproduced exactly. Four of the five new tests proved red against main's reader: 15.0 != 5.0 for the Opus 4.6 overcharge, 'cli-default' != 'model-table' for the Opus 5 label, and KeyError on both TTL fields. The fifth pins Opus 4.0 on its own higher row and is green both ways by design -- it is the guard that stops the 4-6/4-7/4-8 rows from being "simplified" back into the bare prefix. Verified: 74 pr-cost tests OK; ruff clean on both files.
The duplicate guard keys on pr_url + session_id, so the annotate that carries a corrected number for a session already in the ledger was refused -- the exact case someone hits first after learning their posted figure was priced wrong. Before this, the only routes were editing the ledger by hand or changing the session id. `annotate --allow-duplicate` publishes it and reports `"status": "corrected"`. The row is appended rather than substituted, so the ledger keeps both what was published and what replaced it. `from-hook` deliberately has no such flag: a hook that re-fires must stay idempotent, or one retried PR create posts the cost twice. append_ledger now returns the status string instead of a bool, which is what both call sites already derived from it. test_allow_duplicate_publishes_a_corrected_figure proved red against main's collector: argparse exits 2 on the unknown flag. test_from_hook_has_no_allow_duplicate_escape is green both ways by design -- it is the guard, and it fails (0 != 2) against a mutant that leaks the flag onto from-hook's subparser. Verified: 76 pr-cost tests OK; doctor --self-check OK. Follow-up, not touched here: pr_cost_collect.py has imported `tempfile` without using it since before this change (ruff F401). pr-cost is not ruff-gated in CI, so nothing fails on it today.
pr_cost_collect.py imported tempfile without using it. The import was the only reference in the file, so nothing changes at runtime; ruff F401 goes quiet. pr-cost is not ruff-gated in CI, so this was not failing anything -- it is cleanup of the follow-up noted on the previous commit.
cheshirecode
force-pushed
the
fix/pr-cost-pricing-and-corrections
branch
from
September 11, 2026 16:43
04e4685 to
d3dc403
Compare
This was referenced Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two pricing defects in pr-cost's claude lane, plus the missing escape hatch for republishing a figure that was priced wrong.
Why: both defects produced a plausible number, not an error
1. The rate table charged Opus 4.6+ at Opus 4.0's rates.
MODEL_RATESmatches by longest prefix but carried only aclaude-opus-4row, so it swallowedclaude-opus-4-6,-4-7, and-4-8. Those cost $5/$25; the row charged them $15/$75 — a 3× overestimate.It also had no row for the current families at all, so
claude-opus-5fell through to the flat defaults. Those defaults are 5/25, which happen to equal Opus 5's real rates — so the dollar figure looked correct whileusd_basissaid it was not priced from the model. The dollar figure alone cannot catch that, which is why the test asserts the label.Added:
fable-5-1(with its 0.025× cache read),fable-5,opus-5,opus-4-8,opus-4-7,opus-4-6,sonnet-5.claude-opus-4keeps 15/75 for genuine 4.0/4.1 — longest-prefix matching separates them.sonnet-4andhaiku-4already resolved correctly for 4-6 and 4-5.2. Every cache write was priced at the 5-minute TTL rate.
Writes cost 1.25× input at the 5-minute TTL and 2× at the 1-hour TTL. Claude Code sessions run the 1-hour TTL.
usage.cache_creationsays which is which; the flatcache_creation_input_tokenstotal does not. This was a systematic undercharge.The reader now reads the split and emits
cache_write_5m_input_tokens/cache_write_1h_input_tokens. Transcripts predatingcache_creationcarry no breakdown; those writes bill at the cheaper 5-minute rate, so a missing split understates rather than inflates — and the two fields still sum to the total.Prices verified against the bundled
claude-apiskill's model table and caching doc rather than written from memory.3. A corrected figure could not be published.
The duplicate guard keys on
pr_url+session_id, so the annotate carrying a corrected number for the same session was refused — the case someone hits first after learning their posted figure was priced wrong.annotate --allow-duplicatenow publishes it and reports"status": "corrected", appending a second ledger row rather than editing the first, so the ledger keeps both what was published and what replaced it.from-hookdeliberately has no such flag: a hook that re-fires must stay idempotent.Design note
Rates come from a
RatesNamedTuple built by one helper that states Anthropic's published multipliers once — reads 0.1× input, writes 1.25× / 2×. A hand-typed write rate can no longer drift away from its input rate. Every pre-existing table value is reproduced exactly.Provenance
This revives
fix/pr-cost-cache-visibility(2 commits, never pushed). That branch no longer rebases ontomain—mainrestructuredskills/pr-cost/SKILL.mdinto a thin root plus references — so the surviving work is re-applied onto currentmain.Not carried over, because
mainalready has it: the cache read/write split in the reader (mainships it under different key names), and the dedup-by-message-id test (mainhastest_usage_key_contract.py).Tests
Proved red against
main:test_opus_4_6_is_not_priced_as_opus_4_015.0 != 5.0test_opus_5_is_priced_from_the_table_not_the_defaults'cli-default' != 'model-table'test_one_hour_cache_writes_cost_twice_inputKeyErrortest_a_missing_cache_creation_breakdown_bills_the_cheaper_rateKeyErrortest_allow_duplicate_publishes_a_corrected_figureTwo are green both ways by design, and each was mutation-checked so it is not decoration:
test_opus_4_0_keeps_its_own_higher_rate— the guard stopping the 4-6/4-7/4-8 rows from being "simplified" back into the bare prefix.test_from_hook_has_no_allow_duplicate_escape— fails0 != 2against a mutant that leaks the flag ontofrom-hook's subparser.Verification
Follow-up, not touched here
pr_cost_collect.pyhas importedtempfilewithout using it since before this change (ruff F401).pr-costis not ruff-gated in CI, so nothing fails on it today.