Skip to content

Commit b0a4da7

Browse files
committed
Merge fix/cache-ttl-pricing: v0.1.2
2 parents f679e7f + c5f017b commit b0a4da7

4 files changed

Lines changed: 69 additions & 8 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "session-cost",
33
"displayName": "Session Cost Tracker",
4-
"version": "0.1.1",
4+
"version": "0.1.2",
55
"description": "Track estimated per-session Claude Code spend, report it with /session-cost:report, and optionally enforce soft/hard budget limits from ~/.claude/budgets.json",
66
"author": {
77
"name": "OpenSea",

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ Initial release.
2525
- Fix: remove duplicate `hooks` declaration from plugin.json — `hooks/hooks.json`
2626
is auto-loaded by convention and redeclaring it fails plugin load with
2727
"Duplicate hooks file detected". Caught by live install verification.
28+
29+
## 0.1.2 — 2026-06-10
30+
31+
- Fix: price 1-hour-TTL cache writes at 2x base input (5-minute at 1.25x)
32+
using the per-TTL `cache_creation` breakdown in transcript usage blocks.
33+
Claude Code uses 1h cache exclusively, so flat 1.25x underpriced cache
34+
writes by 1.6x. Verified against live pricing docs and a real transcript.

scripts/cost_tracker.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
"default": {"input": 5.00, "output": 25.00},
2525
}
2626

27-
# Cache multipliers (5-minute TTL writes)
28-
CACHE_READ_DISCOUNT = 0.10 # cache reads cost ~10% of input price
29-
CACHE_CREATION_PREMIUM = 1.25 # cache writes cost ~125% of input price
27+
# Cache multipliers (relative to base input price)
28+
CACHE_READ_DISCOUNT = 0.10 # cache reads cost 10% of input price
29+
CACHE_CREATION_PREMIUM = 1.25 # 5-minute-TTL cache writes
30+
CACHE_CREATION_PREMIUM_1H = 2.0 # 1-hour-TTL cache writes (what Claude Code uses)
3031

3132
# Average token estimates per tool type (input_tokens, output_tokens).
3233
# Rough averages — the value is in aggregation, not per-call precision.
@@ -90,13 +91,29 @@ def get_pricing(model):
9091

9192

9293
def compute_cost(usage, model):
93-
"""Estimated USD cost from a Messages API usage dict."""
94+
"""Estimated USD cost from a Messages API usage dict.
95+
96+
When the usage carries a per-TTL `cache_creation` breakdown (real Claude
97+
Code transcripts do, and use 1-hour cache exclusively), price 5m writes
98+
at 1.25x and 1h writes at 2x base input. Otherwise fall back to 1.25x on
99+
the `cache_creation_input_tokens` total.
100+
"""
94101
pricing = get_pricing(model)
102+
breakdown = usage.get("cache_creation")
103+
if isinstance(breakdown, dict):
104+
cache_write_cost = pricing["input"] * (
105+
breakdown.get("ephemeral_5m_input_tokens", 0) * CACHE_CREATION_PREMIUM
106+
+ breakdown.get("ephemeral_1h_input_tokens", 0) * CACHE_CREATION_PREMIUM_1H
107+
)
108+
else:
109+
cache_write_cost = (
110+
usage.get("cache_creation_input_tokens", 0)
111+
* pricing["input"] * CACHE_CREATION_PREMIUM
112+
)
95113
return (
96114
usage.get("input_tokens", 0) * pricing["input"]
97115
+ usage.get("output_tokens", 0) * pricing["output"]
98-
+ usage.get("cache_creation_input_tokens", 0)
99-
* pricing["input"] * CACHE_CREATION_PREMIUM
116+
+ cache_write_cost
100117
+ usage.get("cache_read_input_tokens", 0)
101118
* pricing["input"] * CACHE_READ_DISCOUNT
102119
) / 1_000_000

tests/test_cost_tracker.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_basic_io_tokens(self):
4343
cost = cost_tracker.compute_cost(usage, "claude-opus-4-8")
4444
assert cost == 5.00 + 25.00
4545

46-
def test_cache_multipliers(self):
46+
def test_cache_multipliers_without_ttl_breakdown(self):
4747
usage = {
4848
"input_tokens": 0,
4949
"output_tokens": 0,
@@ -53,6 +53,43 @@ def test_cache_multipliers(self):
5353
cost = cost_tracker.compute_cost(usage, "claude-opus-4-8")
5454
assert cost == 5.00 * 1.25 + 5.00 * 0.10
5555

56+
def test_cache_ttl_breakdown_prices_1h_writes_at_2x(self):
57+
"""Real Claude Code transcripts carry a cache_creation breakdown and
58+
use 1-hour cache exclusively — 1h writes cost 2x base, not 1.25x."""
59+
usage = {
60+
"cache_creation_input_tokens": 1_500_000,
61+
"cache_creation": {
62+
"ephemeral_5m_input_tokens": 500_000,
63+
"ephemeral_1h_input_tokens": 1_000_000,
64+
},
65+
}
66+
cost = cost_tracker.compute_cost(usage, "claude-opus-4-8")
67+
# breakdown replaces the flat 1.25x on the total — no double count
68+
assert cost == 0.5 * 5.00 * 1.25 + 1.0 * 5.00 * 2.0
69+
70+
def test_cache_ttl_breakdown_real_session_shape(self):
71+
"""The exact shape observed in a live CC transcript: all 1h, 5m zero."""
72+
usage = {
73+
"input_tokens": 18,
74+
"output_tokens": 160,
75+
"cache_read_input_tokens": 31_312,
76+
"cache_creation_input_tokens": 31_463,
77+
"cache_creation": {
78+
"ephemeral_5m_input_tokens": 0,
79+
"ephemeral_1h_input_tokens": 31_463,
80+
},
81+
}
82+
cost = cost_tracker.compute_cost(usage, "claude-haiku-4-5")
83+
expected = (
84+
18 * 1.00 + 160 * 5.00 + 31_312 * 1.00 * 0.10 + 31_463 * 1.00 * 2.0
85+
) / 1e6
86+
assert abs(cost - expected) < 1e-12
87+
88+
def test_cache_breakdown_non_dict_falls_back_to_flat(self):
89+
usage = {"cache_creation_input_tokens": 1_000_000, "cache_creation": "junk"}
90+
cost = cost_tracker.compute_cost(usage, "claude-opus-4-8")
91+
assert cost == 5.00 * 1.25
92+
5693
def test_empty_usage_is_zero(self):
5794
assert cost_tracker.compute_cost({}, "claude-opus-4-8") == 0.0
5895

0 commit comments

Comments
 (0)