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清理旧行,再整体重写- 这样可以避免历史脏行残留造成双算