【免费下载链接】codeburn
Free, local tool to track AI coding token usage and cost across 37 tools and agents (Claude Code, Cursor, Codex, Gemini and more), by model, project, and task. npx codeburn
CodeBurn 是一款本地运行的 AI 编程用量与成本追踪工具,支持包括 OpenCode(sst/opencode)在内的数十种工具与 Agent。本文以 docs/providers/opencode.md 为核心,结合 src/providers/opencode.ts 等源码与 tests/providers/opencode.test.ts 测试,系统讲解 CodeBurn 如何发现并解析 OpenCode 会话数据:数据目录的解析优先级与 fork 适配环境变量、从文件 JSON 到 SQLite 再到 2.x 双代次的存储格式演进、按sessionId:messageId的去重策略,以及providerID驱动的计费路由与各类已知怪癖。读完本文,你将掌握为 OpenCode(或其兼容 fork,如 MiMoCode)正确配置 CodeBurn、排查解析告警并验证用量统计的完整方法。
一、Provider 定位:懒加载的 OpenCode 适配器
OpenCode 是 CodeBurn 中的"懒加载"Provider 之一:它不在进程启动时强制引入,而是由注册表按需加载。从 src/providers/index.ts 可以看到,opencode被列入lazyProviderNames与lazyProviderDisplayNames(显示名为OpenCode),loadOpenCode()通过动态import()在首次需要时加载 src/providers/opencode.ts,加载失败也不会拖垮整个发现流程。所有 Provider 的发现结果通过discoverAllSessionsWithFailures并发收集并做失败隔离——某个 Provider 的目录扫描异常只会打印一条codeburn: skipped ... discovery after an error警告,而不会清空其他 Provider 的用量数据。
Provider 对象本身在 src/providers/opencode.ts 中定义:probeRoots()返回数据目录作为探测根,discoverSessions()同时调用文件式与 SQLite 两套发现器并合并结果,createSessionParser()则按source.path是否以.json结尾,把解析委托给文件解析器或 SQLite 解析器。此外它还负责模型与工具的显示名映射:模型名会去掉model/形式的 provider 前缀后交给共享的getShortModelName(src/providers/opencode.ts),内置工具如bash/edit/task会被映射为Bash/Edit/Agent等可读名称。
二、数据从哪里读:目录解析优先级与环境变量
2.1 默认数据目录
OpenCode 的会话数据默认位于~/.local/share/opencode/,若设置了XDG_DATA_HOME则位于$XDG_DATA_HOME/opencode/。目录发现器会拾取该目录下的所有opencode*.db文件(SQLite 形态),以及storage/子目录(文件 JSON 形态)。目录解析逻辑集中在getDataDir()(src/providers/opencode.ts)。
当没有传入dataDir参数(即生产路径)时,优先级为:
OPENCODE_DATA_DIR → $XDG_DATA_HOME/opencode → ~/.local/share/opencodeOPENCODE_DATA_DIR是精确的数据目录——不会追加opencode后缀。这一点与测试桩路径不同:测试中传入的dataDir参数仍会被拼上opencode子目录(join(dataDir, 'opencode')),以保持既有测试固件不变(如tmpDir/opencode/opencode*.db)。
2.2 为 OpenCode 兼容 fork 重定向数据(OPENCODE_DATA_DIR / OPENCODE_DB_PREFIX)
OpenCode 生态中存在改名或 fork 的兼容构建,例如 MiMoCode 会把数据写到~/.local/share/mimocode/mimicode.db,但使用与 OpenCode 完全相同的session/message/part表结构。要让 CodeBurn 找到这类 fork 的数据,需要同时设置两个环境变量(对应 issue #617 的修复):
| 环境变量 | 含义 | 默认值 | 示例 |
|---|---|---|---|
OPENCODE_DATA_DIR | 精确的数据目录,不追加opencode后缀,同时重定向文件存储与 SQLite 存储 | 无(回落$XDG_DATA_HOME/opencode/~/.local/share/opencode) | OPENCODE_DATA_DIR=$HOME/.local/share/mimocode |
OPENCODE_DB_PREFIX | SQLite 文件名前缀,匹配前缀*.db;仅影响 SQLite 发现 | opencode | OPENCODE_DB_PREFIX=mimicode可发现mimicode*.db |
值得注意的细节是OPENCODE_DB_PREFIX的读取用的是真值判断而非空值合并(src/providers/opencode.ts):空字符串前缀会回退为opencode。若用??,空字符串会存活下来,导致discoverSqliteSessions用filename.startsWith('')匹配所有*.db文件,把无关数据库扫进发现流程。这与OPENCODE_DATA_DIR的真值处理保持一致,也让"未设置"与"设置为空"行为相同——这与环境指纹一致,因为computeEnvFingerprint会把两者都折叠为OPENCODE_DB_PREFIX=。
文件存储则不受OPENCODE_DB_PREFIX影响:只要OPENCODE_DATA_DIR指向了 fork 的数据目录,<数据目录>/storage/下的 JSON 文件就会被发现(src/providers/opencode.ts 中文件与 SQLite 两套发现并行执行)。
2.3 环境变量与缓存指纹
这两个变量连同XDG_DATA_HOME一起参与了 OpenCode 的会话缓存环境指纹(src/session-cache.ts)。也就是说,修改OPENCODE_DATA_DIR或OPENCODE_DB_PREFIX会改变缓存指纹,使热读与冷读结果保持一致,不会出现旧指纹下缓存命中导致数据不更新的问题。配置清单亦收录在 docs/configuration.md。
三、存储格式演进:文件 JSON、legacy SQLite 与 2.x 双代次
OpenCode 的历史版本使用三种互有重叠的存储形态,CodeBurn 全部兼容:
3.1 文件式 JSON(OpenCode 1.1+)
OpenCode 1.1 之后将会话存为文件 JSON,目录结构如下(见 src/providers/opencode-file-parser.ts 的注释):
storage/session/<projectID>/<sessionID>.json 会话元数据 storage/message/<sessionID>/<messageID>.json 每条消息一个文件 storage/part/<messageID>/<partID>.json 每个 part 一个文件消息/part 的结构与 SQLite 布局一致,因此每消息的构建逻辑通过共享的buildAssistantCall(src/providers/session-message.ts)复用。解析器按time.created排序消息,用户消息的文本会被记住并作为后续助手调用的userMessage归因;只对assistant/model角色产出调用。
3.2 legacy SQLite(session / message / part)
旧版 OpenCode 将数据写入opencode*.db,含session、message、part三张表。CodeBurn 以只读方式查询并按 LiteLLM 价格重新计算成本;对无价目模型则回退使用 OpenCode 自己写入的cost字段(docs/how-it-works.md)。message.data与part.data均为 JSON 载荷,providerID就存放在 assistant 消息的载荷中。
3.3 OpenCode 2.x:session_v2 + session_message(issue #1293)
OpenCode 2.x(主线自 2.0.3 起,issue #1293)写入第二套 SQLite 代次:session_v2+session_message。session_message的外键指向session_v2(id),消息由type列打标签、按seq排序、载荷 JSON 放在data列。原地升级后,legacy 的session/message/part表会冻结——升级后的会话在session_message中有行,而message表不再新增行。
解析器在 src/providers/sqlite-session-parser.ts 按数据库粒度通过sqlite_master探测:当session_v2与session_message存在时以 v2 为准,legacy 表被整体忽略(两代次永不 JOIN);否则走 legacy 路径。同时存在一个兼容细节:legacy 中那些 id 未进入session_v2的会话并未作废——它们仍以 legacy 读取器解析(src/providers/sqlite-session-parser.ts),避免升级后旧会话用量凭空消失。
会话级的成本/token 汇总与parent_id子会话遍历在两个代次中都存在,因此发现、解析与去重在两种 schema 下行为一致。对应的 v2 测试固件见 tests/providers/opencode.test.ts。
四、缓存与去重
- 缓存:OpenCode provider无独立缓存。每次发现都是直接扫描数据目录,冷启动即拿到最新数据。
- 去重:按
<sessionId>:<messageId>粒度进行。去重键在文件解析器里形如opencode:<sessionId>:<messageId>(src/providers/opencode-file-parser.ts),与 SQLite 路径共用同一seenKeys集合——这使"文件 + SQLite 双形态并存"的迁移期(升级后 legacy JSON 仍在磁盘、新数据流入 SQLite)不会重复计数,旧会话与新会话都能上报。两种形态的每消息构建均经buildAssistantCall共享,保证 token、工具、成本归因一致(src/providers/session-message.ts)。
五、计费路由:providerID 如何映射到账单
OpenCode 把传输通道(transport)记录在每条 assistant 消息的providerID字段里。CodeBurn 在 legacy SQLite、v2session_message、文件存储以及会话级汇总回退之间一致地保留这些承载用量语义的值,然后通过 src/models.ts 的routeFromProviderField映射为计费路由:
providerID值 | 路由 | 计费方式 |
|---|---|---|
openrouter | OpenRouter | 按量计费(metered) |
amazon-bedrock | Bedrock | 按量计费(metered) |
映射定义于 src/models.ts 的路由表:bedrock的providerFields包含bedrock与amazon-bedrock,openrouter的providerFields为openrouter。关键约束:
- 大小写或空白变体不做推断:若原始值是
OpenRouter或带首尾空格,routeFromProviderField会因value !== normalized而返回undefined,落入 unrouted/unknown。 - Bedrock 模型检测器仍是兜底:对于能被识别的 Anthropic / OpenAI 基础模型 id,旧有的 model-id 检测器依然生效;但
providerID=amazon-bedrock还覆盖了 Nova 等 id 与检测器不匹配的模型家族。 - 直连 provider 值(如
openai、anthropic)保持 unrouted/unknown,不强行猜测。 - 测试对两条路由均有断言:
openrouter与amazon-bedrock消息分别产出route: 'openrouter'与route: 'bedrock'(tests/providers/opencode.test.ts)。 - 共享字段指纹:由于 OpenCode 与 KiloCode 共用这套 provider 字段映射,映射一旦变化,两者的会话缓存指纹会同步移动,保证热读与冷读一致(src/session-cache.ts 中两 provider 共享相关环境指纹键)。
六、解析怪癖与语义细节
6.1 只发根会话,遍历整棵 parent_id 子树
OpenCode 的子 Agent(subtask)会话通过parent_id关联。为避免重复计数,发现阶段只输出根会话(parent_id IS NULL,见 src/providers/sqlite-session-parser.ts),解析根会话时再沿session.parent_id走完整棵子树——归档的子会话也包含在内。OpenCode 的归档是组织性的,行不会删除,因此子会话与孙会话的 message、token、工具用量都会归并回根会话。两个相关测试:归档会话可被发现(#1362),归档子会话计入根子树(#1362),断言子会话的去重键为opencode:archived-child:msg-child-assistant(tests/providers/opencode.test.ts、tests/providers/opencode.test.ts)。
6.2 parts 索引顺序决定推理 token 正确性
每条消息的parts会被建立索引,保持顺序对推理 token(reasoning tokens)的正确性至关重要。若出现"reasoning tokens 差一(off by one)"类 bug,优先排查 parts 索引排序逻辑。
6.3 token 维度与成本语义
token 按input、output、reasoning、cache.read、cache.write五个维度上报(Anthropic 语义)。推理 token 按输出价计费——会话级与每消息级回退都必须按 output + reasoning 计价而非只算 output(#1334,见 tests/providers/opencode.test.ts)。
6.4 零成本消息的取舍
- 缺 router usage 的 assistant 消息,只要 parts 包含非空文本或工具活动,就保留为零成本调用;
- 空的、零用量的 assistant 占位符仍然跳过;
- v2 代次中,
compaction类型消息携带 CompactionUsage(成本/token 在 compaction 行上),完成态 compaction 计入统计,运行中的 compaction(无用量)不产出任何调用(tests/providers/opencode.test.ts)。
6.5 MCP 工具名归一化
OpenCode 将外部 MCP 工具存为<server>_<tool>形式(例如clickup_clickup_get_task)。normalizeToolName(src/providers/session-message.ts)会将其归一化为 CodeBurn 的规范命名mcp__<server>__<tool>,使共享的 MCP 面板与optimize发现能够统计 OpenCode 的 MCP 用量;对已经是mcp__前缀的名字原样保留。测试断言clickup_clickup_get_task与figma_get_file分别归一化为mcp__clickup__clickup_get_task、mcp__figma__get_file(tests/providers/opencode.test.ts)。解析时还会从 bash 工具的state.input.command提取 bash 命令串,供命令类报表使用。
6.6 Schema 校验"吵一点"是正确行为
当必需表缺失时,解析器会打印一条可操作的警告,指明哪张表缺失以及期望的 OpenCode 版本。不要静默吞掉这类警告——它通常是升级后 schema 变化的第一信号。
6.7 源码路径编码与项目识别
会话源码路径编码为<dbPath>:<sessionId>(如/home/user/.local/share/opencode/opencode.db:ses_v2_1),测试固件也遵循该格式。项目名取自会话的directory或title,经sanitize处理(如/home/user/myproject→home-user-myproject)。
七、调试与测试指引
tests/providers/opencode.test.ts是当前仓库中最大的 provider 测试文件(已超过 1400 行),覆盖文件 JSON、legacy SQLite、v2 双代次、路由、MCP 归一化、归档子树、推理 token 计价等场景;另有 tests/providers/opencode-file.test.ts 专测文件存储形态。改动 OpenCode provider 前后的标准做法:
- 先跑全套测试:
npx vitest run tests/providers/opencode.test.ts,回归后再做改动。 - 若遇到 "missing table" 警告:不要 catch 后静默。要么升级解析器中的版本预期,要么在文档中记录该破坏性 schema 变更。
- 若遇到 "reasoning tokens off by one":检查 parts 索引排序。
- 若某个 OpenCode 兼容 fork(如 MiMoCode)扫出零会话:检查
OPENCODE_DATA_DIR是否精确指向 fork 的数据目录、OPENCODE_DB_PREFIX是否与*.db文件名前缀一致(注意该变量仅影响 SQLite 发现,文件存储不受其约束)。
八、小结
CodeBurn 对 OpenCode 的支持建立在三条支柱上:精确的目录解析(OPENCODE_DATA_DIR/OPENCODE_DB_PREFIX使其可适配任意兼容 fork)、三代存储形态的无缝兼容(文件 JSON、legacy SQLite、2.xsession_v2双代次并存不重不漏)、以及以providerID为准的计费路由(OpenRouter/Bedrock 按量计费,直连值保持未知)。理解这些机制后,无论你的 OpenCode 处于哪个版本,或使用了何种改名构建,都能让 CodeBurn 给出与官方文档一致的 token 与成本统计。若要深入实现细节,可继续阅读 src/providers/opencode.ts、src/providers/sqlite-session-parser.ts、src/providers/session-message.ts 及对应测试。
【免费下载链接】codeburn
Free, local tool to track AI coding token usage and cost across 37 tools and agents (Claude Code, Cursor, Codex, Gemini and more), by model, project, and task. npx codeburn
相关推荐
codeburn 中的 Gemini CLI 用量解析器:数据来源、存储格式、计费去重与调试指南
codeburn 中的 Gemini CLI 用量解析器:数据来源、存储格式、计费去重与调试指南 Gemini(Google Gemini CLI)是 code
CodeBurn 的 Kimi Code 会话解析指南:wire.jsonl 存储格式、Token 计量与去重原理
CodeBurn 的 Kimi Code 会话解析指南:wire.jsonl 存储格式、Token 计量与去重原理 本指南以 CodeBurn 仓库 https
OpenEBS存储数据流分析:追踪数据路径
OpenEBS存储数据流分析:追踪数据路径 引言 你还在为Kubernetes集群中的数据存储路径不透明而困扰吗?当应用数据在OpenEBS中流转时,你是否清楚
云原生CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考