Skip to content

Fix rate-card pricing and add 5m/1h cache-write split - #11

Open
krsnaa wants to merge 5 commits into
ksred:mainfrom
krsnaa:fix/rate-card-pricing
Open

Fix rate-card pricing and add 5m/1h cache-write split#11
krsnaa wants to merge 5 commits into
ksred:mainfrom
krsnaa:fix/rate-card-pricing

Conversation

@krsnaa

@krsnaa krsnaa commented May 3, 2026

Copy link
Copy Markdown

Summary

The bundled rate card had two material errors that distorted displayed costs:

  1. All Opus 4.x models were priced at the original Opus 4 / 4.1 tier ($15 input / $75 output). Opus 4.5+ dropped 3x to $5 / $25 — anyone using Opus 4.5 / 4.6 / 4.7 was seeing displayed costs ~3x the real billed amount.
  2. Haiku 4.5 was priced as Haiku 3.5 ($0.80 / $4 instead of $1 / $5), undercharging by 25%.

The pricing matcher also collapsed every Opus 4.x into a single claude-opus-4 prefix, so version-specific repricing was structurally impossible to express. And cctrack treated all cache writes as 5-minute writes, undercharging 1-hour writes by ~60% on those tokens specifically.

This PR splits into five logical commits, each independently buildable and testable:

Commit What
ce4f34f Split cache-write tokens into 5m and 1h tiers (refactor; no price changes)
2c45173 Correct model pricing against Anthropic's published rates
3f09811 Surface bundled rate-card version on the dashboard
c0d3539 Add explicit Sonnet 4.5 and 4.6 rate-card entries
e803520 Add release date to each rate-card entry

Source for all rates: https://platform.claude.com/docs/en/about-claude/pricing

Net effect on displayed costs

  • Opus 4.5 / 4.6 / 4.7 traffic → ~3x lower (matches actual billing)
  • Haiku 4.5 traffic → ~25% higher (matches actual billing)
  • Sonnet 4.x → unchanged
  • Sessions with 1h cache writes → small uptick on those tokens specifically

Breaking change

/api/v1/rates now returns {version, updated, rates: [...]} instead of a bare ModelRate[]. The dashboard is the only consumer in this repo and is updated to match.

Schema change (no migration provided)

sessions and requests tables gain *_cache_write_5m / *_cache_write_1h columns; the old combined cache_write column is dropped (replaced with a derived sum at query time). Existing .cctrack/ databases need to be deleted — the issue surfaced in conversation and the maintainer opted to nuke + reparse rather than carry a migration.

Test plan

  • go vet ./... clean
  • go test ./internal/calculator/... — 18 pricing assertions pass (Opus 4 through 4.7, Sonnet 4 / 4.5 / 4.6, Haiku 3.5 / 4.5, 5m vs 1h cache writes, fallback path)
  • bun run build clean
  • Each commit independently passes vet + test + build
  • Reviewer: delete local .cctrack/ and re-parse to verify dashboard numbers reconcile with Anthropic's billing console

Forward-looking

RatesVersion / RatesUpdated constants live alongside the rate data, so anyone editing prices touches the same file as the label and they can't drift. The "last updated" date and vN.N label are now visible on the Rate Card page header, giving a passive signal when a binary is on stale pricing.

krsnaa added 5 commits May 3, 2026 00:53
Anthropic charges 5-minute and 1-hour cache writes at different rates
(1.25x and 2x base input respectively), and the JSONL usage object
already exposes the breakdown via cache_creation.ephemeral_5m_input_tokens
and ephemeral_1h_input_tokens. cctrack collapsed both into a single
cache_write bucket and applied the 5m rate, undercharging 1h writes.

Plumb the split end-to-end:
- ModelRates / TokenUsage / CostBreakdown carry separate 5m and 1h fields
- Parser reads the breakdown when present, falls back to all-5m for
  older log lines that lack it
- Schema gains total_cache_write_5m / total_cache_write_1h on sessions
  and matching columns on requests; combined total_cache_write is now
  derived at read-time so SQL stays the single source of truth
- GetCostBreakdown feeds both columns into Calculate so the per-model
  cost-breakdown chart no longer silently degrades to "all 5m"
- Rate Card view shows 5m and 1h columns
The bundled rate card had two material errors:

