Skip to content

tables: price and classify claude-opus-5-5 (Claude Opus 5.5) and claude-sonnet-5-5 (Claude Sonnet 5.5) - #375

Open
codeslake wants to merge 9 commits into
cnighswonger:mainfrom
codeslake:fix/opus-5-5-model-tables
Open

codeslake wants to merge 9 commits into
cnighswonger:mainfrom
codeslake:fix/opus-5-5-model-tables

Conversation

@codeslake

@codeslake codeslake commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Claude Code 2.1.280 makes claude-opus-5-5 (Claude Opus 5.5) its default Opus, and none of the model tables knew the id:

  • tools/rates.json had no entry, so session-budget-breaker's exact rate lookup returned null (fails open), and tools/cost-report.mjs's lookupRates() fell through to the first key in file order that prefixes it, claude-opus-5: 25% high on input and output, 2.5x on cache reads.
  • tools/update-rates.mjs had no name for it, and its multiplier check would have rejected the row anyway: Opus 5.5 prices cache reads at 0.05x input (footnoted on the pricing page), not the standard 0.1x.
  • proxy/model-families.mjs had no root for claude-opus-5, claude-opus-5-5 or claude-sonnet-5, so all three read "unknown" and cache-telemetry's cross-family divergence latch could not fire for them.

Rates, from the 2.1.280 bundle's tier_4_20_cache_read_0_20 and the live pricing page, USD per MTok: input 4, output 20, 5m cache write 5, 1h cache write 8, cache read 0.2.

Claude Code 2.1.284 makes claude-sonnet-5-5 (Claude Sonnet 5.5) its default Sonnet, and the same tables lacked it: tools/rates.json had no row (the breaker's exact lookup counted no cost for it, and lookupRates() priced it through the claude-sonnet-5 prefix), and tools/update-rates.mjs had no name for it, so a refresh never wrote it. The live pricing page lists it at input 2, output 10, 5m cache write 2.5, 1h cache write 4, cache read 0.2, the same as Sonnet 5 today. proxy/model-families.mjs already classifies it as sonnet through the claude-sonnet-5 root.

Two defects surfaced while pricing Sonnet 5.5, both in code this PR already touches:

  • tools/update-rates.mjs read the pricing table by position (input, 5m, 1h, hits, output). The live page now orders its columns Input, Output, 5m writes, 1h writes, Hits and refreshes under a two-row header, so a real refresh parsed 0 rates with 27 errors and failed closed. With the columns in the right order every parsed rate matches tools/rates.json.
  • session-budget-breaker's onStreamEvent read the session id from ctx.headers, which the stream ctx built in proxy/stream.mjs does not carry, so no streamed turn accrued and none of its levers could fire on Claude Code traffic. The breaker's tests passed because their stream fixtures supplied headers.

Changes

  • tools/rates.json: claude-opus-5-5, placed before claude-opus-5 so the prefix fallback prefers it for a dated 5.5 id; claude-sonnet-5-5, placed before claude-sonnet-5 for the same reason.
  • tools/update-rates.mjs: "Claude Sonnet 5.5" in NAME_TO_WIRE and REQUIRED_WIRE_IDS.
  • tools/probe-tool-addition.mjs: the default lineup probes claude-sonnet-5-5 in place of claude-sonnet-5.
  • tools/update-rates.mjs: each table's column order comes from its own header row, matched by keyword; a header that does not name each price column once yields no rows, so the run still fails closed. New fixture test/fixtures/pricing-page-2026-09-28.html, the live page's model-pricing table.
  • proxy/extensions/session-budget-breaker.mjs: onStreamEvent takes the session id onRequest stashed on ctx.meta, as onResponse already did. A new test drives the real streamResponse; the stream fixtures no longer supply headers, and tools/sim-session-budget-breaker.mjs builds the real ctx shape too. One CHANGELOG Fixed bullet.
  • tools/update-rates.mjs: "Claude Opus 5.5" in NAME_TO_WIRE and REQUIRED_WIRE_IDS; PRICE_CELL accepts one trailing footnote marker ($0.20 / MTok 2, one space and one or two digits, so $4 / MTok2026 still fails closed); a per-model cache-read multiplier override (claude-opus-5-5: 0.05) that leaves every other row and field on the standard check.
  • proxy/model-families.mjs: roots claude-opus-5 -> opus, claude-sonnet-5 -> sonnet.
  • tools/cost-report.mjs: unchanged; the new exact key wins.
  • Tests: test/cost-report.test.mjs (new, one case: a 1M/1M/1M-cache-read Opus 5.5 row costs 24.2, it read 30.5 before), test/tools-update-rates.test.mjs (the Opus 5.5 row parses to its own rates; the override does not loosen the guard for other models; a footnote-shaped suffix without the space fails closed; the shipped rates.json carries the id), test/model-families.test.mjs (bare Opus 5, Opus 5.5, Sonnet 5 and Sonnet 5.5 classify), test/proxy-session-budget-breaker.test.mjs (the cost lever prices claude-sonnet-5-5 instead of failing open), and in test/tools-update-rates.test.mjs the Sonnet 5.5 row parses as its own row, not as Sonnet 5's. The 2026-07-27 pricing fixture gains the Opus 5.5 and Sonnet 5.5 rows copied from the live page, noted in the test header.
  • CHANGELOG.md: one Fixed bullet.

Test plan

  • Each new case failed on the base (0e7aa80 for Opus 5.5, 10f2f78 for Sonnet 5.5) before its fix and passes on the head. The Sonnet 5.5 family assert is coverage and passes on both.
  • The live-shaped update-rates case and the streamed-accrual breaker case each failed on the commit before their fix.
  • node --test: 1957 tests, 1956 pass, 0 fail, 1 skipped.

Not changed here

  • tools/cost-report.mjs --live-rates / --update-rates still use its older parser, which needs MTok</td> in every cell, so it drops the footnoted Opus 5.5 row and rewrites rates.json without it. tools/update-rates.mjs is the validated updater.
  • Claude Fable 5.1 and Mythos 5.1 are on the pricing page with a 0.025x cache-read footnote and are not added.

🤖 Generated with Claude Code

codeslake and others added 2 commits September 23, 2026 02:09
- tools/rates.json: add the wire id, ordered before claude-opus-5 so a
  future dated 5.5 snapshot resolves via cost-report's prefix match
  rather than falling through to Opus 5's rate.
- tools/update-rates.mjs: map the display name, require the wire id on
  every refresh, accept the pricing page's footnote marker on the
  cache-read cell, and validate that cell against its documented 0.05x
  discount instead of the standard 0.1x (a per-model override, not a
  loosened guard for every other row).
- proxy/model-families.mjs: classify bare claude-opus-5 and
  claude-sonnet-5 ids (and by prefix, claude-opus-5-5) so served-model
  divergence detection stops reporting them unknown.
- Tests: the fixture gains the live Opus 5.5 row: cost-report priced a
  1M/1M/1M call at $30.5 (Opus 5's rate) before this change, $24.2
  after.

Co-Authored-By: Claude <noreply@anthropic.com>
- CHANGELOG.md: replace the false "fell back to its longest-matching
  neighbor" with what lookupRates() actually does (first rates.json key
  in file order where either id prefixes the other); fold in the
  model_divergence_sticky latch-timing fix (first divergent turn, not
  third) as the same bullet.
- tools/update-rates.mjs PRICE_CELL: narrow the footnote tolerance to
  exactly one space + 1-2 digits, matching what tableRows() actually
  leaves behind, so junk like "$4 / MTok2026" fails closed.
- test/tools-update-rates.test.mjs: red-first case for the narrowed
  regex, and note the Opus 5.5 fixture row was copied in from the later
  live page.

Co-Authored-By: Claude <noreply@anthropic.com>
@codeslake
codeslake marked this pull request as ready for review September 23, 2026 06:24
Claude Code 2.1.284's default Sonnet is claude-sonnet-5-5. update-rates
never wrote it (no NAME_TO_WIRE / REQUIRED_WIRE_IDS entry), rates.json had no
row so the session-budget-breaker's exact-id lookup counted no cost for it, and
the tool-addition probe still named claude-sonnet-5.

Co-Authored-By: Claude <noreply@anthropic.com>
@codeslake codeslake changed the title tables: price and classify claude-opus-5-5 (Claude Opus 5.5) tables: price and classify claude-opus-5-5 (Claude Opus 5.5) and claude-sonnet-5-5 (Claude Sonnet 5.5) Sep 28, 2026
codeslake and others added 6 commits September 28, 2026 18:23
…osition

The live pricing page now has two header rows and orders the columns
Input | Output | 5m writes | 1h writes | Hits and refreshes. priceRow() read
them by fixed position, so every real refresh parsed 0 rates and 27 errors
and failed closed. Take the column order from the header row that names
Input/Output/5m/1h/Hits, per table; a header that does not name each once
yields no rows (fails closed), never a guessed order.

Fixture: the model-pricing table of the 2026-09-28 live page. Inline test
tables gain a header row, since a table without one now yields no rows.

Co-Authored-By: Claude <noreply@anthropic.com>
…uest stashed on meta

onStreamEvent read the session id from ctx.headers, but the real stream ctx
(proxy/stream.mjs) has no request headers, so no streamed request accrued and
the token, cost and rate levers never fired on real traffic. Read
ctx.meta._sbbSessionId, as onResponse does. Stream fixtures now model the real
ctx, and one test drives the shipped streamResponse. The sim's stream legs
carried the same wrong shape and are corrected.

Co-Authored-By: Claude <noreply@anthropic.com>
…5.5)

