// Cost of a run, and what the same work would have cost on Opus. Pure: no `$`. // // Prices: USD per million tokens, from platform.claude.com/docs/en/about-claude/pricing // (read 2026-10-08). A setting can replace them. export type Usage = { input_tokens: number output_tokens: number cache_creation_input_tokens?: number | null cache_read_input_tokens?: number | null } /** in, out, cache write (5 min), cache read: USD per MTok. */ export type Price = { in: number; out: number; write: number; read: number } export type PriceRow = { match: string; price: Price; label: string } // First match wins: more specific ids come first. export const DEFAULT_PRICES: readonly PriceRow[] = [ { match: 'fable-5-1', label: 'Fable 5.1', price: { in: 10, out: 50, write: 12.5, read: 0.25 } }, { match: 'mythos-5-1', label: 'Mythos 5.1', price: { in: 10, out: 50, write: 12.5, read: 0.25 } }, { match: 'opus-5-5', label: 'Opus 5.5', price: { in: 4, out: 20, write: 5, read: 0.2 } }, { match: 'sonnet-5-5', label: 'Sonnet 5.5', price: { in: 2, out: 10, write: 2.5, read: 0.1 } }, // Haiku 5.5 doubles past 100k-token prompts; a run's summed usage cannot tell, so the low tier is used. { match: 'haiku-5-5', label: 'Haiku 5.5', price: { in: 0.1, out: 0.5, write: 0.125, read: 0.01 } }, { match: 'haiku-4-5', label: 'Haiku 4.5', price: { in: 1, out: 5, write: 1.25, read: 0.1 } }, { match: 'fable-5', label: 'Fable 5', price: { in: 10, out: 50, write: 12.5, read: 1 } }, { match: 'opus-5', label: 'Opus 5', price: { in: 5, out: 25, write: 6.25, read: 0.5 } }, { match: 'sonnet-5', label: 'Sonnet 5', price: { in: 2, out: 10, write: 2.5, read: 0.2 } }, ] /** The model every saving is measured against. */ export const BASELINE = 'opus-5-5' /** * Models before Claude 4.7 use a tokenizer that yields ~30% fewer tokens for * the same text, so their counts are scaled up before pricing them as Opus 5.5. */ const OLD_TOKENIZER = /(haiku-4-5|sonnet-4|opus-4-[0-6]|opus-4-1|opus-4(?!-)|haiku-3)/ export const tokenizerFactor = (modelId: string) => (OLD_TOKENIZER.test(modelId) ? 1.3 : 1) export const priceRow = (modelId: string, table: readonly PriceRow[] = DEFAULT_PRICES) => table.find(r => modelId.toLowerCase().includes(r.match)) /** USD for a usage at a price; tokens scaled by `factor`. */ export const cost = (u: Usage, p: Price, factor = 1) => (factor * (u.input_tokens * p.in + u.output_tokens * p.out + (u.cache_creation_input_tokens ?? 0) * p.write + (u.cache_read_input_tokens ?? 0) * p.read)) / 1_000_000 export type Priced = { actual: number; opus: number; known: boolean; label: string } /** What a run cost on its own model, and the same tokens on the baseline. */ export const priceRun = (modelId: string, u: Usage, table: readonly PriceRow[] = DEFAULT_PRICES): Priced => { const row = priceRow(modelId, table) const base = priceRow(BASELINE, table) if (!row || !base) return { actual: 0, opus: 0, known: false, label: modelId } return { actual: cost(u, row.price), opus: cost(u, base.price, tokenizerFactor(modelId)), known: true, label: row.label, } } /** A task's running totals, across every agent and round it took. */ export type TaskCost = { task: string /** Writer runs on each model, USD. */ writerActual: number /** Writer runs below Opus, priced at Opus: what Opus would have cost for the same tokens. */ writerAsOpus: number /** Writer runs that were on Opus already, USD. */ writerOnOpus: number /** Classifier and verifier calls the router added, USD. */ overhead: number models: string[] unpriced: string[] } export const emptyTask = (task: string): TaskCost => ({ task, writerActual: 0, writerAsOpus: 0, writerOnOpus: 0, overhead: 0, models: [], unpriced: [], }) export const addWriterRun = (t: TaskCost, modelId: string, u: Usage, table?: readonly PriceRow[]): TaskCost => { const p = priceRun(modelId, u, table) const next = { ...t, models: [...t.models], unpriced: [...t.unpriced] } if (!p.known) { if (!next.unpriced.includes(modelId)) next.unpriced.push(modelId) return next } if (!next.models.includes(p.label)) next.models.push(p.label) next.writerActual += p.actual if (modelId.toLowerCase().includes(BASELINE)) next.writerOnOpus += p.actual else next.writerAsOpus += p.opus return next } export const addOverhead = (t: TaskCost, modelId: string, u: Usage, table?: readonly PriceRow[]): TaskCost => { const p = priceRun(modelId, u, table) return p.known ? { ...t, overhead: t.overhead + p.actual } : t } /** * What the task would have cost had it gone to Opus at once. * * When Opus wrote part of it (a rewrite after failed rounds, or an open * design), Opus's own runs are taken as what Opus alone would have needed: * the failed cheaper rounds then count as pure loss. Otherwise every writer * token is priced at Opus, an upper bound, since Opus may need fewer tokens. */ export const counterfactual = (t: TaskCost) => (t.writerOnOpus > 0 ? t.writerOnOpus : t.writerAsOpus) export const actualTotal = (t: TaskCost) => t.writerActual + t.overhead export const saved = (t: TaskCost) => counterfactual(t) - actualTotal(t) export const savedPct = (t: TaskCost) => { const c = counterfactual(t) return c > 0 ? (saved(t) / c) * 100 : 0 } /** Reads a prices setting: a JSON list of rows, or empty for the defaults. */ export const parsePrices = (json: string): readonly PriceRow[] => { if (!json.trim()) return DEFAULT_PRICES try { const rows = JSON.parse(json) as PriceRow[] const ok = Array.isArray(rows) && rows.every(r => typeof r.match === 'string' && typeof r.price?.in === 'number') return ok ? [...rows.map(r => ({ ...r, label: r.label ?? r.match })), ...DEFAULT_PRICES] : DEFAULT_PRICES } catch { return DEFAULT_PRICES } } export const usd = (n: number) => (Math.abs(n) < 0.01 && n !== 0 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`) /** * The savings table: per task, what it cost, what Opus would have cost, and * the difference, newest first; then the total over every task. */ export const savingsReport = (costs: Readonly>, limit: number, only?: string): string => { const ids = Object.keys(costs) .filter(id => !only || id === only) .sort((a, b) => Number(b.slice(1)) - Number(a.slice(1))) if (ids.length === 0) return only ? `model-router: no costs recorded for ${only}.` : 'savings: no agent runs recorded yet.' const row = (id: string, t: TaskCost) => { const pct = counterfactual(t) > 0 ? `${savedPct(t).toFixed(0)}%` : '-' const how = t.writerActual === 0 ? 'running' : t.writerOnOpus > 0 ? 'opus ran' : 'upper bound' return `${id.padEnd(5)} ${usd(actualTotal(t)).padStart(9)} ${usd(counterfactual(t)).padStart(9)} ${usd(saved(t)).padStart(9)} ${pct.padStart(5)} ${how.padEnd(11)} ${t.models.join('+').padEnd(20)} ${t.task.slice(0, 32)}${t.unpriced.length ? ` [no price: ${t.unpriced.join(', ')}]` : ''}` } let actual = 0 let opus = 0 for (const id of Object.keys(costs)) { const t = costs[id] // a task whose writer has not finished has no comparison yet if (!t || t.writerActual === 0) continue actual += actualTotal(t) opus += counterfactual(t) } const shown = ids.slice(0, limit) const lines = [ `savings vs Opus 5.5 (USD, API list prices)${only ? '' : ` - ${shown.length} of ${ids.length} tasks`}`, `task actual if opus saved % basis models task`, ...shown.map(id => row(id, costs[id] as TaskCost)), ] if (!only) lines.push( `total ${usd(actual).padStart(9)} ${usd(opus).padStart(9)} ${usd(opus - actual).padStart(9)} ${(opus > 0 ? `${(((opus - actual) / opus) * 100).toFixed(0)}%` : '-').padStart(5)}`, ) lines.push( 'actual = writer runs + classifier + verifier. "upper bound": all tokens priced at Opus; Opus may need fewer. "opus ran": the Opus run stands for Opus alone, cheaper failed rounds count as loss.', ) return lines.join('\n') }