Claude Code 本地日志比 Dashboard 多一倍?cache_read 的隐藏折扣坑
某天收到告警:「7 天限额已用 33%」。明明当天只做了几个小功能,怎么会这么猛?翻开 Anthropic Dashboard,显示当日用了约 4 亿 token。于是我写了个脚本,遍历 ~/.claude/projects/**/*.jsonl,把每个 assistant turn 的 message.usage 全加起来——结果出来是 8 亿。
Dashboard 说 4 亿,本地算出来 8 亿,差了整整一倍。是日志写错了?还是 Dashboard 有 bug?
现象
本地 JSONL 聚合脚本输出大致如下:
input: 0.03M
output: 4.33M
cache_create: 11.70M
cache_read: 785.53M ← 占原始总量 98%
TOTAL raw: 801.6M
而 Dashboard 显示当日 ~408M。
根因
每个 JSONL 行的 message.usage 包含四个字段:
input_tokens— 本轮实际输入output_tokens— 本轮输出cache_creation_input_tokens— 写入 prompt cache 的部分cache_read_input_tokens— 从 prompt cache 命中并复读的部分
关键就在 cache_read:在 $-计费里,它的单价是普通 input 的 10%;在 限额计数器(rate-limit / weekly usage)里,Anthropic 似乎对它按约 50% 折算——但这个系数从未在官方文档中公开说明。
把 50% 系数代进去:
0.03 + 4.33 + 11.70 + 785.53 × 0.5 = 408.8M ✓ 吻合 Dashboard
一次搞清楚:cache_read 存在两套折扣体系——计费体系(10% 价格)和限额体系(~50% 折算)。这两套是独立的,不要混用。
直接把四个字段相加得到的"原始 token 总量"≈ Dashboard 的 2 倍,因为 cache_read 在限额计数器里不是全额计入。如果你的 cache_read 占比越高(长 session 必然如此),差距就越大。
正解
用如下公式估算 Dashboard 数字:
dashboard_estimate = input + output + cache_create + cache_read × 0.5
验证脚本(Node.js ESM):
// /tmp/token_analysis.mjs
import { readdirSync, statSync, readFileSync } from 'fs';
import { join } from 'path';
const root = `${process.env.HOME}/.claude/projects`;
const today = new Date().toISOString().slice(0, 10); // 本地日期手动核对
function walk(dir, files = []) {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const s = statSync(p);
if (s.isDirectory()) walk(p, files);
else if (name.endsWith('.jsonl') && s.mtime.toISOString().slice(0, 10) === today)
files.push(p);
}
return files;
}
const total = { input: 0, output: 0, cache_create: 0, cache_read: 0 };
const sessions = [];
for (const f of walk(root)) {
const s = { input: 0, output: 0, cache_create: 0, cache_read: 0, turns: 0 };
for (const line of readFileSync(f, 'utf8').split('\n').filter(Boolean)) {
try {
const o = JSON.parse(line);
const u = o.message?.usage;
if (u && o.timestamp?.startsWith(today)) {
s.input += u.input_tokens || 0;
s.output += u.output_tokens || 0;
s.cache_create += u.cache_creation_input_tokens || 0;
s.cache_read += u.cache_read_input_tokens || 0;
s.turns += 1;
}
} catch {}
}
if (s.turns > 0) { s.file = f; sessions.push(s); }
}
for (const s of sessions) for (const k of ['input','output','cache_create','cache_read']) total[k] += s[k];
const raw = total.input + total.output + total.cache_create + total.cache_read;
const est = total.input + total.output + total.cache_create + total.cache_read * 0.5;
console.log({
raw_M: (raw / 1e6).toFixed(1),
dashboard_estimate_M: (est / 1e6).toFixed(1),
total,
top_sessions: sessions
.sort((a, b) => (b.input+b.output+b.cache_create+b.cache_read) - (a.input+a.output+a.cache_create+a.cache_read))
.slice(0, 5)
.map(s => ({ turns: s.turns, cache_read_M: (s.cache_read/1e6).toFixed(1), file: s.file.split('/').slice(-2).join('/') }))
});聚合出来的 raw 总量除以 Dashboard 数,如果商约为 2,说明你的 session 主要是 cache_read 组成,用 50% 系数即可对齐。如果商差得更远(比如 10×),尝试 0.1 权重——那对应的是 $-计费折扣而非限额折扣。两个系统,两套系数,分清楚。
实测:谁真正烧掉了限额
拿到 Top session 排名后,三个问题直接定位根因:
- 用的哪个模型? Opus 的限额权重约是 Sonnet 的 5 倍,单会话 Opus + 500 轮 = 灾难。
- 跑了多少轮? 一个会话跑了 534 轮,
cache_read占 99%——说明几百 KB 的文件被每轮重复送进 context。 - cache_read 占比多高? 超过 90% 意味着是"长 session 复读膨胀",不是真正的新工作量。
一句话外卖
cache_read在限额计数器里打 5 折,在计费里打一折,两套系统别混用——本地 JSONL 算出的"原始 token 数"永远比 Dashboard 大,差距取决于你的 cache 命中率有多高。