【免费下载链接】tokentab
A CLI that reads Claude Code, Codex, and Gemini CLI session logs and works out how much they cost, by model, project, and day.
tokentab是一个命令行工具,读取 Claude Code、Codex、Gemini CLI 自动写在本地磁盘上的会话日志,统计 token 用量和花费,并按模型、项目、日期、活动类型拆分——全程本地运行,不需要 API key,数据不出机器。这个项目最有意思的部分就是日志解析:三家工具的日志格式、token 计数口径各不相同,各有各的坑。这篇文章带你走进 tokentab/providers/ 目录,看看解析器分别为每种格式踩了什么坑、又是怎么绕过去的。
一、先看日志在哪:三家工具的落盘习惯
tokentab 不依赖任何网络接口,全部数据来自这些工具自己留下的日志文件:
| 工具 | 日志位置 | 文件格式 |
|---|---|---|
| Claude Code | ~/.claude/projects/**/*.jsonl | 每行一个 JSON 对象(JSONL) |
| Codex | ~/.codex/sessions/**/rollout-*.jsonl | 逐事件 JSONL,token 为累计值 |
| Gemini CLI | ~/.gemini/tmp/**/session-*.json | 整个会话一个 JSON 文档 |
每个工具对应tokentab/providers/下一个独立模块(claude.py、codex.py、gemini.py),只需暴露一个collect()函数。工具没装就返回空列表,自动跳过——你只会看到自己实际用过的工具。
二、Claude Code:最友好的格式,坑在缓存字段
Claude Code 是三个格式里最规整的,源码注释直接称它为 "friendliest format":一次会话一个.jsonl文件,每行一个 JSON 对象,assistant 回合自带模型名和 usage 块。
它的主要坑在于token 缓存被拆成独立字段。Claude 把缓存读取和缓存写入分开记录,如果不拆开来读,就会把"便宜的缓存命中"和"较贵的缓存写入"混成一笔糊涂账:
tokens=TokenCounts( input=usage.get("input_tokens", 0), output=usage.get("output_tokens", 0), cache_read=usage.get("cache_read_input_tokens", 0), cache_write=usage.get("cache_creation_input_tokens", 0), )对应源码:claude.py。
另一个细节是两遍扫描 + 去重(claude.py):
- 第一遍收集第一条用户消息和
tool_use工具名,用于活动分类(见 classify.py); - 第二遍才提取 usage 块,并用消息
id去重,防止同一次调用被计入两遍。
项目名也不是白拿的:会话文件路径里的项目目录名是被工具"把斜杠换成连字符"编码过的字符串,pretty_project()无法无损还原,只能取最后一段有意义的片段当标签(比如显示my-app而不是一长串编码)。
三、Codex:累计 token 必须做差,事件结构三层嵌套
Codex 的日志是事件流,最大的坑在计数口径:token_count事件里存的是整场会话的累计值。如果直接把每条事件加起来,token 会被重复计算 N 次。解析器的做法是对相邻事件做差,得到每次调用的增量:
d_in = max(0, tot_in - prev_in) d_out = max(0, tot_out - prev_out) d_cache = max(0, tot_cache - prev_cache)max(0, …)保证计数器被重置(比如新开会话)时不会出现负数。对应源码:codex.py。
第二个坑是事件结构不一致:token_count可能直接出现在顶层,也可能包在payload或info里。_find_token_count()一次兼容了三种可能的位置(codex.py),否则换个版本的 Codex 就会读不到数据。
还有两个"顺手"的坑:
- 工作目录来自文件开头附近的 session-meta 事件,得先做一遍"上下文扫描"才能知道这条记录属于哪个项目;
- 老会话存放在
archived_sessions/目录,和sessions/都要扫描,不然历史记录会凭空消失。
四、Gemini CLI:格式最"文明",坑在输入包含缓存
Gemini CLI 的格式最规整:一个会话一个 JSON 文件,messages数组里每条消息自带tokens对象。解析起来最省心,但它埋了一个静默的坑:input字段的口径是"包含缓存"的。
如果不做处理,缓存部分会在input和cached里各被计费一次——同样的 token 付两份钱。解析器在计费前先扣回去:
fresh_input = max(0, raw_input - cached) # input is inclusive of cache对应源码:gemini.py。这正是 README 里提到的 "pulls the cached tokens back out before pricing"。
另外两个小坑:
- 消息缺
model字段时兜底为gemini-2.5-pro(gemini.py); - 项目名取自
tmp/后的目录层级(gemini.py)。
五、三家共通的"防御式"解析技巧 🧩
格式各不相同,但防御烂数据的思路高度一致,公共工具都放在 shared.py:
| 工具函数 | 解决的问题 |
|---|---|
try_json() | 单行 JSON 解析失败时返回None而不是抛异常,一行坏数据不会搞挂整场统计 |
walk_files() | 遍历日志目录,跳过任何读不了的文件 |
to_date() | 同时兼容秒/毫秒时间戳(数值大于1e12视为毫秒)和带Z后缀的 ISO 字符串,失败时回退到纪元时间 |
pretty_project() | 把编码过的项目目录名还原成可读标签 |
对应源码:shared.py。
更外一层,providers/init.py 给每个工具的collect()都包了try/except:某个工具的日志坏了,只打印一条 warning,整份报告照常出——"一个坏掉的工具不该拖垮整个报表"。
六、解析之后:一切归一成 UsageRecord 📊
三家解析器输出的是同一个数据形状UsageRecord(定义在 types.py):provider、model、project、timestamp、activity、tokens、id。下游的计价、分组、Web 仪表盘只接触这一个结构,这也是"加一个新工具只需写一个解析模块"能成立的原因。
两个值得了解的下游设计:
- 活动分类是纯规则(classify.py):根据用过的工具名(编辑类/搜索类)加首条消息里的关键词(
fix、refactor、test…)猜出 debugging / refactor / testing / chat 等标签,没有模型调用,快且可人工核对; - 价格表是手工维护(prices.py):按"美元 / 百万 token"查表,模型名做模糊匹配(
claude-opus-4-6-20260514也能匹配到claude-opus-4-6)。匹配不上的模型会显示$0.00并明确提示,而不是静默按免费算。
七、小结:踩坑清单与扩展方向
| 格式 | 核心坑 | 解法 |
|---|---|---|
| Claude Code | 缓存读/写分字段;消息可能重复 | 分字段读取 + 按消息 id 去重 |
| Codex | token 是累计值;事件嵌套位置不固定 | 相邻事件做差 + 多位置兼容查找 |
| Gemini CLI | input含缓存 token | 计费前扣掉cached |
| 三家共通 | 坏行、坏时间戳、编码项目名 | 防御式解析,失败降级不崩溃 |
想动手扩展的话,仓库留了一个现成的练习位:cursor.py 是未实现的 stub——Cursor 的数据存在 SQLite 数据库state.vscdb里而非文本日志,按文件里的 TODO 补上解析逻辑、注册进 ALL_PROVIDERS,计价和报告就自动生效了。
安装也只需三步,装完得到一条tokentab命令,直接跑即可看到最近 7 天的账单:
git clone https://gitcode.com/gh_mirrors/to/tokentab cd tokentab pip install .整个项目的第三方依赖只有一个rich(终端表格),其余解析器、JSON 处理、Web 服务器全部是 Python 标准库——这也意味着读它的全部源码,不会遇到任何框架黑盒。
【免费下载链接】tokentab
A CLI that reads Claude Code, Codex, and Gemini CLI session logs and works out how much they cost, by model, project, and day.
相关推荐
Claude Code 会话恢复实战:掌握 `--continue` 与 `--resume` 三种接续方式
Claude Code 会话恢复实战:掌握 continue 与 resume 三种接续方式 本篇技术指南围绕 til(Today I Learned)仓库中
文档教程知识库新手快速上手:Hugging Face Skills 接入 Claude Code、Codex、Gemini CLI 与 Cursor 的 4 种方式
新手快速上手:Hugging Face Skills 接入 Claude Code、Codex、Gemini CLI 与 Cursor 的 4 种方式 Hugg
人工智能AI 技能/插件大模型AI 评测把 Claude Code 接入 9Router 后我踩的 5 个坑
把 Claude Code 接入 9Router 后我踩的 5 个坑 9Router 是一个跑在本地(默认 http://localhost:20128 )的
人工智能LLM 网关AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考