Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
{
"name": "claude-usage-report",
"source": "./skills/claude-usage-report",
"skills": [
"."
],
"description": "Usage/cost reports with self-refreshing pricing and per-account attribution."
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,17 @@ account-recording hook existed; `mixed: a | b` = a session that switched account
## Reference

Model prices ($/MTok), records in effect during the period:
| Model | Effective | Input | Output | Cache-write (5m) | Cache-read | Note |
|---|---|---:|---:|---:|---:|---|
| Model | Effective | Input | Output | Cache-write (5m) | Cache-write (1h) | Cache-read | Note |
|---|---|---:|---:|---:|---:|---:|---|
{the RATES block from parser output — one row per applicable record; a model with a
mid-period price change contributes more than one row}

Caveats:
- Estimate from local transcripts × public list prices, not a bill — for the authoritative
number use the Anthropic Console usage dashboard for the period.
- Cache-write assumes the 5-minute TTL (1.25× input); a 1-hour TTL would be 2× input.
- Cache-write is priced at each message's actual TTL (5-minute = 1.25× input, 1-hour = 2×
input), read from the transcript's `cache_creation` breakdown. Legacy transcripts that
lack the breakdown fall back to the 5-minute rate.
- Opus's 1M-context (`[1m]`) runs bill at standard rates; there is no >200K premium tier.
- Account attribution comes from this plugin's `SessionStart` hook (`hooks/hooks.json` →
`record_account.sh` → `record_account.py` → `~/.claude/session-accounts.jsonl`) and is
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"_comment": "Anthropic list prices, $/MTok. Fields: in=input, out=output, cw=5-minute cache write (1.25x input), cr=cache read (0.1x input). Each model has one or more records sorted by 'effective' date; the rate applied to a message is the record with the latest effective date <= that message's (UTC) day. This makes a report use the price in effect on the day being reported. Validate against current published pricing when generating a report and append a new dated record when a price changes (see SKILL.md). Known gap: if a price changes between two report runs, days billed before this file was updated will be costed at the older record.",
"_comment": "Anthropic list prices, $/MTok. Fields: in=input, out=output, cw=5-minute cache write (1.25x input), cw_1h=1-hour cache write (2x input), cr=cache read (0.1x input). Each message's cache-write tokens are split into 5m/1h from the transcript's cache_creation breakdown and costed at cw / cw_1h respectively; if a record omits cw_1h the parser falls back to 2x in. Each model has one or more records sorted by 'effective' date; the rate applied to a message is the record with the latest effective date <= that message's (UTC) day. This makes a report use the price in effect on the day being reported. Validate against current published pricing when generating a report and append a new dated record when a price changes (see SKILL.md). Known gap: if a price changes between two report runs, days billed before this file was updated will be costed at the older record.",
"models": {
"claude-fable-5": [
{
"effective": "2000-01-01",
"in": 10.0,
"out": 50.0,
"cw": 12.5,
"cw_1h": 20.0,
"cr": 1.0
}
],
Expand All @@ -16,6 +17,7 @@
"in": 5.0,
"out": 25.0,
"cw": 6.25,
"cw_1h": 10.0,
"cr": 0.5
}
],
Expand All @@ -25,6 +27,7 @@
"in": 5.0,
"out": 25.0,
"cw": 6.25,
"cw_1h": 10.0,
"cr": 0.5
}
],
Expand All @@ -34,6 +37,7 @@
"in": 5.0,
"out": 25.0,
"cw": 6.25,
"cw_1h": 10.0,
"cr": 0.5
}
],
Expand All @@ -43,6 +47,7 @@
"in": 2.0,
"out": 10.0,
"cw": 2.5,
"cw_1h": 4.0,
"cr": 0.2,
"note": "introductory through 2026-08-31"
},
Expand All @@ -51,6 +56,7 @@
"in": 3.0,
"out": 15.0,
"cw": 3.75,
"cw_1h": 6.0,
"cr": 0.3,
"note": "standard"
}
Expand All @@ -61,6 +67,7 @@
"in": 3.0,
"out": 15.0,
"cw": 3.75,
"cw_1h": 6.0,
"cr": 0.3
}
],
Expand All @@ -70,6 +77,7 @@
"in": 1.0,
"out": 5.0,
"cw": 1.25,
"cw_1h": 2.0,
"cr": 0.1
}
],
Expand Down Expand Up @@ -145,7 +153,7 @@
]
},
"_meta": {
"last_checked": "2026-07-07T16:38:34",
"last_checked": "2026-07-16T19:46:42",
"source": "https://platform.claude.com/docs/en/about-claude/pricing.md"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,15 @@ def rate_for(model, day):
else: break
return best # None if day precedes the earliest record

def cw1_rate(rec):
# 1-hour cache-write rate; fall back to 2x input if a record predates the cw_1h column
return rec.get("cw_1h", 2*rec["in"])

def msg_cost(rec, u):
return 0.0 if not rec else sum(u[f]*rec[f] for f,_ in FIELDS)/1e6
if not rec: return 0.0
c = sum(u[f]*rec[f] for f,_ in FIELDS if f != "cw")
c += u["cw5"]*rec["cw"] + u["cw1"]*cw1_rate(rec) # cache-write split by actual TTL
return c/1e6

# --- arg parsing: period + optional --top N
argv = sys.argv[1:]
Expand Down Expand Up @@ -157,12 +164,24 @@ def acct_of(sid):
if model:
usage = msg["usage"]
u = {fk: (usage.get(k,0) or 0) for fk,k in FIELDS}
# split cache-write by TTL; legacy transcripts w/o the
# breakdown fall back to the combined field, costed as 5m
cc = usage.get("cache_creation") or {}
if "ephemeral_5m_input_tokens" in cc or "ephemeral_1h_input_tokens" in cc:
u["cw5"] = cc.get("ephemeral_5m_input_tokens",0) or 0
u["cw1"] = cc.get("ephemeral_1h_input_tokens",0) or 0
else:
u["cw5"] = u["cw"]; u["cw1"] = 0
rec = rate_for(model, day)
if rec is None: unpriced.add(model)
c = msg_cost(rec, u); t = sum(u.values())
c = msg_cost(rec, u); t = sum(u[fk] for fk,_ in FIELDS)
for fk,_ in FIELDS:
model_tok[model][fk] += u[fk]
if rec: model_catcost[model][fk] += u[fk]*rec[fk]/1e6
if not rec: continue
if fk == "cw":
model_catcost[model][fk] += (u["cw5"]*rec["cw"] + u["cw1"]*cw1_rate(rec))/1e6
else:
model_catcost[model][fk] += u[fk]*rec[fk]/1e6
model_cost[model] += c
sess_cost[sid] += c; sess_tok[sid] += t
day_cost[day] += c; day_tok[day] += t; day_sids[day].add(sid)
Expand Down Expand Up @@ -199,13 +218,13 @@ def applicable_records(model):
if unpriced:
print(f"WARNING: no price record covers these models for part of the period: {', '.join(sorted(unpriced))} — add/extend them in prices.json.")

print("\nRATES in effect this period ($/MTok): model | effective | in | out | cache-write | cache-read | note")
print("\nRATES in effect this period ($/MTok): model | effective | in | out | cache-write(5m) | cache-write(1h) | cache-read | note")
for m in sorted(model_tok, key=lambda m: model_cost[m], reverse=True):
recs = applicable_records(m)
if not recs:
print(f" {m} | (no price record — UNPRICED)")
for r in recs:
print(f" {m} | {r['effective']} | {r['in']} | {r['out']} | {r['cw']} | {r['cr']} | {r.get('note','')}")
print(f" {m} | {r['effective']} | {r['in']} | {r['out']} | {r['cw']} | {cw1_rate(r)} | {r['cr']} | {r.get('note','')}")

print("\nBY MODEL: model | total$ | in(tok,$) | out(tok,$) | cache-write(tok,$) | cache-read(tok,$)")
for m in sorted(model_tok, key=lambda m: model_cost[m], reverse=True):
Expand Down