[BUG] Local /cost doubles usage when parsing session JSONL (cache write & I/O tokens; ~2× total)
Environment
- Platform (select one):
- [ ] Anthropic API
- [ ] AWS Bedrock
- [ ] Google Vertex AI
- [x] Other: Custom local CLI (with a
/costcommand) calling Anthropic models - Claude CLI version: 1.0.77
- Operating System: macOS
- Terminal: Warp.app
Bug Description
As issue #5902
<img width="750" height="344" alt="Image" src="https://github.com/user-attachments/assets/9dcdbd53-ba90-435a-8b1f-91ff932f5ccb" />
and https://x.com/badlogicgames/status/1957221028603535617 on X
The local /cost command double-counts usage:
cache writeis reported at \~2× the actualcache_creation_input_tokens.inputandoutputare also inflated (input grows by 2× per call; output shows +1 token per response vs. serverusage.output_tokens).
This makes the total cost appear roughly double the true amount.
Steps to Reproduce
- Make several short requests to Claude Sonnet 4 with prompt caching enabled (ephemeral 5-minute cache), e.g., send “hi” three times.
- After each response, run the local
/costcommand. - Compare
/costoutput vs. the rawusageobjects persisted in session JSONL logs.
Expected Behavior
cache writeshould equal eitherusage.cache_creation_input_tokensor the sum of typed fields (e.g.,usage.cache_creation.ephemeral_5m_input_tokens+..._1h_...), but not both.inputshould match the sum ofusage.input_tokensacross final messages.outputshould match the sum ofusage.output_tokens(no spurious +1 per message).
Actual Behavior
cache writeis \~2×: e.g., 15,079 becomes \~30.2k in/cost.inputincrements by 6 per call even thoughusage.input_tokensis 3 each time.outputincrements by 13 per call even thoughusage.output_tokensis 12.
Total shown by /cost is about 2× the true cost (e.g., $0.349 vs. actual ≈ $0.176 for the 3 calls).
Additional Context
Where local records come from:
Use the session JSONL logs stored under your project directory, e.g.:~/.claude/project/<YOUR_PROJECT>/<SESSION_ID>/*.jsonl
(Example session id from our logs: 0205ebf6-1fa8-4ac7-9716-27dd911a7e9d.)
Local token test code (Python, JSONL-only, local-sum view)
Reads one JSON object per line from the session .jsonl files and sums every snapshot as-is (no dedup), so you can compare the CLI’s /cost with raw local totals.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
JSONL local usage summarizer (no dedup).
- Input: one or more .jsonl files (or stdin with '-')
- Output: per-model totals and a grand total
"""
import argparse
import json
import glob
import re
import sys
from collections import defaultdict
from typing import Dict, Any, Iterable, Tuple
USAGE_KEYS = (
"input_tokens",
"output_tokens",
"cache_read_input_tokens",
"cache_creation_input_tokens",
)
MODEL_SUFFIX_RE = re.compile(r"^(.*?)-(\d{8})$")
def normalize_model(model: str) -> str:
if not model:
return "(unknown-model)"
m = MODEL_SUFFIX_RE.match(model)
return m.group(1) if m else model
def coerce_usage(u: Dict[str, Any]) -> Dict[str, int]:
out = {}
for k in USAGE_KEYS:
v = u.get(k, 0)
try:
out[k] = int(v)
except Exception:
try:
out[k] = int(float(v))
except Exception:
out[k] = 0
return out
def extract_usage(obj: Dict[str, Any]) -> Iterable[Tuple[str, Dict[str, int]]]:
"""
Yields (model, usage_dict) for common shapes:
A) { "message": { "model": "...", "usage": {...} } }
B) { "model": "...", "usage": {...} }
"""
msg = obj.get("message")
if isinstance(msg, dict):
model = normalize_model(msg.get("model") or obj.get("model") or "")
usage = msg.get("usage") or obj.get("usage")
if isinstance(usage, dict) and model:
yield (model, coerce_usage(usage))
return
model = normalize_model(obj.get("model") or "")
usage = obj.get("usage")
if isinstance(usage, dict) and model:
yield (model, coerce_usage(usage))
def iter_jsonl(paths: Iterable[str]) -> Iterable[Dict[str, Any]]:
"""
Reads JSON objects from .jsonl files or stdin ('-').
Skips malformed lines.
"""
expanded = []
for p in (paths or []):
if p == "-":
expanded.append("-")
else:
hits = glob.glob(p)
expanded.extend(hits if hits else [p])
if not expanded:
expanded = ["-"]
def lines_from(path):
if path == "-":
for line in sys.stdin:
yield line
else:
with open(path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
yield line
for path in expanded:
for line in lines_from(path):
s = line.strip()
if not s:
continue
try:
obj = json.loads(s)
except Exception:
continue
else:
yield obj
def main():
ap = argparse.ArgumentParser(description="Sum usage from session JSONL logs (no dedup).")
ap.add_argument("paths", nargs="*", help="JSONL paths/globs; use '-' or none for stdin")
args = ap.parse_args()
totals = defaultdict(lambda: {"input":0, "output":0, "cache_read":0, "cache_write":0})
count = 0
for obj in iter_jsonl(args.paths):
for model, usage in extract_usage(obj):
t = totals[model]
t["input"] += usage.get("input_tokens", 0)
t["output"] += usage.get("output_tokens", 0)
t["cache_read"] += usage.get("cache_read_input_tokens", 0)
t["cache_write"]+= usage.get("cache_creation_input_tokens", 0)
count += 1
if not totals:
print("No usage records found.")
return
print("\nUsage by model (LOCAL sum of all snapshots):")
for model in sorted(totals.keys()):
t = totals[model]
print(f" {model:20s} {t['input']:8d} in, {t['output']:6d} out, "
f"{t['cache_read']:8d} read, {t['cache_write']:8d} write")
g = {"input":0, "output":0, "cache_read":0, "cache_write":0}
for t in totals.values():
g["input"] += t["input"]
g["output"] += t["output"]
g["cache_read"] += t["cache_read"]
g["cache_write"]+= t["cache_write"]
print("\nGrand total (LOCAL):")
print(f" {g['input']:8d} in, {g['output']:6d} out, "
f"{g['cache_read']:8d} read, {g['cache_write']:8d} write")
print(f"\nSnapshots processed: {count}")
if __name__ == "__main__":
main()
How to run against session logs
# Example: first move the script to ~/.claude/project/<YOUR_PROJECT>
cd ~/.claude/project/<YOUR_PROJECT>
python local_jsonl_sum.py 0205ebf6-1fa8-4ac7-9716-27dd911a7e9d.jsonl
This makes it explicit that the “local” numbers come directly from the JSONL session records under your project directory.
11 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
爬
I'm having the same problem here.
I'm having the same problem here.
same problem occur
same problem
It's a bug in Claude Code. It double counts the token usage reported by the API both when calling Haiku and when calling non-small models.
https://x.com/badlogicgames/status/1957221028603535617
It appears that this is not an isolated incident, but Anthropics has not yet released an update to resolve this issue. :(
I'm having the same problem here.
This issue was resolved in version 1.0.88. https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md#1088
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.