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
180 lines
7.6 KiB
TypeScript
180 lines
7.6 KiB
TypeScript
import type { On } from 'claude-code'
|
|
import { expect, mock, test, type Engine } from 'claude-code/testing'
|
|
|
|
import {
|
|
addOverhead,
|
|
addWriterRun,
|
|
actualTotal,
|
|
counterfactual,
|
|
emptyTask,
|
|
parsePrices,
|
|
priceRun,
|
|
saved,
|
|
savingsReport,
|
|
tokenizerFactor,
|
|
} from '../hooks/pricing'
|
|
|
|
const U = (i: number, o: number, w = 0, r = 0) => ({
|
|
input_tokens: i,
|
|
output_tokens: o,
|
|
cache_creation_input_tokens: w,
|
|
cache_read_input_tokens: r,
|
|
})
|
|
const M = 1_000_000
|
|
const round = (n: number) => Math.round(n * 1e6) / 1e6
|
|
|
|
test('prices: each part of the usage at its own rate', async () => {
|
|
// Sonnet 5.5: in 2, out 10, write 2.5, read 0.1 per MTok
|
|
expect(priceRun('claude-sonnet-5-5', U(M, M, M, M)).actual).toBe(2 + 10 + 2.5 + 0.1)
|
|
// the same tokens at Opus 5.5: 4 + 20 + 5 + 0.2
|
|
expect(round(priceRun('claude-sonnet-5-5', U(M, M, M, M)).opus)).toBe(29.2)
|
|
expect(priceRun('claude-opus-5-5', U(M, 0)).actual).toBe(4)
|
|
expect(priceRun('claude-haiku-5-5', U(M, 0)).actual).toBe(0.1)
|
|
expect(priceRun('claude-haiku-4-5', U(M, 0)).actual).toBe(1)
|
|
})
|
|
|
|
test('prices: the most specific model wins, unknown models are flagged', async () => {
|
|
expect(priceRun('claude-opus-5-5-20260601', U(M, 0)).label).toBe('Opus 5.5')
|
|
expect(priceRun('claude-opus-5', U(M, 0)).label).toBe('Opus 5')
|
|
expect(priceRun('claude-sonnet-5', U(M, 0)).label).toBe('Sonnet 5')
|
|
expect(priceRun('gpt-9', U(M, 0)).known).toBe(false)
|
|
})
|
|
|
|
test('older tokenizers are scaled before pricing as Opus 5.5', async () => {
|
|
expect(tokenizerFactor('claude-haiku-4-5')).toBe(1.3)
|
|
expect(tokenizerFactor('claude-sonnet-5-5')).toBe(1)
|
|
expect(tokenizerFactor('claude-haiku-5-5')).toBe(1)
|
|
expect(round(priceRun('claude-haiku-4-5', U(M, 0)).opus)).toBe(5.2)
|
|
})
|
|
|
|
test('a task below opus: every writer token at opus price is the comparison', async () => {
|
|
let t = emptyTask('clamp')
|
|
t = addWriterRun(t, 'claude-sonnet-5-5', U(M, 0))
|
|
t = addWriterRun(t, 'claude-sonnet-5-5', U(M, 0)) // resumed round
|
|
t = addOverhead(t, 'claude-opus-5-5', U(M / 2, 0)) // verifier
|
|
expect(t.writerActual).toBe(4)
|
|
expect(counterfactual(t)).toBe(8)
|
|
expect(actualTotal(t)).toBe(6)
|
|
expect(saved(t)).toBe(2)
|
|
})
|
|
|
|
test('a chain that ended on opus: the opus run is the comparison, failed rounds are loss', async () => {
|
|
let t = emptyTask('clamp')
|
|
t = addWriterRun(t, 'claude-sonnet-5-5', U(M, 0)) // failed twice
|
|
t = addWriterRun(t, 'claude-opus-5-5', U(M, 0)) // rewrite
|
|
expect(counterfactual(t)).toBe(4)
|
|
expect(actualTotal(t)).toBe(6)
|
|
expect(saved(t)).toBe(-2)
|
|
})
|
|
|
|
test('unpriced models are listed, never counted as free', async () => {
|
|
const t = addWriterRun(emptyTask('x'), 'mystery-1', U(M, M))
|
|
expect(t.unpriced).toEqual(['mystery-1'])
|
|
expect(t.writerActual).toBe(0)
|
|
expect(savingsReport({ T1: t }, 5)).toContain('[no price: mystery-1]')
|
|
})
|
|
|
|
test('price overrides come first; bad JSON falls back to list prices', async () => {
|
|
const table = parsePrices('[{"match":"sonnet-5-5","price":{"in":1,"out":1,"write":1,"read":1}}]')
|
|
expect(priceRun('claude-sonnet-5-5', U(M, 0), table).actual).toBe(1)
|
|
expect(priceRun('claude-opus-5-5', U(M, 0), table).actual).toBe(4)
|
|
expect(priceRun('claude-sonnet-5-5', U(M, 0), parsePrices('{oops')).actual).toBe(2)
|
|
})
|
|
|
|
test('the report: newest first, totals over every task, filter by task', async () => {
|
|
const a = addWriterRun(emptyTask('first'), 'claude-sonnet-5-5', U(M, 0))
|
|
const b = addWriterRun(emptyTask('second'), 'claude-sonnet-5-5', U(2 * M, 0))
|
|
const r = savingsReport({ T1: a, T2: b }, 1)
|
|
expect(r.indexOf('T2')).toBeGreaterThan(-1)
|
|
expect(r).not.toContain('T1 ')
|
|
expect(r).toMatch(/total\s+\$6\.00\s+\$12\.00\s+\$6\.00\s+50%/)
|
|
expect(savingsReport({ T1: a, T2: b }, 9, 'T1')).toContain('first')
|
|
expect(savingsReport({ T1: a, T2: b }, 9, 'T1')).not.toContain('second')
|
|
})
|
|
|
|
// ---------- end to end ----------
|
|
|
|
const USAGE = { input_tokens: 1000, output_tokens: 10, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 }
|
|
const ok = (stdout: string) => ({ value: { exitCode: 0, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false } })
|
|
const DEV = '/home/homelab/projects/goodwe/clones/OPT-11-P4-dev'
|
|
|
|
function world(on: On, models: string[]) {
|
|
mock.store(on)
|
|
const clock = mock.clock(on)
|
|
on('ui.status', async () => ({ value: undefined }))
|
|
on('ui.toast', async () => ({ value: undefined }))
|
|
on('model.complete', async (_$, e) => {
|
|
if (e.system?.startsWith('You route')) return { value: { isAnswered: true as const, text: 'sonnet', usage: USAGE } }
|
|
return { value: { isAnswered: true as const, text: '{"verdict":"PASS"}', usage: USAGE } }
|
|
})
|
|
on('process.run', async (_$, e) => {
|
|
const a = e.argv.join(' ')
|
|
if (a.startsWith('git merge-base')) return ok('base\n')
|
|
if (a.startsWith('git diff')) return ok('diff')
|
|
return ok('')
|
|
})
|
|
on('agent.spawn', async (_$, e) => {
|
|
models.push(String(e.model))
|
|
return { model: e.model === 'sonnet' ? 'claude-sonnet-5-5' : String(e.model), agentId: `a${models.length}` }
|
|
})
|
|
on('turn.complete', async (_$, e) => ({ text: e.answer }))
|
|
on('prompt.submit', async (_$, e) => ({ text: e.text }))
|
|
return clock
|
|
}
|
|
|
|
async function end($: Engine, agentId: string, model: string, input: number) {
|
|
await ($.turn as unknown as { complete: (e: unknown) => Promise<unknown> }).complete({
|
|
answer: 'done',
|
|
durationMs: 1,
|
|
isAborted: false,
|
|
turnId: 't',
|
|
agentId,
|
|
reason: 'answer',
|
|
usage: { model, input_tokens: input, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
})
|
|
}
|
|
|
|
test('end to end: a routed dev agent is priced on the model it ran on, a kept review agent is not counted', async ($, on) => {
|
|
const models: string[] = []
|
|
const clock = world(on, models)
|
|
const dev = await $.agent.spawn({ prompt: `Clone: ${DEV}/\nExpected: x`, description: 'P4 dev' } as never)
|
|
await end($, dev.agentId as string, 'claude-sonnet-5-5', M)
|
|
await clock.advance(1)
|
|
const review = await $.agent.spawn({ prompt: 'Clone: /x/clones/OPT-11-P4-review/', description: 'P4 review', model: 'opus' } as never)
|
|
await end($, review.agentId as string, 'claude-opus-5-5', M)
|
|
await clock.advance(1)
|
|
|
|
const r = (await $.command.run({ command: 'router', args: 'savings' } as never)).text ?? ''
|
|
expect(r).toContain('P4 dev')
|
|
expect(r).not.toContain('P4 review')
|
|
// writer $2.00 + classifier and verifier (1000 in + 10 out each, Haiku 4.5 then Opus 5.5)
|
|
expect(r).toMatch(/T1\s+\$2\.01\s+\$4\.00/)
|
|
expect(r).toContain('Sonnet 5.5')
|
|
})
|
|
|
|
test('agents started in parallel each get their own task, and every run is counted', async ($, on) => {
|
|
const models: string[] = []
|
|
const clock = world(on, models)
|
|
const ids = (
|
|
await Promise.all(
|
|
['A', 'D', 'E', 'F', 'G'].map(c => $.agent.spawn({ prompt: `Scope chunk ${c}: list the open cards`, description: `Scope chunk ${c}` } as never)),
|
|
)
|
|
).map(r => r.agentId as string)
|
|
await Promise.all(ids.map(id => end($, id, 'claude-sonnet-5-5', M)))
|
|
await clock.advance(1)
|
|
|
|
const r = (await $.command.run({ command: 'router', args: 'savings' } as never)).text ?? ''
|
|
for (const c of ['A', 'D', 'E', 'F', 'G']) expect(r).toContain(`Scope chunk ${c}`)
|
|
expect(r).toContain('5 of 5 tasks')
|
|
// five writer runs of $2.00 each, all counted
|
|
expect(r).toMatch(/total\s+\$10\.\d\d\s+\$20\.00/)
|
|
})
|
|
|
|
test('a task whose writer has not finished shows as running and stays out of the total', async () => {
|
|
const running = addOverhead(emptyTask('MAINT-14 dev'), 'claude-haiku-4-5', U(M, 0)) // $1 of classifier, no writer yet
|
|
const done = addWriterRun(emptyTask('done'), 'claude-sonnet-5-5', U(M, 0))
|
|
const r = savingsReport({ T1: done, T2: running }, 5)
|
|
expect(r).toMatch(/T2 .* running /)
|
|
expect(r).toMatch(/total\s+\$2\.00\s+\$4\.00/)
|
|
})
|