Skip to content

fix(pr-cost): price the claude lane by model family and cache TTL, and allow a corrected figure - #35

Merged
cheshirecode merged 3 commits into
mainfrom
fix/pr-cost-pricing-and-corrections
Sep 11, 2026
Merged

fix(pr-cost): price the claude lane by model family and cache TTL, and allow a corrected figure#35
cheshirecode merged 3 commits into
mainfrom
fix/pr-cost-pricing-and-corrections

Conversation

@cheshirecode

Copy link
Copy Markdown
Owner

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_RATES matches by longest prefix but carried only a claude-opus-4 row, so it swallowed claude-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-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 correct while usd_basis said 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-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 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_creation says which is which; the flat cache_creation_input_tokens total 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 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.

Prices verified against the bundled claude-api skill'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-duplicate now 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-hook deliberately has no such flag: a hook that re-fires must stay idempotent.

Design note

Rates come from a Rates NamedTuple 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 onto mainmain restructured skills/pr-cost/SKILL.md into a thin root plus references — so the surviving work is re-applied onto current main.

Not carried over, because main already has it: the cache read/write split in the reader (main ships it under different key names), and the dedup-by-message-id test (main has test_usage_key_contract.py).

Tests

Proved red against main:

Test Red on main
test_opus_4_6_is_not_priced_as_opus_4_0 15.0 != 5.0
test_opus_5_is_priced_from_the_table_not_the_defaults 'cli-default' != 'model-table'
test_one_hour_cache_writes_cost_twice_input KeyError
test_a_missing_cache_creation_breakdown_bills_the_cheaper_rate KeyError
test_allow_duplicate_publishes_a_corrected_figure argparse exit 2

Two 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 — fails 0 != 2 against a mutant that leaks the flag onto from-hook's subparser.

Verification

python3 -m unittest discover skills/pr-cost/tests  -> 76 tests OK
pr_cost_doctor.py --self-check                     -> OK
tests/run.sh                                       -> 157 pass, 0 fail, exit 0
ruff check (both changed files)                     -> clean

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.

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
cheshirecode force-pushed the fix/pr-cost-pricing-and-corrections branch from 04e4685 to d3dc403 Compare September 11, 2026 16:43
@cheshirecode
cheshirecode merged commit 55dc046 into main Sep 11, 2026
5 checks passed
@cheshirecode
cheshirecode deleted the fix/pr-cost-pricing-and-corrections branch September 11, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant