Claude / Codex / ZCode / Qwen Token 统计链路说明
本文档说明 Open ACE 如何为 claude、codex、zcode、qwen 这 4 个本地工具抓取 token、计算每日/消息级统计、写入数据库,并被 WebUI、配额、分析与报表模块消费。
目标读者:
- 用户:理解为什么 Open ACE 的 token 数与官方控制台可能不同
- 项目维护者:排查统计异常、修 fetcher、接入新工具
- 二次开发者:明确该改哪一层,避免重复累计或字段误用
1. 总体链路
本地 JSONL / SQLite
-> scripts/fetch_*.py
-> scripts/shared/db.py
-> daily_usage
-> daily_messages
-> agent_sessions
-> session_messages
-> daily_stats / hourly_stats
-> user_daily_stats
-> app/repositories/* / app/services/*
-> Work / Manage 页面、报表、配额与分析接口
可以把这条链路理解为两层事实表、三层摘要:
daily_usage:按日、按工具、按主机的聚合事实表,适合总量和趋势daily_messages:消息级事实表,适合小时粒度、时间线、sender/project 归因agent_sessions/session_messages:Workspace 会话视图和 transcript 镜像daily_stats/hourly_stats/user_daily_stats:从事实表再派生的预聚合表
2. 统一口径
2.1 核心字段
| 字段 | 含义 | 主要存储位置 |
|---|---|---|
tokens_used | Open ACE 认为该记录的总 token | daily_usage、daily_messages、agent_sessions、session_messages |
input_tokens | 该记录的非 cache 输入 token | daily_usage、daily_messages、agent_sessions、session_messages |
output_tokens | 输出 token | daily_usage、daily_messages、agent_sessions、session_messages |
cache_tokens | cache token 总和 | 只在 daily_usage 中单独存列 |
request_count | Open ACE 定义下的请求数 | daily_usage、agent_sessions、user_daily_stats |
关键注意点:
daily_messages没有单独的cache_tokens列。- cache 只在
daily_usage.cache_tokens中单独保留,消息级只能看到tokens_used、input_tokens、output_tokens。 - 下游消费方如果已经拿到了
tokens_used,通常不应该再做tokens_used + cache_tokens,否则很容易 double count。
2.2 当前推荐理解
对这 4 个工具,Open ACE 的目标是尽量让:
tokens_used == 非 cache input_tokens + output_tokens + cache_tokens
但要注意 provider 原始语义并不完全相同:
- Claude:cache 单独给出,
tokens_used在 Open ACE 中显式把 cache 加进去 - Codex:provider
total_tokens已经包含 cached input,Open ACE 保留 provider total,并把input_tokens存成去 cache 后的输入 - Qwen:
totalTokenCount走 provider 口径,promptTokenCount里包含 cache,Open ACE 会把input_tokens改写为去 cache 后的输入 - ZCode:以
turn_usage为权威源,computed_total_tokens、input_tokens、output_tokens、cache_*一起使用
3. 各工具如何抓取与计算
3.1 Claude
源数据
- 路径:
~/.claude/projects/**.jsonl - 脚本:
scripts/fetch_claude.py
抓取方式
- Claude 本地日志按 JSONL 存储
- usage 主要从
entry["usage"]或entry["message"]["usage"]提取 - 入口函数:
extract_tokens_from_entry()process_jsonl_file()_merge_messages_by_id()
字段映射
input_tokens来自input_tokensoutput_tokens来自output_tokenscache_read_tokens来自cache_read_input_tokenscache_creation_tokens来自cache_creation_input_tokenstokens_used按input + output + cache_read + cache_creation计算
为什么要 merge message id
- Claude 同一个逻辑消息可能被拆成多行写入 JSONL
- 如果逐行直接入库,既会重复算 token,也会丢结构化内容
- 当前做法是先按逻辑
message_id合并,再归入daily_messages/session_messages
request_count
- 按 assistant 逻辑消息计数
- 有稳定 message id 时去重
- 即使某条 assistant 消息 token 为 0,也可能仍计为一次请求
3.2 Codex
源数据
- 路径:
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl - 脚本:
scripts/fetch_codex.py
关键事件
task_startedturn_contextresponse_itemtoken_counttask_complete
抓取方式
- Codex 不按“消息自带 usage”存账单数据,而是通过事件流重建 turn
task_started建 turnresponse_item收集 user / assistant messagetoken_count累加该 turn 的 token 使用task_complete结束该 turn
为什么按 turn,而不是按 session
- 一个 Codex session 可能跨多个小时甚至多天
- 如果把整场 session token 挂到第一条 assistant message,会扭曲每日/每小时统计
- 当前逻辑按 turn 重建,再把每个 turn 的 token 归给发起该 turn 的 user message
字段映射
tokens_used来自last_token_usage.total_tokens的逐事件累加值cache_tokens来自cached_input_tokensinput_tokens按input_tokens - cached_input_tokens计算output_tokens来自output_tokensthoughts_tokens只在抓取阶段参与日汇总,不单独入daily_messages
重要语义
- 当前本地 Codex 源日志中,
token_count.last_token_usage表现为事件级增量,应累加 cached_input_tokens是 provider total 的组成部分,不是额外再加的一层
request_count
- 按
task_started计数 - 不是按 assistant message 数量计数
重抓为什么会先删旧消息
- token 归因可能因为 parser 修复从 assistant message 挪到 user message
delete_messages_for_agent_sessions()会先按tool_name + host_name + agent_session_id清理旧行,再整体重写- 这样可以避免历史脏行残留造成双算
3.3 ZCode
源数据
- 路径:
~/.zcode/cli/db/db.sqlite - 脚本:
scripts/fetch_zcode.py - 原始表:
session、message、part、turn_usage
为什么和其他 3 个工具不同
- ZCode 原始源不是 JSONL,而是 SQLite 关系型结构
message/part更适合拿 transcriptturn_usage才是 token 统计的权威来源
抓取方式
- 用
remote-agent/session_sync.py里的ZcodeSession解析消息和项目路径 - 再单独查
turn_usage做 token 归因 - 关键函数:
_get_turn_usage_rows()_get_turn_usage_by_date()process_zcode_session()
字段映射
tokens_used来自computed_total_tokensinput_tokens来自turn_usage.input_tokensoutput_tokens来自turn_usage.output_tokenscache_tokens按cache_creation_input_tokens + cache_read_input_tokens计算
为什么按 turn_usage.started_at 分日期
- 一个 session 可能跨日
- ZCode 的 token 权威时间戳在
turn_usage.started_at - 所以
daily_usage的分日逻辑应基于 turn,而不是 session 的创建时间或“消息最多的那一天”
消息级归因
- 每个 turn 会优先归给
turn_usage.user_message_id对应的 user message - 这样
daily_messages/hourly_stats才能和真实 turn 发起时间对齐
匹配失败时怎么办
- 部分 turn 匹配失败:会打印告警
daily_usage仍然可信- 但
daily_messages/agent_sessions可能缺少部分 turn token
- 全部 turn 匹配失败:会退回旧逻辑,把整场 session token 注入第一条 assistant message,并打印 fallback 告警
3.4 Qwen
源数据
- 路径:
~/.qwen/projects/**/chats/*.jsonl - 兼容某些旧布局下的直接
*.jsonl - 脚本:
scripts/fetch_qwen.py
抓取方式
- usage 来自
usageMetadata - 入口函数:
extract_tokens_from_entry()process_jsonl_file()
字段映射
prompt_tokens来自promptTokenCountcandidates_tokens来自candidatesTokenCountthoughts_tokens来自thoughtsTokenCountcached_tokens来自cachedContentTokenCounttokens_used来自totalTokenCount
重要语义
promptTokenCount包含 cache- Open ACE 会额外计算
actual_input_tokens = promptTokenCount - cachedContentTokenCount - 最终写入
daily_messages.input_tokens的是actual_input_tokens tokens_used仍保留 providertotalTokenCount口径
为什么 thoughts 不再额外加进 total
- 当前逻辑把
thoughtsTokenCount视为额外观测维度,而不是一定要叠加进totalTokenCount - 否则会把 provider 已经给出的 total 再重复放大
request_count
- 按 assistant message 计数
- 有 message id 时做去重
4. 如何落库
4.1 daily_usage
职责
- 每日、每工具、每主机的聚合事实表
- 适合做趋势、总量、配额、ROI、成本估算
写入入口
scripts/shared/db.py的save_usage()
主要列
datetool_namehost_nametokens_usedinput_tokensoutput_tokenscache_tokensrequest_countmodels_used
4.2 daily_messages
职责
- 消息级分析事实表
- 适合做小时统计、时间线、sender 归属、conversation/project 维度分析
写入入口
scripts/shared/db.py的save_messages_batch()
重要限制
- 没有单独的
cache_tokens列 - 它不是 Workspace 运行时 transcript 权威表,更多是分析事实表
- 直接
SUM(tokens_used)时必须先确认工具归因语义,不要想当然地把它当成“官方账单逐条镜像”
4.3 agent_sessions
职责
- session 级摘要
- Work 模式 / 远程会话列表 / session 详情头部统计
更新入口
- 各 fetcher 的
update_agent_sessions_stats()
特点
- 聚合
message_count - 聚合
total_tokens - 聚合
request_count - 记录
model、project_path、updated_at
4.4 session_messages
职责
- session 详情页使用的 transcript 镜像
- 便于从 fetcher 导入后直接按 session 回放
更新入口
- 各 fetcher 的
update_agent_sessions_stats()在更新 session 摘要时顺带插入
4.5 daily_stats / hourly_stats
职责
- 从
daily_messages派生出的预聚合表
刷新机制
save_messages_batch()成功后会调用_refresh_daily_stats_for_messages(messages)- 它会按受影响日期重建:
daily_statshourly_stats
4.6 user_daily_stats
职责
- 面向用户维度的日聚合表
- 供配额、趋势和某些快速查询路径使用
刷新机制
save_messages_batch()完成后,会经scripts/shared/user_stats_helper.py- 调用
app/services/user_stats_aggregator.py - 把
daily_messages和agent_sessions汇总到user_daily_stats
5. 下游如何读取这些数据
| 模块 | 主要读取层 | 说明 |
|---|---|---|
app/repositories/usage_repo.py | daily_usage 优先 | 总量、按工具统计、CSV、请求数等优先读聚合表,避免 JOIN daily_messages 放大 request_count |
app/repositories/message_repo.py | daily_messages | 小时模式、时间线、消息检索、按 sender / project / conversation 分析 |
app/services/analysis_service.py | message_repo + hourly_stats | 部分趋势直接走消息聚合,部分按日小时视图走预聚合表 |
app/services/user_stats_aggregator.py | daily_messages + agent_sessions | 生成用户日级摘要 |
| Work 模式 session 详情 | agent_sessions + session_messages | 不是直接从 daily_messages 读 |
| Manage 模式 usage / analysis | daily_usage、daily_messages、daily_stats、hourly_stats | 取决于页面是看总量、趋势还是明细 |
可操作的经验规则:
- 看“每天总共用了多少 token”,先查
daily_usage - 看“某个小时发生了什么”,查
daily_messages或hourly_stats - 看“某个 session 的 transcript”,查
session_messages - 看“某个用户今天已用多少”,优先查
user_daily_stats
6. 常见误区
6.1 为什么 Open ACE 和官方控制台不完全一致?
常见原因:
- 官方与本地日志的刷新延迟不同
- 官方统计可能带有本地日志中没有落下来的模型桶
- session / turn / message 的归因口径不同
- 某些 provider 的 total 是否含 cache / thoughts 语义不同
这类差异不一定是 bug,先看差异属于哪一层:
- 源日志缺失
- fetcher 归因不同
daily_usage与daily_messages被拿去做了不同用途的对比