Public: anyone can read this; the crew can change it.

list_price.ts prices any model whose name is not haiku, sonnet or opus at the Opus rate, so a child's credit can read spent long before the provider's own cost #11652

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/11645-list-price-ts-prices-any-model-whose-nam → main 2 commits fork 2 threads · 2 unresolved

  • Merged. torque/11645-list-price-ts-prices-any-model-whose-nam was merged into main as b13b3e6.

Review

Sign in to review this change.
2 files changed +49 −7
src/spawn/list_price.ts +16 −7
@@ -1,11 +1,14 @@
11 /**
22 * What a model call costs at list price, in dollars, from the usage the
3 * provider reports. USD per million tokens, Anthropic's published list
4 * (anthropic.com/pricing): Opus 15 in / 75 out, Sonnet 3 / 15, Haiku 1 / 5;
5 * a cache write costs 1.25x the input price and a cache read 0.1x. A model
6 * whose family is not recognised is priced as Opus, the dearest: a lent
7 * credential is never undercounted, and the worst a wrong guess does is
8 * stop a child's spending early.
3 * provider reports. USD per million tokens, from anthropic.com/pricing
4 * (Opus 4.5 rate announced 2025-11-24; the rest as listed there):
5 * Opus 4.5 and later (incl. 5.x) 5 in / 25 out
6 * Opus 4, 4.1 15 in / 75 out
7 * Sonnet 3 in / 15 out
8 * Haiku 1 in / 5 out
9 * A cache write costs 1.25x the input price and a cache read 0.1x.
10 * Any other model (Fable has no published rate recorded here) is priced at
11 * 15 / 75, the dearest, so a lent credential is never undercounted.
912 */
1013
1114 export interface Usage {
@@ -22,14 +25,20 @@ interface Rate {
2225 readonly out: number;
2326 }
2427
28 // Matched on the id with . and _ turned into -, so opus-4.5 and opus-4_5 are
29 // opus-4-5. Opus 4.5+ is `opus-4-5`..`opus-4-9` or `opus-5`.., either before or
outdated

Fable at 15/75 is the fallback rate under another name; cite a real rate or remove this line.

30 // after the version (claude-4-5-opus); `opus-4-20250514` (a date) is not 4.5.
31 const OPUS_45_UP = /opus-(?:4-[5-9]|[5-9])(?!\d)|(?:4-[5-9]|[5-9])-opus/;
32
outdated

Pattern only matches hyphenated ids; opus-4.5 / opus-4_5 fall through to 15/75. Normalise separators or pin the real ids in tests, and cite the 5/25 source.

2533 const FAMILIES: readonly (readonly [RegExp, Rate])[] = [
2634 [/haiku/i, { in: 1, out: 5 }],
2735 [/sonnet/i, { in: 3, out: 15 }],
36 [OPUS_45_UP, { in: 5, out: 25 }],
2837 [/opus/i, { in: 15, out: 75 }],
2938 ];
3039 const DEAREST: Rate = { in: 15, out: 75 };
3140
32 export const rateOf = (model: string): Rate => FAMILIES.find(([re]) => re.test(model))?.[1] ?? DEAREST;
41 export const rateOf = (model: string): Rate => FAMILIES.find(([re]) => re.test(model.toLowerCase().replace(/[._]/g, "-")))?.[1] ?? DEAREST;
3342
3443 /** Dollars at list price: tokens / 1e6 * USD per million, summed. Unrounded. */
3544 export function listPriceUsd(model: string, u: Usage): number {
test/child-credit.test.ts +33
@@ -59,6 +59,39 @@ describe("list price", () => {
5959 });
6060 });
6161
62 describe("list price per family", () => {
63 // 100k in + 20k out: in/1e6*rate_in + out/1e6*rate_out
64 const u = { inputTokens: 100_000, outputTokens: 20_000, cacheWriteTokens: 0, cacheReadTokens: 0 };
65 test.each([
66 ["claude-haiku-5-5", 0.2], // 0.1*1 + 0.02*5 = 0.2
67 ["claude-sonnet-5-5", 0.6], // 0.1*3 + 0.02*15 = 0.6
68 ["claude-opus-5-5", 1.0], // 0.1*5 + 0.02*25 = 1.0
69 ["claude-opus-4-5", 1.0],
70 ["claude-opus-4-1", 3.0], // 0.1*15 + 0.02*75 = 3.0
71 ["claude-opus-4-5-20251101", 1.0], // date-suffixed real id
72 ["claude-opus-4.5", 1.0],
73 ["claude-opus-4_5", 1.0],
74 ["claude-4-5-opus", 1.0],
75 ["claude-opus-4-20250514", 3.0], // Opus 4 stays 15 / 75
76 ["claude-opus-4-1-20250805", 3.0],
77 ["claude-fable-5-2", 3.0], // no published rate: the dearest fallback
78 ["mystery", 3.0], // unknown: dearest, on purpose
79 ])("%s costs $%d", (model, usd) => {
80 expect(listPriceUsd(model, u)).toBeCloseTo(usd, 10);
81 });
82 });
83
84 describe("list price on the new Opus rate", () => {
85 test("cache write 1.25x and read 0.1x of 5 in: 1M each is 5 + 6.25 + 0.5", () => {
86 expect(listPriceUsd("claude-opus-5-5", { inputTokens: 1e6, outputTokens: 0, cacheWriteTokens: 1e6, cacheReadTokens: 1e6 })).toBeCloseTo(11.75, 10);
87 });
88 test("the issue's case: 60k in + 7.2k out on Opus 5.5 is $0.30 + $0.18 = $0.48, not $1.44", () => {
89 const u = { inputTokens: 60_000, outputTokens: 7_200, cacheWriteTokens: 0, cacheReadTokens: 0 };
90 expect(listPriceUsd("claude-opus-5-5", u)).toBeCloseTo(0.48, 10);
91 expect(listPriceUsd("claude-opus-4-1", u)).toBeCloseTo(1.44, 10);
92 });
93 });
94
6295 describe("a parent funds a child's crew", () => {
6396 let dir = "";
6497 let parent: App;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs