Skip to content

Claude models missing from the pricing table are silently costed at $0 #92

Description

@TLXyloph

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

  1. Take a Claude JSONL transcript with assistant-message usage but no result event (i.e. a crashed/interrupted run).
  2. Call parseClaudeJsonl(raw, { model: "claude-opus-4-5" }).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions