ccusage Pi 适配器深度解析:从 pi-agent 会话存储到用量报表的完整链路
【免费下载链接】ccusagenpx ccusage项目地址: https://gitcode.com/gh_mirrors/cc/ccusage
本指南以 ccusage 仓库中的 Pi 适配器(ccusage-adapter-pi)为主体,系统讲解它如何将 pi-agent 的会话存储(含配置文件声明的命名存储)转换为报表所需的用量条目(usage entries),涵盖数据源定位、模块架构、回放去重、Token 解析计价与报告输出的完整链路。读完本文,你将掌握 Pi 数据源目录约定、pi.stores配置规则、去重与父子会话回放抑制原理,以及ccusage pi系列命令的底层实现依据。
Pi 适配器的定位与职责边界
Pi 适配器位于 rust/adapters/pi,是 ccusage 众多 agent 源适配器之一。它的职责非常聚焦:把 pi-agent 的存储(store)转换成报表渲染所消费的用量条目。其中"存储"既包括 pi-agent 默认写入的会话目录,也包括用户在ccusage.json中通过pi.stores[]声明的其他命名目录。
从源码结构看,该 crate 严格遵循"仅处理本源特有逻辑"的边界:任何与 Pi 源无关的通用能力(时间日期分组、价格计算、模型别名解析等)都归属于ccusage-core或ccusage-adapter-common,适配器只保留本源的差异部分。这一点在 rust/adapters/pi/README.md 的 Owns 一节有明确声明,也是理解后续模块划分的总纲。
四个自有模块的分工
Pi 适配器将自身逻辑拆分为四个文件,各司其职:
| 模块 | 职责 | 对应源码 |
|---|---|---|
loader.rs | 数据源读取、去重(dedupe)、日期过滤 | rust/adapters/pi/src/loader.rs |
parser.rs | 原始记录解析、Token 映射、模型命名 | rust/adapters/pi/src/parser.rs |
paths.rs | 环境变量、默认目录、文件发现 | rust/adapters/pi/src/paths.rs |
report.rs | 与共享形状不同的 JSON 与表格输出 | rust/adapters/pi/src/report.rs |
对外公开的 API 面
适配器通过 rust/adapters/pi/src/lib.rs 暴露以下公共函数,供上层 CLI 与统一加载器(all-agent 报表)调用:
loader::load_entries— 加载默认 Pi 存储的条目loader::load_entries_for_store_path— 按单个存储路径加载loader::load_entries_for_store_paths— 按多个存储路径加载(命名存储走此路径)paths::named_store_paths— 解析配置中的命名存储路径paths::paths as default_paths— 解析默认 Pi 会话路径report::report_from_rows— 由汇总行生成 JSON 报表report::summarize_entries— 将条目汇总为日报/月报/周报/会话报告行run— 适配器级入口,串联定价加载、加载、过滤、汇总、输出全流程
数据源:环境变量、默认目录与命名存储
Pi 适配器的数据源定位遵循一个清晰的优先级链,实现在 rust/adapters/pi/src/paths.rs:
${PI_AGENT_DIR:-~/.pi/agent/sessions} + ccusage.json 中的 pi.stores[] 条目具体解析顺序(paths::paths):
- 命令行参数优先:若传入
--pi-path且非空,则将其作为路径列表使用; - 环境变量次之:读取
PI_AGENT_DIR环境变量,非空则使用其值; - 默认目录兜底:取
~/.pi/agent/sessions(通过 ccusage 的 home 目录工具定位),仅当该目录存在时返回。
值得注意的细节:--pi-path与PI_AGENT_DIR都刻意不展开~(沿用其历史语义),而命名存储路径则会像其他配置文件路径一样展开~。路径列表支持逗号分隔,且会自动去重、过滤不存在的目录——例如" /a, /b, /a, /missing "最终只得到[/a, /b]。
命名存储pi.stores[]
配置文件ccusage.json中可声明额外的 pi 格式会话目录。该配置项的 schema 定义在 rust/crates/ccusage-config/src/config_schema.rs,结构为:
{ "pi": { "stores": [ { "name": "omp", "path": "~/.omp/agent/sessions" }, { "name": "o3-fork", "path": "/data/pi-o3/sessions" } ] } }每个条目必须包含字符串字段name与path,校验规则(见 rust/crates/ccusage-config/src/config.rs 的parse_named_pi_stores):
name必须匹配^[a-z][a-z0-9_-]{0,31}$(小写字母开头,最长 32 字符);name不能与内置 agent 名称冲突(如pi、codex、all等均为保留名);name不能重复;path必须是非空字符串。
空数组"stores": []视为无操作。校验错误只对真正读取这些存储的命令生效(command_uses_named_pi_stores门控),不会拖累其他 agent 的命令执行。
命名存储的模型命名与去重身份
当通过命名存储加载时,解析出的模型名会被加上[store_name]前缀,例如原始模型gpt-5在omp存储中显示为[omp] gpt-5;同时去重身份(entry_id_for_store)也会把 store 名纳入拼接键,因此同名会话、相同时间戳的记录在不同存储之间不会被误判为重复(rust/adapters/pi/src/parser.rs 中entry_id_for_store与对应测试includes_named_store_in_dedupe_identity可验证)。
加载链路:并行读取、去重与日期过滤
加载的核心实现在loader.rs的load_entries_from_paths,整体流程为:
- 解析时区(
parse_tz,支持--timezone); - 对每个路径递归收集
.jsonl文件(collect_files_with_extension); - 过滤掉
subagent-artifacts/路径段下的派生调试转录文件; - 通过
read_files_parallel并行读取(受--single-thread开关控制),读取失败的文件仅记录 debug 日志,不中断整体加载; - 计算回放抑制计划(
PiReplayPlan),按计划跳过子会话中与父会话回放匹配的前缀; - 按 entry id 首见去重(first-wins),最后按时间戳排序输出。
回放抑制(Replay Suppression)原理
pi-agent 的分叉会话(forked session)文件可能会把父会话的用量历史完整"回放"进自己的文件里,如果直接全部计数会造成严重重复。loader.rs中的PiReplayPlan专门处理这一问题,其核心逻辑值得展开:
- 父候选路径:对 pi 的树形格式会话,父候选按"root 到 leaf 的活跃分支"确定,即从最终物理条目(当前叶子)沿
parentId链回溯,而不是简单地按 JSONL 物理顺序取上一行;被废弃的兄弟分支、断开的根分支仍然留在父会话中计数; - 候选范围:只取父会话中时间戳不晚于子会话 fork 时间戳(
fork_timestamp)的原始记录; - 抑制粒度:只删除与候选前缀完全匹配的前缀,匹配内容包括时间戳、模型、全部 token 字段、有效的 total-token 回退值,以及 Display/Auto 模式下的有效计费成本;第一个不匹配的条目及其后的所有记录都保留在子会话中;
- 宽松失败:父文件缺失、损坏、自引用或成环的血统关系保持原样,不投机地丢弃任何用量(对应测试
fails_open_for_missing_malformed_and_unrelated_same_token_sessions与fails_open_for_self_referential_and_cyclic_sessions)。
此外,replay_billed_cost_bits将成本编码为位模式参与签名比较:Calculate 模式下不计入签名,Display/Auto 模式下+0.0与-0.0视为相等,缺失与 0 成本也视为相等,避免浮点符号差异引发误判。
跳过 subagent 派生转录
pi-subagents 会在会话树内写入subagent-artifacts/目录,存放派生调试转录文件。这些文件是主会话文件中已记录调用的纯拷贝(无会话头、无 entry id),既无法被回放抑制覆盖,也无法靠 entry id 去重,因此加载器直接跳过任何路径段包含subagent-artifacts/的.jsonl文件。而run-*/全新上下文(fresh-context)子会话是这些子代理的主要记录,必须继续计入用量——skips_subagent_artifact_transcripts_but_keeps_fresh_context_child_sessions测试明确验证了这一区分。
解析与计价:Pi 记录结构、Token 字段与成本模式
parser.rs定义了 Pi 会话记录的 serde 结构。ccusage 只声明自己消费的字段,其余字段由 serde 自动忽略(见PiLine、PiMessage、PiUsage、PiCost)。
一条典型的 pi 用量记录形如:
{"type":"message","timestamp":"2026-01-02T00:00:00.000Z","message":{"role":"assistant","model":"gpt-5","usage":{"input":1000,"output":2000,"cacheRead":300,"cacheWrite":50,"totalTokens":3350,"cost":{"total":0.05}}}}解析与计价的关键规则:
- 行级预过滤:只有同时包含
"usage"与"message"两个子串的行才进入 JSON 解析(LinePrefilter),大幅降低非用量行的解析开销; - 有效记录判定:
type必须为message、message.role必须为assistant且携带usage对象,同时时间戳可解析、总 token 数非零(total_usage_tokens + extra_total_tokens > 0); - Token 字段映射:
input→ 输入 token、output→ 输出 token、cacheRead→ 缓存读取、cacheWrite→ 缓存写入(对应核心的 cache-creation 语义)、totalTokens→ 总量。当各分项缺失时,使用totalTokens作为回退(apply_total_token_fallback),测试falls_back_to_total_tokens_when_pi_parts_are_missing验证了仅含totalTokens:333的记录被解析为 output 333 的行为; - 宽松容错:token 字段用
lenient_u64解析,cost字段用lenient_object解析——即使cost不是对象(如"cost":0),记录也不会整行失败,只是把显示成本视为缺失(keeps_record_when_cost_is_not_an_object); - 成本来源:可选的
cost.total作为显示成本(display cost)。三种成本模式的取舍在calculate_store_cost/source_cost_for_mode中实现:Display:直接使用源文件中的显示成本;Auto:显示成本为有限且非负时使用之,否则按 token 与定价重新计算(calculate_store_cost_from_tokens);Calculate:忽略显示成本,始终按定价从 token 计算。
- 缺失定价提示:在需要定价却找不到模型的场景下设置
missing_pricing_model(默认存储记[pi] model形式,命名存储记原始模型名),仅在 Display 模式或 Auto 模式已有显示成本时不告警。
项目归属推断
会话文件的目录结构决定了条目的 project 归属:extract_project扫描路径中sessions段之后的第一个目录段作为项目名;命名存储则优先基于存储根目录取相对路径首段,失败时回退到默认推断。会话 ID 取自文件名:形如agent_session-a.jsonl的文件,取下划线后段session-a(extract_session_id)。
报表输出:四种粒度的汇总与 JSON 形状
report.rs负责把加载后的条目汇总为报表行,再组装成 JSON:
summarize_entries按AgentReportKind分派:Daily:按entry.date聚合(时区已在前置阶段应用);Monthly:先按日聚合,再按BucketKind::Monthly、WeekDay::Sunday周期桶化;Weekly:同样基于日汇总做周桶化;Session:按session_id分组,用SessionAccumulator累积每条会话的元数据与用量。
report_from_rows输出统一形状:{ "<daily|weekly|monthly|sessions>": [...], "totals": {...} },其中totals汇总inputTokens、outputTokens、cacheCreationTokens、cacheReadTokens、totalTokens与totalCost(空报表也保证输出零值 totals,见empty_report_has_zero_totals_object测试)。
完整的命令入口在lib.rs的run:加载定价覆盖 → 加载条目 → 按共享参数过滤日期 → 汇总 → 排序(--order与时间周期键)→ 按需输出 JSON(支持--jq后处理与--no-cost)或渲染pi-agent Token Usage Report表格。
使用方式与依赖边界
pi-agent 源的标准命令(见 rust/adapters/pi/src/README.md):
ccusage pi daily ccusage pi monthly ccusage pi session ccusage pi daily --json ccusage pi daily --pi-path /path/to/sessions其中--pi-path对应配置中的pi.defaults.piPath(见 rust/crates/ccusage-config/src/config_schema.rs),支持逗号分隔的多个路径。若某模型未收录在默认定价表中,可像 ccusage.example.json 那样按带前缀的模型名配置覆盖,例如:
{ "pricingOverrides": { "[pi] gpt-5.4": { "input_cost_per_token": 0.000002, "output_cost_per_token": 0.000008 } } }依赖方面,rust/adapters/pi/Cargo.toml 声明了四个运行时依赖:ccusage-adapter-common(JSONL 解析、文件遍历、并行读取)、ccusage-core(类型、定价、汇总、输出)、jiff(时区/时间处理)与serde/serde_json。构建上,该适配器位于adaptersCrane 产物层,与其余适配器在同一次 Cargo 调用中并发编译。
测试保障
适配器的行为由 rust/adapters/pi/src/loader.rs 与 rust/adapters/pi/src/parser.rs 内置的测试覆盖,重点场景包括:
- 父子回放前缀抑制与活跃分支识别(含嵌套 fork、废弃兄弟分支、多断开根分支);
- 回放签名的字段级比较(任一 token 字段、模型、total 回退变化都不再抑制);
- 显示成本差异不影响回放判定(Display/Auto 模式保留子会话成本);
- 签名 ±0、缺失/0 成本视为等价;
- 自引用与循环血统宽松失败、命名存储跨目录匹配父文件、subagent-artifacts 跳过等边界。
这些测试同时以single_thread开关验证并行与单线程路径结果一致,保证了去重顺序的确定性。
【免费下载链接】ccusagenpx ccusage项目地址: https://gitcode.com/gh_mirrors/cc/ccusage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考