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
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/11645-list-price-ts-prices-any-model-whose-nam → main 2 commits fork 2 threads · 2 unresolved
2 files
changed +49
−7
src/spawn/list_price.ts +16 −7
| @@ -1,11 +1,14 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * 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. | |
| 9 | 12 | */ |
| 10 | 13 | |
| 11 | 14 | export interface Usage { |
| @@ -22,14 +25,20 @@ interface Rate { | ||
| 22 | 25 | readonly out: number; |
| 23 | 26 | } |
| 24 | 27 | |
| 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 | |
| 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. |
||
| 25 | 33 | const FAMILIES: readonly (readonly [RegExp, Rate])[] = [ |
| 26 | 34 | [/haiku/i, { in: 1, out: 5 }], |
| 27 | 35 | [/sonnet/i, { in: 3, out: 15 }], |
| 36 | [OPUS_45_UP, { in: 5, out: 25 }], | |
| 28 | 37 | [/opus/i, { in: 15, out: 75 }], |
| 29 | 38 | ]; |
| 30 | 39 | const DEAREST: Rate = { in: 15, out: 75 }; |
| 31 | 40 | |
| 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; | |
| 33 | 42 | |
| 34 | 43 | /** Dollars at list price: tokens / 1e6 * USD per million, summed. Unrounded. */ |
| 35 | 44 | export function listPriceUsd(model: string, u: Usage): number { |
test/child-credit.test.ts +33
| @@ -59,6 +59,39 @@ describe("list price", () => { | ||
| 59 | 59 | }); |
| 60 | 60 | }); |
| 61 | 61 | |
| 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 | ||
| 62 | 95 | describe("a parent funds a child's crew", () => { |
| 63 | 96 | let dir = ""; |
| 64 | 97 | let parent: App; |
new above
offline
Fable at 15/75 is the fallback rate under another name; cite a real rate or remove this line.