From 68e79ebcc1760d826fc1dd18ac2f076048c21f3b Mon Sep 17 00:00:00 2001 From: Pratik Bhavsar Date: Fri, 6 Nov 2026 09:51:57 +0530 Subject: [PATCH 1/2] fix(claude): keep cost covered when a resumed session carries zeroed copies Resumed and forked Claude Code transcripts copy earlier messages with all four token totals zeroed but the original cache_creation split left in place. When the parent transcript is gone, those copies reached pricing, failed the split consistency check, and marked the whole session's cost unavailable. Treat a row whose four totals are all reported zero as an unbilled copy and ignore its stale split; any nonzero total with an inconsistent split still fails closed. Co-Authored-By: Claude Code --- tests/test_claude_cost_correctness.py | 34 +++++++++++++++++++++++++++ token_meter/runtimes/claude.py | 14 ++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_claude_cost_correctness.py b/tests/test_claude_cost_correctness.py index 4cd001c..befe758 100644 --- a/tests/test_claude_cost_correctness.py +++ b/tests/test_claude_cost_correctness.py @@ -342,6 +342,40 @@ def test_inconsistent_cache_detail_and_unknown_dimensions_are_unpriceable(self): unknown_geo, "claude-opus-4-8", "claude", ).values()), 0.0) + def test_zeroed_copy_with_stale_cache_split_is_free_and_keeps_cost_covered(self): + copied = claude_usage(cache_write_5m=953_342) + copied["cache_creation_input_tokens"] = 0 + source = { + "provider": "claude", "client": "claude_code", + "label": "Claude Code", "id": "zeroed-copy", + "session": "zeroed-copy.jsonl", "path": "/private/not-read", + "project": "private", "mtime": 1, "title": None, "model": None, + } + + normalized = _normalized_usage(copied) + summary = meter.claude_summary(source, [ + assistant("copied-from-deleted-parent", copied), + assistant( + "own", claude_usage(input_tokens=1_000_000), + "2026-09-07T00:00:01.000Z", + ), + ]) + + self.assertTrue(normalized["billing_available"]) + self.assertEqual(normalized["cache_creation_5m_input_tokens"], 0) + self.assertEqual(sum(meter.cost_of( + copied, "claude-opus-4-8", "claude", + ).values()), 0.0) + self.assertEqual(summary["cost"], 5.0) + self.assertTrue(summary["availability"]["cost"]) + self.assertFalse(summary["cost_approx"]) + + def test_zero_cache_total_beside_billed_tokens_still_fails_closed(self): + usage = claude_usage(output_tokens=10, cache_write_5m=500) + usage["cache_creation_input_tokens"] = 0 + + self.assertFalse(_normalized_usage(usage)["billing_available"]) + def test_aggregate_only_cache_write_remains_a_labeled_compatibility_estimate(self): normalized = _normalized_usage({ "input_tokens": 0, diff --git a/token_meter/runtimes/claude.py b/token_meter/runtimes/claude.py index 052e4d1..3a7f9b1 100644 --- a/token_meter/runtimes/claude.py +++ b/token_meter/runtimes/claude.py @@ -154,7 +154,19 @@ def _normalized_usage(usage): cache_write_unspecified = 0 cache_duration_valid = True cache_duration_complete = cache_write == 0 - if isinstance(cache_creation, dict): + output_count, output_count_reported = normalize_reported_token_count( + usage.get("output_tokens") + ) + # Resumed and forked transcripts copy history with the four totals zeroed + # but the original cache_creation split left in place; the copy is not billed. + zeroed_copy = ( + input_reported and cache_read_reported and cache_write_reported + and output_count_reported + and not (input_tokens or cache_read or cache_write or output_count) + ) + if isinstance(cache_creation, dict) and zeroed_copy: + cache_duration_complete = True + elif isinstance(cache_creation, dict): cache_write_5m, cache_write_5m_reported = normalize_reported_token_count( cache_creation.get("ephemeral_5m_input_tokens") ) From 68d1efc69a37601950ecb7fced44b9f2c5bfa103 Mon Sep 17 00:00:00 2001 From: Pratik Bhavsar Date: Fri, 6 Nov 2026 09:57:32 +0530 Subject: [PATCH 2/2] fix(claude): require all four totals before treating a row as a zeroed copy A row that omits a cache total must still fail the split consistency check instead of being priced as an unbilled copy. Co-Authored-By: Claude Code --- tests/test_claude_cost_correctness.py | 20 ++++++++++++++++---- token_meter/runtimes/claude.py | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_claude_cost_correctness.py b/tests/test_claude_cost_correctness.py index befe758..d07c2d7 100644 --- a/tests/test_claude_cost_correctness.py +++ b/tests/test_claude_cost_correctness.py @@ -371,10 +371,22 @@ def test_zeroed_copy_with_stale_cache_split_is_free_and_keeps_cost_covered(self) self.assertFalse(summary["cost_approx"]) def test_zero_cache_total_beside_billed_tokens_still_fails_closed(self): - usage = claude_usage(output_tokens=10, cache_write_5m=500) - usage["cache_creation_input_tokens"] = 0 - - self.assertFalse(_normalized_usage(usage)["billing_available"]) + for field in ("output_tokens", "cache_read_input_tokens"): + with self.subTest(field=field): + usage = claude_usage(cache_write_5m=500) + usage["cache_creation_input_tokens"] = 0 + usage[field] = 10 + + self.assertFalse(_normalized_usage(usage)["billing_available"]) + + def test_zero_cache_split_row_missing_a_total_still_fails_closed(self): + for field in ("cache_read_input_tokens", "cache_creation_input_tokens"): + with self.subTest(field=field): + usage = claude_usage(cache_write_5m=500) + usage["cache_creation_input_tokens"] = 0 + del usage[field] + + self.assertFalse(_normalized_usage(usage)["billing_available"]) def test_aggregate_only_cache_write_remains_a_labeled_compatibility_estimate(self): normalized = _normalized_usage({ diff --git a/token_meter/runtimes/claude.py b/token_meter/runtimes/claude.py index 3a7f9b1..e0bd760 100644 --- a/token_meter/runtimes/claude.py +++ b/token_meter/runtimes/claude.py @@ -160,7 +160,8 @@ def _normalized_usage(usage): # Resumed and forked transcripts copy history with the four totals zeroed # but the original cache_creation split left in place; the copy is not billed. zeroed_copy = ( - input_reported and cache_read_reported and cache_write_reported + all(field in usage for field in USAGE_TOKEN_FIELDS) + and input_reported and cache_read_reported and cache_write_reported and output_count_reported and not (input_tokens or cache_read or cache_write or output_count) )