Both benchmarks price Claude runs from a table keyed by exact model id (CLAUDE_PRICING_PER_1M[model] in bench-browser/src/usage.ts and bench-github/src/usage.ts). The table lists a handful of ids — claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5-20251001, plus the bare sonnet/opus/haiku aliases.
Any Claude model that isn't literally one of those keys gets no entry, and the lookup returns undefined:
function getClaudePricing(model: string): ModelPricing | undefined {
const entry = CLAUDE_PRICING_PER_1M[model];
if (!entry) return undefined;
...
}
The caller then just skips costing entirely:
let totalCost = reportedCost;
if (!totalCost && inputTokens > 0) {
const pricing = opts.model ? getClaudePricing(opts.model) : undefined;
if (pricing) {
totalCost = /* ... */;
}
}
So the run is reported at $0.00 rather than erroring or warning. Token counts are still correct — only the cost column is wrong, which makes it look plausible rather than broken.
Models that hit this today include claude-opus-4-5, any new point release, and vendor-prefixed ids such as us.anthropic.claude-sonnet-4-5-v1:0. Nothing stops you using them: --model is taken as args.model ?? DEFAULT_MODEL with no allowlist and passed straight through to the agent CLI, so the run itself succeeds normally.
Why it's silent
The local pricing table is only consulted when the Claude JSONL has no result event. When a result event is present, total_cost_usd from Claude is used and the table is bypassed entirely — so on the common path the bug is invisible regardless of which model you pass.
The table is the sole cost source exactly when the run has no result event: the agent crashed, hit a timeout, was killed, or otherwise ended early. Those are the runs where you most want the numbers, and they're the ones that silently read $0.
Reproduction
- Take a Claude JSONL transcript with assistant-message usage but no
result event (i.e. a crashed/interrupted run).
- Call
parseClaudeJsonl(raw, { model: "claude-opus-4-5" }).
total_cost_usd comes back 0, while input_tokens / output_tokens are populated.
Passing claude-sonnet-4-6 instead gives a correct non-zero cost, which is what makes the failure mode easy to miss.
Why it matters
Cost is one of the headline columns the benchmarks exist to produce. A silent $0 doesn't look like an error — it looks like a cheap run, and it will happily average into published results, making a model appear free next to the ones that happen to be in the table. Since the benchmarks accept any model, this hits anyone benchmarking outside the default set.
Anthropic prices per family tier rather than per point release, so resolving the model id onto its family (opus / sonnet / haiku) and pricing from that would cover aliases, dated and undated ids, future point releases and vendor-prefixed ids, and would need no code change on each new release. Failing loudly on a genuinely unknown model would also beat costing it at $0.
Happy to open a PR if that approach sounds right.
Both benchmarks price Claude runs from a table keyed by exact model id (
CLAUDE_PRICING_PER_1M[model]inbench-browser/src/usage.tsandbench-github/src/usage.ts). The table lists a handful of ids —claude-sonnet-4-6,claude-opus-4-6,claude-haiku-4-5-20251001, plus the baresonnet/opus/haikualiases.Any Claude model that isn't literally one of those keys gets no entry, and the lookup returns
undefined:The caller then just skips costing entirely:
So the run is reported at $0.00 rather than erroring or warning. Token counts are still correct — only the cost column is wrong, which makes it look plausible rather than broken.
Models that hit this today include
claude-opus-4-5, any new point release, and vendor-prefixed ids such asus.anthropic.claude-sonnet-4-5-v1:0. Nothing stops you using them:--modelis taken asargs.model ?? DEFAULT_MODELwith no allowlist and passed straight through to the agent CLI, so the run itself succeeds normally.Why it's silent
The local pricing table is only consulted when the Claude JSONL has no
resultevent. When aresultevent is present,total_cost_usdfrom Claude is used and the table is bypassed entirely — so on the common path the bug is invisible regardless of which model you pass.The table is the sole cost source exactly when the run has no
resultevent: the agent crashed, hit a timeout, was killed, or otherwise ended early. Those are the runs where you most want the numbers, and they're the ones that silently read $0.Reproduction
resultevent (i.e. a crashed/interrupted run).parseClaudeJsonl(raw, { model: "claude-opus-4-5" }).total_cost_usdcomes back0, whileinput_tokens/output_tokensare populated.Passing
claude-sonnet-4-6instead gives a correct non-zero cost, which is what makes the failure mode easy to miss.Why it matters
Cost is one of the headline columns the benchmarks exist to produce. A silent $0 doesn't look like an error — it looks like a cheap run, and it will happily average into published results, making a model appear free next to the ones that happen to be in the table. Since the benchmarks accept any model, this hits anyone benchmarking outside the default set.
Anthropic prices per family tier rather than per point release, so resolving the model id onto its family (opus / sonnet / haiku) and pricing from that would cover aliases, dated and undated ids, future point releases and vendor-prefixed ids, and would need no code change on each new release. Failing loudly on a genuinely unknown model would also beat costing it at $0.
Happy to open a PR if that approach sounds right.