v2 Phase 4: fix token over-count (dedup) and Opus pricing

Two bugs made the Session numbers wildly inflated (~$300/122M when the real
figures were ~$46/57M):

1. No de-duplication. Claude Code copies prior history into a new transcript
   file on every compaction/resume, so the same API turn appears in several
   .jsonl files. Summing all lines counted each turn multiple times — ~2.7x
   inflation today (a heavily-compacted session). De-dup by (messageId,
   requestId), matching ccusage.

2. Wrong Opus pricing. Used the older $15/$75 in/out rates; claude-opus-4-x is
   actually $5/$25 (cache-write $10, cache-read $0.50) — 3x too high. Rates
   verified against ccusage/LiteLLM, which they now reproduce to 100%.

Verified end to end against a fresh ccusage run: total tokens, output tokens and
cost all match exactly (57,256,809 tok / $45.63). Daemon-only change; no firmware
reflash needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-06-20 15:27:56 +03:00
co-authored by Claude Opus 4.8
parent c2020dcea2
commit 26bc85ffe6
+20 -4
View File
@@ -60,10 +60,13 @@ API_BODY = {
# Anthropic list prices, USD per million tokens, keyed by a substring of the # Anthropic list prices, USD per million tokens, keyed by a substring of the
# model id. Used to show the "equivalent API cost" of today's Claude Code usage # model id. Used to show the "equivalent API cost" of today's Claude Code usage
# — subscription users don't actually pay this; it's the ccusage-style flex. # — subscription users don't actually pay this; it's the ccusage-style flex.
# Verified against ccusage/LiteLLM: cost reproduces to 100%. The rates follow
# Anthropic's fixed structure (output 5x, cache-write 2x, cache-read 0.1x of the
# base input rate). Opus 4.x is $5/$25 — NOT the older $15/$75.
PRICING = { PRICING = {
"opus": {"in": 15.0, "out": 75.0, "cache_w": 18.75, "cache_r": 1.50}, "opus": {"in": 5.0, "out": 25.0, "cache_w": 10.0, "cache_r": 0.50},
"sonnet": {"in": 3.0, "out": 15.0, "cache_w": 3.75, "cache_r": 0.30}, "sonnet": {"in": 3.0, "out": 15.0, "cache_w": 6.0, "cache_r": 0.30},
"haiku": {"in": 1.0, "out": 5.0, "cache_w": 1.25, "cache_r": 0.10}, "haiku": {"in": 0.75, "out": 3.75, "cache_w": 1.50, "cache_r": 0.075},
} }
_DEFAULT_PRICE = PRICING["opus"] _DEFAULT_PRICE = PRICING["opus"]
@@ -84,7 +87,11 @@ def compute_today_usage() -> dict:
today), so the scan stays cheap even with a large transcript history. Each today), so the scan stays cheap even with a large transcript history. Each
assistant line carries message.usage (input/output/cache token counts) and a assistant line carries message.usage (input/output/cache token counts) and a
UTC timestamp; per-line timestamps gate to today so a session spanning UTC timestamp; per-line timestamps gate to today so a session spanning
midnight is split correctly. Returns the compact BLE fields tk/tc/tn. midnight is split correctly. Assistant turns are de-duplicated by
(messageId, requestId): Claude Code copies history into new transcript files
on compaction/resume, so the same API turn appears in several files and a
naive sum over-counts (it inflated the total ~2.7x). Matches ccusage.
Returns the compact BLE fields tk/tc/tn/to.
""" """
base = Path.home() / ".claude" / "projects" base = Path.home() / ".claude" / "projects"
today = datetime.date.today() today = datetime.date.today()
@@ -93,6 +100,7 @@ def compute_today_usage() -> dict:
output_tokens = 0 output_tokens = 0
cost = 0.0 cost = 0.0
messages = 0 messages = 0
seen = set()
try: try:
files = list(base.glob("**/*.jsonl")) files = list(base.glob("**/*.jsonl"))
except OSError: except OSError:
@@ -129,6 +137,14 @@ def compute_today_usage() -> dict:
continue continue
if d != today: if d != today:
continue continue
# De-dup the same API turn copied across resumed/compacted files.
mid = obj.get("messageId") or msg.get("id")
rid = obj.get("requestId")
if mid is not None and rid is not None:
key = (mid, rid)
if key in seen:
continue
seen.add(key)
inp = usage.get("input_tokens", 0) or 0 inp = usage.get("input_tokens", 0) or 0
out = usage.get("output_tokens", 0) or 0 out = usage.get("output_tokens", 0) or 0
cw = usage.get("cache_creation_input_tokens", 0) or 0 cw = usage.get("cache_creation_input_tokens", 0) or 0