modelFamily() read claude-haiku-5-5 as unknown (the claude-haiku-4-5 root cannot
match), and rates.json had no entry, so the session-budget-breaker cost lever
counted $0 for the CC 2.1.293 default Haiku. Add the family root and the
lower-tier (up to 100,000 tokens) rates. The live page tiers this row, so the
parser reads no row for it and a refresh keeps the hand-written entry (measured
against the live page); NAME_TO_WIRE prices it should the page go flat.

Co-Authored-By: Claude <noreply@anthropic.com>
The shipped-entry comparison read undefined == undefined on a tree with no
claude-haiku-5-5 entry and passed. Compare both the flat-row parse and
rates.json to the literal rates; carry the ponytail marker for the lower-tier
ceiling on the NAME_TO_WIRE row.

Co-Authored-By: Claude <noreply@anthropic.com>
The 'direct is the LAST resort' case counted two refusal traces for two
dead hops but took both ports from freePort(), which gives the number
back before returning. The kernel can return the same number to the next
call (the relay folds a repeated hop into one) or to the relay's carrier
(the relay drops its own address as a hop), and a neighbouring process's
listener on a freed port answers as a live hop. Each leaves one trace:
'1 refusals traced, want one per hop ... 1 !== 2', measured at ~1% of
runs with 40 concurrent runners and reproduced deterministically by
forcing either collision.

Use ports 1 and 2, as proxy-forward-attach-fallback.test.mjs already does
for its dead hop: an unprivileged runner cannot bind below 1024 and a
connect there refuses at once. The two other cases in the file that
dialled a freePort() hop (the second-hop fall-through, whose trace
assertion fails the same way, and REQUIRE_HOP, which passed while
silently measuring a no-hop chain) take the same constants.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude Code 2.1.296 and the pricing page as of 2026-10-10 both price Sonnet 5.5
cache reads at $0.10 per MTok (was $0.20), 0.05x its $2 input. rates.json still
carried $0.20, so tools/cost-report.mjs over-counted a 1M-read call by $0.10, and
update-rates.mjs refused the live row ("contradicts the documented 0.1x input
multiplier"), leaving rates.json untouched on every refresh.

rates.json takes $0.10. update-rates.mjs adds the 0.05x override Opus 5.5 already
uses, gated to pages dated 2026-10-10 or later: the 2026-09-28 page and the
2026-07-27 fixture still say $0.20 and keep parsing at their own dates.

New fixture pricing-page-2026-10-10.html (the model-pricing table of the live
page). The "rates.json agrees with the page" check now reads it, since it is the
newest page; the older fixtures' own parse assertions are unchanged. cost-report
gains a Sonnet 5.5 case beside the Opus 5.5 one.

Co-Authored-By: Claude <noreply@anthropic.com>
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