From c78d597b9364f1f8e10ceca3b976d30c66514bb4 Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Sat, 15 Aug 2026 04:25:29 +0000 Subject: [PATCH 1/2] refactor: dedup identical opus-tier rates in modelPricing EXACT_RATES (#4163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All five claude-opus-* rows carried the same [5.0, 25.0] literal, and the /opus/i FAMILY_RULES entry had to be re-pointed at the newest id on every opus bump (migration 206 and every bump before it) purely to relabel the reported rateModel — zero behavioral effect, two edit sites. The tier now owns one OPUS_TIER_RATES pair and a newest-first OPUS_MODEL_IDS list that generates its EXACT_RATES rows; the family rule takes its pointer from the head of that list. An opus bump is a one-line prepend. The pointer stays a real EXACT_RATES key rather than becoming inline rates on the family rule, because rateModel is load-bearing downstream: UsagePage prints "Priced as ", and usageReconciler compares two ids' rateModel to decide whether a recorded and a transcript model are the same family (a null or synthetic label would make 'opus' and 'claude-opus-5' stop matching). A differential run of resolveModelRates and isFreeModelId over 954 provider/model pairs before and after returns byte-identical results. --- .changelog/next/changed-issue-4163.md | 1 + server/lib/modelPricing.js | 28 +++++++++++--- server/lib/modelPricing.test.js | 55 ++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 .changelog/next/changed-issue-4163.md diff --git a/.changelog/next/changed-issue-4163.md b/.changelog/next/changed-issue-4163.md new file mode 100644 index 0000000000..ed9e66036f --- /dev/null +++ b/.changelog/next/changed-issue-4163.md @@ -0,0 +1 @@ +- Deduped the identical Claude Opus rate rows in the usage-cost table: the tier now shares one rate pair and the opus family rule derives its pointer, so an opus model bump no longer needs a second hand-edit (#4163) diff --git a/server/lib/modelPricing.js b/server/lib/modelPricing.js index aeacbc1f12..500f2c500b 100644 --- a/server/lib/modelPricing.js +++ b/server/lib/modelPricing.js @@ -31,16 +31,29 @@ export const PRICING_AS_OF = '2026-07-12'; +/** + * Every shipped Claude Opus generation bills at the same published rate, so the + * tier owns one [input, output] pair rather than a hand-copied literal per row. + * `OPUS_MODEL_IDS` is ordered NEWEST FIRST: its head is the id the `/opus/i` + * family rule reports for opus ids the table doesn't list, so an opus bump is a + * single prepend here — the family rule no longer has to be re-pointed by hand + * (it was, at every bump through migration 206, for zero behavioral effect). + */ +const OPUS_TIER_RATES = [5.0, 25.0]; +const OPUS_MODEL_IDS = [ + 'claude-opus-5', + 'claude-opus-4-8', + 'claude-opus-4-7', + 'claude-opus-4-6', + 'claude-opus-4-5', +]; + /** USD per 1M tokens: [input, output]. Exact model-id matches. */ const EXACT_RATES = { // Anthropic 'claude-fable-5': [10.0, 50.0], 'claude-mythos-5': [10.0, 50.0], - 'claude-opus-5': [5.0, 25.0], - 'claude-opus-4-8': [5.0, 25.0], - 'claude-opus-4-7': [5.0, 25.0], - 'claude-opus-4-6': [5.0, 25.0], - 'claude-opus-4-5': [5.0, 25.0], + ...Object.fromEntries(OPUS_MODEL_IDS.map((id) => [id, OPUS_TIER_RATES])), 'claude-sonnet-5': [2.0, 10.0], // intro pricing through 2026-08-31 ($3/$15 after) 'claude-sonnet-4-6': [3.0, 15.0], 'claude-sonnet-4-5': [3.0, 15.0], @@ -94,7 +107,10 @@ const EXACT_KEYS_BY_LENGTH = Object.keys(EXACT_RATES).sort((a, b) => b.length - */ const FAMILY_RULES = [ { test: /fable|mythos/i, rateModel: 'claude-fable-5' }, - { test: /opus/i, rateModel: 'claude-opus-5' }, + // Reports the newest listed opus id (see OPUS_MODEL_IDS) — the whole tier + // shares one rate pair, so the pointer only supplies the label, and deriving + // it means an opus bump never has to edit this line. + { test: /opus/i, rateModel: OPUS_MODEL_IDS[0] }, { test: /sonnet[-.]?5/i, rateModel: 'claude-sonnet-5' }, { test: /sonnet/i, rateModel: 'claude-sonnet-4-5' }, { test: /haiku/i, rateModel: 'claude-haiku-4-5' }, diff --git a/server/lib/modelPricing.test.js b/server/lib/modelPricing.test.js index f75fd5e833..0acb158089 100644 --- a/server/lib/modelPricing.test.js +++ b/server/lib/modelPricing.test.js @@ -10,11 +10,64 @@ describe('resolveModelRates', () => { }); it('resolves CLI shorthand model names via family rules', () => { - expect(resolveModelRates('claude-code', 'opus')).toMatchObject({ rateModel: 'claude-opus-5', matched: 'family' }); + // Asserted by RATES, not by pointer id: the whole opus tier shares one rate + // pair, so which listed opus id gets reported is a bump-to-bump detail the + // suite should not have to be edited for. The pointer's validity is pinned + // by the dedicated test below. + expect(resolveModelRates('claude-code', 'opus')).toMatchObject({ inputPer1M: 5, outputPer1M: 25, matched: 'family' }); expect(resolveModelRates('claude-code', 'sonnet')).toMatchObject({ rateModel: 'claude-sonnet-4-5', matched: 'family' }); expect(resolveModelRates('claude-code', 'haiku')).toMatchObject({ rateModel: 'claude-haiku-4-5', matched: 'family' }); }); + // #4163: the opus tier is one shared rate pair, and the `/opus/i` family rule + // derives its pointer from the model list instead of naming an id by hand. + // These three pin the properties that made the hand-maintained pointer safe, + // so a future opus bump is a one-line prepend with no other edit anywhere. + describe('opus tier', () => { + const LISTED_OPUS_IDS = [ + 'claude-opus-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-opus-4-6', 'claude-opus-4-5', + ]; + + it('bills every listed opus generation at the same tier rates', () => { + for (const id of LISTED_OPUS_IDS) { + expect(resolveModelRates('claude-code', id)).toMatchObject({ + rateModel: id, inputPer1M: 5, outputPer1M: 25, matched: 'exact', + }); + } + }); + + // The point of the dedup: an opus id the table has never heard of still + // prices at $5/$25 without anyone re-pointing a family rule at it. + it('prices an unlisted opus id at tier rates without a pointer bump', () => { + for (const id of [ + 'opus', 'claude-opus-9', 'claude-opus-6-2', 'global.anthropic.claude-opus-7-20270101-v1:0', + ]) { + expect(resolveModelRates('claude-code', id)).toMatchObject({ + inputPer1M: 5, outputPer1M: 25, matched: 'family', + }); + } + }); + + // The family rule reports a LABEL, and downstream code leans on it: the UI + // prints "Priced as ", and usageReconciler compares two ids' + // rateModel to decide whether they are the same family. So the pointer must + // stay a non-null, exactly-resolvable key whose rates agree with the tier — + // a null or synthetic label would silently break both. + it('reports a family label that is itself an exact table id at the same rates', () => { + const viaFamily = resolveModelRates('claude-code', 'opus'); + expect(viaFamily.rateModel).toEqual(expect.any(String)); + expect(resolveModelRates('claude-code', viaFamily.rateModel)).toMatchObject({ + rateModel: viaFamily.rateModel, + inputPer1M: viaFamily.inputPer1M, + outputPer1M: viaFamily.outputPer1M, + matched: 'exact', + }); + // …and every opus id agrees on that label, which is what lets the + // reconciler treat `opus` and `claude-opus-N` as one model. + expect(resolveModelRates('claude-code', 'claude-opus-9').rateModel).toBe(viaFamily.rateModel); + }); + }); + it('resolves fable/mythos to the Claude 5 flagship rates', () => { expect(resolveModelRates('claude-code', 'claude-fable-5')).toMatchObject({ inputPer1M: 10, outputPer1M: 50 }); expect(resolveModelRates('claude-code', 'fable')).toMatchObject({ rateModel: 'claude-fable-5', matched: 'family' }); From e790d697ab63e3fe3044cc5f2410091dc390c032 Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Sat, 15 Aug 2026 04:27:57 +0000 Subject: [PATCH 2/2] address review (antigravity): clone the tier rate pair per row and tighten the family-label assertion Sharing one OPUS_TIER_RATES array across all five keys meant a future edit to a single opus row would silently rewrite the whole tier; each row now owns its pair, matching the hand-written rows around it. The family-label test asserted only expect.any(String); it now shape-matches /^claude-opus-\d/ rather than hardcoding the current head id, which would have reintroduced the per-bump edit this change removes. --- server/lib/modelPricing.js | 5 ++++- server/lib/modelPricing.test.js | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/lib/modelPricing.js b/server/lib/modelPricing.js index 500f2c500b..ff766f24f8 100644 --- a/server/lib/modelPricing.js +++ b/server/lib/modelPricing.js @@ -53,7 +53,10 @@ const EXACT_RATES = { // Anthropic 'claude-fable-5': [10.0, 50.0], 'claude-mythos-5': [10.0, 50.0], - ...Object.fromEntries(OPUS_MODEL_IDS.map((id) => [id, OPUS_TIER_RATES])), + // Cloned per row so each key owns its pair exactly as the hand-written rows + // above and below do — sharing one array across five keys would make any + // future edit to one opus row silently rewrite the whole tier. + ...Object.fromEntries(OPUS_MODEL_IDS.map((id) => [id, [...OPUS_TIER_RATES]])), 'claude-sonnet-5': [2.0, 10.0], // intro pricing through 2026-08-31 ($3/$15 after) 'claude-sonnet-4-6': [3.0, 15.0], 'claude-sonnet-4-5': [3.0, 15.0], diff --git a/server/lib/modelPricing.test.js b/server/lib/modelPricing.test.js index 0acb158089..9449aeb8a1 100644 --- a/server/lib/modelPricing.test.js +++ b/server/lib/modelPricing.test.js @@ -55,7 +55,11 @@ describe('resolveModelRates', () => { // a null or synthetic label would silently break both. it('reports a family label that is itself an exact table id at the same rates', () => { const viaFamily = resolveModelRates('claude-code', 'opus'); - expect(viaFamily.rateModel).toEqual(expect.any(String)); + // Shape-matched rather than hardcoded to the current head id, so the next + // opus bump doesn't have to edit this suite — that per-bump edit is what + // the refactor removed. The exact-resolution check below is what actually + // pins the pointer to a real table key. + expect(viaFamily.rateModel).toMatch(/^claude-opus-\d/); expect(resolveModelRates('claude-code', viaFamily.rateModel)).toMatchObject({ rateModel: viaFamily.rateModel, inputPer1M: viaFamily.inputPer1M,