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
|
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user