caveman 的 /caveman-stats:opencode 插件中的会话 Token 节省统计命令实现详解
在 caveman 的 opencode 插件中,/caveman-stats 是一个由纯 Markdown 提示词模板驱动的斜杠命令:它不执行任何脚本,而是让模型读取 caveman 的终身历史日志,输出一张包含总节省 token、会话数与平均压缩比的简短统计表。读完本文,你将理解这条命令在 opencode 插件体系中的完整定义、它所依赖的历史日志数据格式、与之对应的 Claude Code 端 caveman-stats.js 脚本的源码级实现细节,以及测试如何保证每一个数字都可验证、不夸大。
命令定义文件:frontmatter 与提示词正文
/caveman-stats 的完整定义位于 caveman-stats.md,全文如下:
---
description: Show caveman lifetime token-savings stats
---
Show caveman stats — total tokens saved, sessions, average compression ratio.
Read the lifetime history log at `~/.config/caveman/.caveman-history.jsonl`
(or wherever the caveman-stats script writes it). Output: total saved,
sessions counted, avg ratio. One short table.
这份文件由两部分构成:
- YAML frontmatter:
description字段供 opencode 的命令补全/列表展示,声明该命令的用途是"显示 caveman 终身 token 节省统计"。 - 提示词正文:这才是命令的真正逻辑。opencode 在用户输入
/caveman-stats时,会把命令文件中的正文替换进消息发给模型(plugin.js 源码注释中明确说明:opencode 会在chat.message钩子看到消息之前,把输入的斜杠命令替换为命令文件的提示词文本)。也就是说,这段文字本质上是一份给模型的执行指令,包含三个明确约束:- 做什么:展示 caveman 统计——总节省 token 数(total tokens saved)、会话数(sessions)、平均压缩比(average compression ratio);
- 从哪读:终身历史日志,默认路径为
~/.config/caveman/.caveman-history.jsonl,括号内的补充说明承认实际路径取决于 caveman-stats 脚本的写入位置; - 输出形式:total saved、sessions counted、avg ratio 三项数据,且要求压缩成"一张简短表格"(One short table),符合 caveman 自身的简洁风格。
这与 caveman 在 Claude Code 端的实现形成对照:Claude Code 中 /caveman-stats 是通过 caveman-mode-tracker.js 这个 UserPromptSubmit 钩子拦截后,直接执行 caveman-stats.js 子进程(2.5 秒超时),并把脚本输出以 additionalContext 的形式要求模型逐字转述;而 opencode 端没有等价的脚本执行通道,因此 README 中明确列出该插件"不提供 statusline 徽章"等限制,统计能力退化为"模型读文件 + 汇总"的提示词方案。
数据来源:终身历史日志 .caveman-history.jsonl
命令提示词指向的 ~/.config/caveman/.caveman-history.jsonl 是 caveman 的终身累计统计账本。从源码 caveman-stats.js 看,脚本实际写入的路径是 $CLAUDE_CONFIG_DIR(未设置时为 ~/.claude)下的 .caveman-history.jsonl——这正是模板里 "(or wherever the caveman-stats script writes it)" 这句留白的原因:路径随宿主与安装布局而变,命令让模型自行寻找。
每次运行 stats 且会话已有至少一个回合时,脚本会向该文件追加一行 JSON 快照(main 函数):
{
"ts": 1725000000000,
"session_id": "s",
"mode": "full",
"model": "claude-sonnet-4-7",
"output_tokens": 350,
"turns": 1,
"est_saved_tokens": 650,
"est_saved_usd": 0.00975
}
各字段的写入逻辑与语义:
| 字段 | 来源 | 说明 |
|---|---|---|
ts |
Date.now() |
快照时间戳,用于时间窗过滤与"每会话取最新"去重 |
session_id |
钩子转发或 transcript 文件名 | 会话标识,终身聚合的主键 |
mode |
.caveman-active 标志文件 |
当前 caveman 模式(如 full),非 active 则为 null |
model |
会话 JSONL 中首个带 usage 的 assistant 消息 |
用于查输出 token 单价 |
output_tokens / turns |
会话日志解析 | 输出 token 总量与助手回合数 |
est_saved_tokens |
按模式归属后的估算 | 见下文"按模式归属" |
est_saved_usd |
token 数 × 模型输出单价 | 未知模型时为 0 |
追加写入走 caveman-config.js 提供的 appendFlag,该写入器是符号链接安全的(拒绝 symlinked 目标、原子 temp+rename、0600 权限),这一点有专门测试 appendFlag is symlink-safe (refuses symlinked target)(test_caveman_stats.js)守护。
同一会话内多次运行 /caveman-stats 会写入多行,但聚合时只保留每个 session_id 时间戳最新的一行——aggregateHistory 用 Map 按 session_id 覆盖实现(无 session_id 的旧行归入 '_' 桶)。测试 --all aggregates latest entry per session 验证了这一点:两个会话、其中一个会话有两条快照时,只计最新一条,185 + 371 = 556。
底层脚本:会话解析、模式归属与节省估算
虽然 opencode 端由模型直接读历史文件,但同一套数据的生产端是 caveman-stats.js(约 680 行),理解它才能理解历史日志里每个数字是怎么来的。
会话日志解析
parseSession 逐行解析 Claude Code 的会话 JSONL,只统计 type === 'assistant' 且带 message.usage 的条目,累计 output_tokens、cache_read_input_tokens 与回合数 turns,同时记录每条消息的 {ts, outputTokens} 供后续时间对齐。无 --session-file 参数时,findRecentSession 会在 $CLAUDE_CONFIG_DIR/projects 下深度优先搜索 mtime 最新的 .jsonl 文件。
按模式归属(#601):不把整段会话算给当前模式
统计中最容易出错的是"整段会话的 token 都记在 stats 时刻 flag 所显示的模式下"——会话中途开启 caveman 会虚增节省,中途关闭则归零。attributeByMode 采用三级证据链解决:
log(最精确):模式追踪钩子(caveman-mode-tracker.js 中的recordModeChange)在每次真实模式切换时向.caveman-mode-log.jsonl追加{ts, mode, prev}行;stats 把这些时间戳与会话消息时间戳对齐,每条消息的 token 记在其生成时刻实际激活的模式名下。会话隔离由--session-id参数保证,其他窗口的切换行会被丢弃(readModeLog)。flag-mtime:无切换日志但 flag 文件在会话中途被写入时,只有写入之后的 token 可归属;之前的 token 标记为 unknown 并排除而非猜测(源码称之为 no-fake-savings 原则)。whole-session:既无日志也无中途变更证据时,才按"当前模式覆盖整段会话"处理(#601 之前的行为,仅在模式从未变化时正确)。
测试 attributes tokens to the mode active when each message happened (#601)(test_caveman_stats.js)构造了"开启前 300 verbose token + 开启后 350 token"的会话:正确结果是只有 350 个 full 模式 token 产生 650 的节省估算,而旧的全会话算法会虚报 1,207。
压缩比与金额估算
节省估算的核心是一张模式→压缩比表(第 81 行):
const COMPRESSION = { 'full': 0.65 };
目前只有 full 模式有 benchmarks/ 目录中 10 个任务、sonnet-4 的实测均值 65% 支撑;lite/ultra/wenyan 等模式在跑过基准并提交结果前不产生任何估算值——对 ultra 模式直接输出 No savings estimate for 'ultra' mode — only 'full' has benchmark data.。
估算公式(deriveSavings):已知有 caveman 时输出为 T、压缩比为 r,则无 caveman 时的等效输出约为 T / (1 - r),节省即两者之差。测试用例验证:350 个 full 模式 token → 350 / 0.35 = 1000 → 节省 650,约 65%。
金额换算由 MODEL_OUTPUT_PRICE_PER_M 这张按前缀匹配的输出单价表完成,覆盖 Claude 5 系列(Fable/Mythos $50/M、Opus 5 $25/M、Sonnet 5 $10/M)到 Claude 3 系列的各档,且"最具体的前缀必须排在前面"(因为 priceForModel 返回首个命中)。未知模型(如 gpt-4)返回 null,此时只保留 token 估算、省略 USD 行——测试 omits USD line when model is unknown 守护了该行为。
规则开销与净额:不藏起负收益
caveman 的规则每回合都会注入约 1,000–1,500 个输入 token(约 5 KB 的 SKILL.md 加每回合的强化提醒,详见 HONEST-NUMBERS.md)。脚本用每回合 1,250 token 的默认值(第 89 行,可用环境变量 CAVEMAN_RULE_OVERHEAD_TOKENS 覆盖)计算开销,deriveNet 输出 Est. net = 节省输出 − 规则输入开销。关键设计:
- 净额为正时显示
Est. net: +1,536 (net saving after rule overhead); - 净额为负时直白提示
caveman cost more than it saved for this workload — consider turning it off——测试session shows a NEGATIVE net and tells the user to consider turning caveman off (#145)专门验证了这条文案; - 若节省区间本身无法归属(unknown tokens)或模式无基准数据,则干脆不输出净额行,避免用猜测的数字拼出净额。
CAVEMAN_RULE_OVERHEAD_TOKENS 的输入校验同样有测试:非数字、0、负数、小数一律回落到默认 1250,而不是产出无意义的开销。
输出缩减比例:只敢说"输出占比"
outputReductionPct 计算的唯一比例是 saved / (saved + used),即 caveman 避免了多大比例的本会产生的输出 token。源码注释明确说明:agentic 会话中 input + cache token 占绝对大头且计入 Pro/Max 限额,而 caveman 并不减少它们,所以这个数字绝不能被标榜为"会话用量占比"或"预算占比"。测试 session view never claims a % of usage/budget — only output reduction 甚至用正则断言输出中不得出现 budget / of your usage 等字样,也不得凭空编造 Anthropic 配额尺寸。
命令行接口:脚本可直接运行的完整参数
在 Claude Code 之外,脚本本身是一个可独立运行的 CLI(main),/caveman-stats 斜杠命令的参数会被模式追踪钩子原样转发:
| 参数 | 行为 | 验证测试 |
|---|---|---|
--session-file <path> |
指定要解析的会话 JSONL(钩子用 transcript_path 传入,保证读的是活跃会话而非最新 mtime 文件) |
reads --session-file directly and sums output tokens |
--session-id <id> |
按会话过滤模式切换日志,避免其他窗口的切换行污染本会话时间线 | #601 系列测试 |
--share |
输出单行可分享摘要,如 🪨 Saved 650 output tokens (~$0.0098) across 1 turns this session — caveman.sh;无基准数据时退化为 🪨 1 turns, 200 output tokens this session |
--share prints single-line tweetable summary |
--all |
输出终身聚合视图:Sessions、Output tokens、Est. tokens saved、Est. output reduction、Est. saved (USD) 及净额块;无历史时提示 No sessions logged yet |
--all aggregates latest entry per session |
--since <Nh|Nd> |
时间窗过滤,如 7d、24h;非法格式(如 sometime)以退出码 2 报错 --since takes Nh or Nd (e.g. 7d, 24h), got: ... |
--since rejects malformed durations |
此外脚本还有两个"副作用"输出:
- statusline 后缀文件:每次运行后把终身节省总量渲染成
⛏ 2.8k形式写入.caveman-statusline-suffix,供 caveman-statusline.sh / caveman-statusline.ps1 直接cat而无需解析 JSONL;写入同样经过 symlink 安全通道,且测试验证了 statusline 会对该文件做控制字节剥离,防止 ANSI 转义注入终端。 - 记忆文件压缩检测:findCompressedPairs 扫描
$CLAUDE_CONFIG_DIR与当前工作目录下的*.original.md备份对(/caveman-compress留下的原件),若压缩版更小则按约 4 字符/token 折算,输出Memory compressed: N files, ~X tokens saved per session start (approx);压缩版不小于原件的伪压缩对被跳过。
测试矩阵:从端到端到纯函数
tests/test_caveman_stats.js 用真实子进程调用覆盖了整条链路,运行方式即文件头注释:node tests/test_caveman_stats.js。代表性用例:
- 端到端(脚本直跑):造一个
.claude/projects/p/s.jsonl假会话 + 临时CLAUDE_CONFIG_DIR,断言Turns/Output tokens/Cache-read tokens求和正确; - 端到端(经模式追踪钩子):向 caveman-mode-tracker.js 喂
{"prompt": "/caveman-stats --share", "transcript_path": sess},断言输出 JSON 的hookSpecificOutput.additionalContext含统计块,且运行 stats 不改变模式 flag(mode tracker preserves caveman flag when /caveman-stats fires); - 纯函数级:
priceForModel前缀匹配(含带[1m]后缀的模型 id 与 null 输入)、humanizeTokens(2786 → '2.8k'、1_250_000 → '1.3M')、outputReductionPct的边界(0 节省返回 null 而非 0%)、deriveNet与开销覆盖值。
skill 侧的交付说明见 skills/caveman-stats/SKILL.md:该 skill 由 hooks/caveman-stats.js 交付,模型在 skill 触发时无需做任何事——钩子直接以格式化好的统计作为回复上下文,用户立即看到数字。
opencode 插件侧的运行前提与边界
把 /caveman-stats 放进 opencode 使用时,需要理解 opencode 插件 的整体形态(package.json 标记为 ESM 的 caveman-opencode-plugin):
- 插件由
session.created事件写入默认模式到~/.config/opencode/.caveman-active,通过chat.message拦截/caveman命令与自然语言切换,通过experimental.chat.system.transform注入每回合强化行;安装布局与文件角色见 src/plugins/opencode/README.md。 commands/目录下共六个斜杠命令模板(/caveman、/caveman-commit、/caveman-compress、/caveman-help、/caveman-review、/caveman-stats),全部以提示词展开方式工作,因此/caveman-stats在 opencode 中的实际效果是"模型按模板指令读历史文件并生成一张表",而不是像 Claude Code 端那样由脚本生成确定性数字。- 相应地,opencode 端没有 statusline 徽章(TUI 未暴露插件可写的 statusline),若想在 shell 中查看模式,只能自行读取
~/.config/opencode/.caveman-active标志文件。 - 历史文件路径的差异也随之而来:模板写的是
~/.config/caveman/.caveman-history.jsonl,而脚本生产端写在CLAUDE_CONFIG_DIR(默认~/.claude)下——若你的环境从未跑过 Claude Code 端的 stats 脚本,历史文件可能尚不存在,此时 opencode 端的命令应如实报告"暂无统计",而不是编造数字。
诚实数字边界:统计输出能声称什么
stats 的所有输出都受 docs/HONEST-NUMBERS.md 约束,值得随命令一并理解:
- caveman 的响应 skill 只压缩输出,不压缩输入、上下文、文件与思考 token(本地引擎与代理是独立组件);
- skill 每回合新增约 1–1.5k 输入 token 的规则成本,若输出节省低于该成本即为净亏(简短编码问答、按请求计费的平台如 Copilot 是典型场景);
- 官方规则是:对同一任务开/关 caveman 各跑一次,以服务商账单总额对比为准;A/B 结果为净负时直接关掉。
这也是为什么 caveman-stats.js 的输出措辞如此克制:节省一律标注 "est." 与 "output tokens only; input/cache usage is unchanged",比例只称 "output reduction",未知模型不报金额,无法归属的 token 不猜。
小结
/caveman-stats 在 opencode 插件里只是一段三行提示词(caveman-stats.md),但它背后的数据链是完整的:模式追踪钩子记录带时间戳的模式切换(.caveman-mode-log.jsonl),stats 脚本解析会话 JSONL、按模式归属 token、用唯一有基准支撑的 full 模式 65% 压缩比估算节省、按模型前缀查价折算美元、再减去每回合 1,250 token 的规则开销给出净额,最后把每会话最新快照追加进 .caveman-history.jsonl 终身账本。opencode 端的命令模板则负责在宿主没有脚本执行通道时,让模型基于这份账本输出"total saved / sessions / avg ratio"三要素的简短表格——数字的生产与呈现被分离在两个宿主上,而口径(估算、仅输出 token、不夸大)由同一套源码与 tests/test_caveman_stats.js 中的数十个断言共同保证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00