1. All Opus 4.x models were priced at the original Opus 4 / 4.1 tier
   ($15 input / $75 output), but Opus 4.5+ dropped 3x to $5 / $25.
   Anyone running Opus 4.5 / 4.6 / 4.7 was seeing displayed costs
   ~3x the real billed amount.

2. Haiku 4.5 was priced as Haiku 3.5 ($0.80 / $4 instead of $1 / $5),
   undercharging by 25%.

The single "claude-opus-4" prefix also matched every Opus 4.x model
indiscriminately. Replaced with version-specific entries ordered most-
specific-first so 4.5/4.6/4.7 and 4.1 each pick up their own rates.

Also added entries for Haiku 3.5, Opus 3, Sonnet 3.7, and Haiku 3 so
older log lines aren't silently misattributed to the Sonnet fallback.
The fallback itself now lives outside the prefix-match table to avoid
double-matching.

calculator_test.go bills 1M tokens of one kind per case so a failing
assertion points directly at the rate that moved.

Source: https://platform.claude.com/docs/en/about-claude/pricing
Without a visible version it's hard to tell whether a binary is on
stale pricing — particularly after Anthropic ships a new model or
adjusts a rate. Add RatesVersion / RatesUpdated constants alongside
the rate data so anyone editing prices touches the same file as the
label, and surface them via /api/v1/rates so the Rate Card page can
show "v1.1 · updated 2026-05-03" in the page header.

API shape change: /api/v1/rates now returns
  { version, updated, rates: [...] }
instead of a bare ModelRate[]. cctrack's only consumer is its own
dashboard so there's no compat baggage to preserve.
Sonnet 4.5 and 4.6 currently match the blanket "claude-sonnet-4"
prefix and pick up the right price ($3 / $15 / $0.30 / $3.75 / $6),
so this is not a billing fix today. But the implicit coverage means:

  1. The Rate Card UI shows one row labelled "claude-sonnet-4",
     which doesn't tell the user that 4.5 and 4.6 are recognized.
  2. If Anthropic ships a Sonnet variant with different pricing
     (as they did with Opus 4.5+), the same blanket prefix that's
     correct today becomes a silent misattribution.

Listing 4.6 / 4.5 / 4 explicitly mirrors the Opus split, makes the
Rate Card actually verifiable, and turns any future divergence into
a one-line edit. Bumps RatesVersion to v1.2 per the docstring rule
of "bump on rate change or model addition."
Surface when each model became available so users can correlate
displayed costs with their own usage timeline (e.g. "I started using
Opus 4.7 the week it shipped"). Adds a Released field to ModelRates,
populates all 13 entries with confirmed dates, and renders a Released
column on the Rate Card view.

Dates are model-release dates, not pricing-effective dates — they
usually coincide for new models, and Anthropic doesn't publish a
formal pricing changelog to source effective-dates from. Ground truth
for several modern models comes from the date suffix in their model
IDs (e.g. claude-opus-4-1-20250805 → 2025-08-05).

Bumps RatesVersion to v1.3 per the docstring rule.

@ksred ksred left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the more complete pricing fix (vs the simpler #12 which I just merged).

What's good

  • Correct Opus 4.5+ rates ($5/$25), Haiku 4.5 ($1/$5), explicit per-version prefixes
  • Proper 5m vs 1h cache-write split matching Anthropic's published multipliers
  • Versioned rate card + dashboard surface for stale builds
  • Solid test coverage on the calculator

Caveats

  1. #12 already merged — it applied the basic rate corrections on rates.go without the schema split. This PR will conflict on that file and needs a rebase on current main.
  2. Breaking schema — dropping cache_write in favour of *_5m / *_1h with no migration means users must delete .cctrack/ and reparse. That's acceptable if called out clearly in the release notes / README, but please make the "nuke and reparse" step explicit in the PR description and preferably a one-line note in the app on first run after upgrade.
  3. API shape change/api/v1/rates going from bare array → {version, updated, rates} is fine (dashboard is the only in-repo consumer), just confirm no external tools scrape the old shape.

Recommendation: rebase onto main (post-#12), keep the 5m/1h work + versioned rate card, and re-verify the calculator tests still pass against the rates already updated by #12. Once rebased and green, this is the right long-term fix and I'm happy to merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants