Claude Code plugin: routes subagents by how well-defined the brief is, verifies dev work on a second model in the agent's own clone, and estimates savings against Opus 5.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MdqPWvuDpbpRvDEdURoAwo
186 lines
7.9 KiB
TypeScript
186 lines
7.9 KiB
TypeScript
// 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<Record<string, TaskCost>>, 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')
|
|
}
|