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:
@@ -60,10 +60,13 @@ API_BODY = {
|
||||
# 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
|
||||
# — 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 = {
|
||||
"opus": {"in": 15.0, "out": 75.0, "cache_w": 18.75, "cache_r": 1.50},
|
||||
"sonnet": {"in": 3.0, "out": 15.0, "cache_w": 3.75, "cache_r": 0.30},
|
||||
"haiku": {"in": 1.0, "out": 5.0, "cache_w": 1.25, "cache_r": 0.10},
|
||||
"opus": {"in": 5.0, "out": 25.0, "cache_w": 10.0, "cache_r": 0.50},
|
||||
"sonnet": {"in": 3.0, "out": 15.0, "cache_w": 6.0, "cache_r": 0.30},
|
||||
"haiku": {"in": 0.75, "out": 3.75, "cache_w": 1.50, "cache_r": 0.075},
|
||||
}
|
||||
_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
|
||||
assistant line carries message.usage (input/output/cache token counts) and a
|
||||
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"
|
||||
today = datetime.date.today()
|
||||
@@ -93,6 +100,7 @@ def compute_today_usage() -> dict:
|
||||
output_tokens = 0
|
||||
cost = 0.0
|
||||
messages = 0
|
||||
seen = set()
|
||||
try:
|
||||
files = list(base.glob("**/*.jsonl"))
|
||||
except OSError:
|
||||
@@ -129,6 +137,14 @@ def compute_today_usage() -> dict:
|
||||
continue
|
||||
if d != today:
|
||||
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
|
||||
out = usage.get("output_tokens", 0) or 0
|
||||
cw = usage.get("cache_creation_input_tokens", 0) or 0
|
||||
|
||||
Reference in New Issue
Block a user