diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e1792f6..f4b3ae7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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." } ] diff --git a/skills/claude-usage-report/SKILL.md b/skills/claude-usage-report/skills/claude-usage-report/SKILL.md similarity index 96% rename from skills/claude-usage-report/SKILL.md rename to skills/claude-usage-report/skills/claude-usage-report/SKILL.md index 1237e74..29fb3d1 100644 --- a/skills/claude-usage-report/SKILL.md +++ b/skills/claude-usage-report/skills/claude-usage-report/SKILL.md @@ -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 diff --git a/skills/claude-usage-report/prices.json b/skills/claude-usage-report/skills/claude-usage-report/prices.json similarity index 72% rename from skills/claude-usage-report/prices.json rename to skills/claude-usage-report/skills/claude-usage-report/prices.json index 4cfad19..3f42461 100644 --- a/skills/claude-usage-report/prices.json +++ b/skills/claude-usage-report/skills/claude-usage-report/prices.json @@ -1,5 +1,5 @@ { - "_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": [ { @@ -7,6 +7,7 @@ "in": 10.0, "out": 50.0, "cw": 12.5, + "cw_1h": 20.0, "cr": 1.0 } ], @@ -16,6 +17,7 @@ "in": 5.0, "out": 25.0, "cw": 6.25, + "cw_1h": 10.0, "cr": 0.5 } ], @@ -25,6 +27,7 @@ "in": 5.0, "out": 25.0, "cw": 6.25, + "cw_1h": 10.0, "cr": 0.5 } ], @@ -34,6 +37,7 @@ "in": 5.0, "out": 25.0, "cw": 6.25, + "cw_1h": 10.0, "cr": 0.5 } ], @@ -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" }, @@ -51,6 +56,7 @@ "in": 3.0, "out": 15.0, "cw": 3.75, + "cw_1h": 6.0, "cr": 0.3, "note": "standard" } @@ -61,6 +67,7 @@ "in": 3.0, "out": 15.0, "cw": 3.75, + "cw_1h": 6.0, "cr": 0.3 } ], @@ -70,6 +77,7 @@ "in": 1.0, "out": 5.0, "cw": 1.25, + "cw_1h": 2.0, "cr": 0.1 } ], @@ -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" } } diff --git a/skills/claude-usage-report/update_pricing.py b/skills/claude-usage-report/skills/claude-usage-report/update_pricing.py similarity index 100% rename from skills/claude-usage-report/update_pricing.py rename to skills/claude-usage-report/skills/claude-usage-report/update_pricing.py diff --git a/skills/claude-usage-report/usage_report.py b/skills/claude-usage-report/skills/claude-usage-report/usage_report.py similarity index 88% rename from skills/claude-usage-report/usage_report.py rename to skills/claude-usage-report/skills/claude-usage-report/usage_report.py index f3cee53..2459519 100644 --- a/skills/claude-usage-report/usage_report.py +++ b/skills/claude-usage-report/skills/claude-usage-report/usage_report.py @@ -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:] @@ -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) @@ -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):