用 Litefuse 追踪 OpenCode
OpenCode 是一个开源的终端编码 Agent,采用客户端/服务端架构、运行在 Bun 上。本集成是一个 OpenCode 原生插件,安装在 ~/.config/opencode/plugin/litefuse.ts —— 单个 TypeScript 文件,零 npm 依赖。把它放进插件目录即自动加载,无需改 opencode.jsonc。
插件运行在 OpenCode 服务端进程内,订阅它的事件总线与若干专用 hook(chat.message、message.part.updated 的 step-start/step-finish、tool.execute.before/after、session.idle),为每个用户回合生成一条 Litefuse trace,使用真实的逐事件时间戳。Span 以裸 OTLP/HTTP JSON 形式、用内置 fetch 直接发往 Litefuse 的 OTLP 端点 —— 没有 SDK、没有 node_modules、不需要构建步骤(OpenCode 直接加载 TypeScript)。
给 AI —— 自动安装
如果你正在和 OpenCode 对话,把下面这段 prompt 粘贴过去,Agent 会端到端完成整个安装:
Read https://litefuse.ai/SKILL.md and follow the instructions to install and configure Litefuse for OpenCode.
skill 会向你索取 Litefuse 的 API Key(如果还没有账号,会引导你注册),然后在本地完成全部配置。如果你想手工一步步配置,请继续往下看。
会捕获哪些数据
| 数据 | 捕获方式 | 备注 |
|---|---|---|
| 用户 prompt | trace input | 文本;图片 block 数量记入 metadata |
| 每次 LLM API 调用 | generation observation | plan (n tools) #N / response / think #N,按模型这一步做了什么命名 |
| thinking / text / toolCall block | generation output | 保留块结构,含 reasoning 文本 |
| 首 token 时间 | generation 的 completion_start_time | 来自第一个流式 part —— 驱动 Litefuse 的 TTFT 指标 |
| 采样参数 | generation 的 model_parameters | temperature / topP / topK / maxOutputTokens,模型设置了才采集 |
token 用量(input、output、cache_read_input_tokens、cache_creation_input_tokens) | generation 的 usage_details | Anthropic 风格 key,来自 OpenCode 每步的 step-finish |
| 模型名 + OpenCode 自带成本 | generation 的 model + cost_details | OpenCode 给出权威成本,覆盖服务端价格表 |
| 工具执行 | tool observation | tool: bash (grep) #N、tool: read (index.ts) #N —— 名字带关键信息,完整参数在 input |
子 agent(task 工具) | 子树 | tool (1 subagent) #N → subagent 容器 → 子 agent 自己的 plan/tool/response 步骤;子成本汇入父 trace |
| 工具错误 | tool observation,level=ERROR | 来自 tool part 的 error 终态,带 status_message 预览 |
| LLM 错误 / 中止 | generation level=ERROR / WARNING | 来自 assistant 消息的 error 字段 |
| 上下文压缩 | context compaction event | 解释下一次调用 input token 骤降 |
| 没有最终回答收尾的回合 | 根 span level=WARNING | 在工具循环中被杀或中断 |
| 会话分组 | trace 的 session_id | OpenCode 会话 id(ses_…);续接会话 Turn 编号接着数 |
| 用户身份 | trace 的 user_id | $LITEFUSE_USER_ID,回退到系统用户名 |
| 标签 | trace 的 tags | opencode + model:<name> |
Trace 结构
一个委派了子 agent 的典型回合产生的 trace 形如(真实示例):
opencode — Turn 1 (AGENT root —— 真实回合时长)
├── plan (1 tool) #1 (generation,usage_details + 成本,真实延迟)
├── tool (1 subagent) #2 (tool —— 父进程视角的委派)
│ └── subagent (AGENT 容器 —— 子会话)
│ ├── plan (1 tool) #1 (容器内局部编号,从 #1 重计)
│ ├── tool: read (note.txt) #2
│ └── subagent response (generation —— 子 agent 的最终回答)
└── response (generation —— 最终回答,结束回合)设计说明:
- Generation 按”模型这一步做了什么”命名,而不是用哪个模型(
plan (2 tools)、response、think)—— 模型名是 generation 的model属性,换模型不会破坏看板。OpenCode 把一次 LLM 调用表示成一对step-start/step-finishpart,插件据此切分 generation,token 与成本取自step-finish。 - Generation 延迟不含工具时间。 OpenCode 的
step-finish在该步工具执行之后才发,因此插件把 generation 的结束时间取在最后一个流式内容片到达的时刻(≈ LLM 真正返回的时刻),把工具耗时排除在 LLM 延迟之外 —— token/成本仍取自权威的step-finish。 - 每个 agent 容器一个步骤计数器:
#N由 generation 和 tool 按时间顺序共用。tool 的 metadataagent_plan_step指向发起它的 plan 的agent_step_index—— join 条件是tool.agent_plan_step == generation.agent_step_index。每个子 agent 容器从#1重新计数,层级由树结构表达。 - 子 agent 子树。 OpenCode 是单服务端进程,一个插件实例能看到所有会话的事件 —— 包括
task工具派生的子会话。因此子树无需跨进程的 traceparent 协议:插件用子会话的parentID把它绑到父进程仍在执行的task工具 span 上,形成tool (1 subagent)→subagent容器 → 子步骤的三层结构。工具 span 包裹容器是有意设计:tool span 时长 − 容器时长 = 委派的真实开销(子会话启动、结果回收)。 - 每个 span 恰好发送一次、在它结束时 —— OTel 的 span 不可变;没有临时发送、没有 upsert。trace header(name / session / user / input / tags)随每个 span 携带,所以第一个 observation(通常是几秒内完成的
plan #1)一完成,trace 就出现在 Litefuse 里 —— 长回合执行中即可见。 - 真实 wall-clock 时间戳,来自每个事件触发的时刻 —— 时间线反映真实的 LLM 延迟、工具耗时与间隔。
- 打平的
agent_*metadata:所有 OpenCode 专属字段都是带统一前缀的顶层 metadata key(agent_step_index、agent_plan_step、agent_duration_ms……)—— 同一个看板过滤条件对所有 Litefuse agent 集成通用。稀疏存储:没有值的字段完全不出现,不用null占位。 - Fail-open:任何意外错误写入
~/.config/opencode/litefuse.log后静默吞掉 —— 插件永不阻塞 OpenCode。不可达的目标直接跳过。
快速开始
前置条件
- 已安装 OpenCode —— 用
opencode --version检查。 curl在PATH上(macOS / Linux 默认都有)。插件用它同步投递每个回合的根 span,详见工作原理。- 在 https://litefuse.cloud 创建一个 Litefuse 项目,拿到 public + secret key。
没有其他依赖:插件是只用 Node 内置模块的单文件,OpenCode 直接加载 TypeScript。
下载插件
mkdir -p ~/.config/opencode/plugin
curl -fsSL https://litefuse.ai/integrations/opencode/litefuse.ts \
-o ~/.config/opencode/plugin/litefuse.ts源码也在同一 URL 上 —— 部署前可以先读一遍。~/.config/opencode/plugin/ 下的文件会被 OpenCode 自动加载,无需改 opencode.jsonc。
配置凭据
创建 ~/.config/opencode/litefuse-targets.json(不需要改 shell 配置文件):
cat > ~/.config/opencode/litefuse-targets.json <<'EOF'
[
{
"publicKey": "pk-lf-xxx",
"secretKey": "sk-lf-xxx",
"baseUrl": "https://litefuse.cloud",
"environment": "production"
}
]
EOF
chmod 600 ~/.config/opencode/litefuse-targets.json把占位值替换成真实值。也可以改用环境变量 LITEFUSE_PUBLIC_KEY / LITEFUSE_SECRET_KEY / LITEFUSE_BASE_URL(同名 LANGFUSE_* 作为 fallback 也接受),在启动 OpenCode 的 shell 里 export 即可。targets 文件支持多个目标 —— 同一份 trace 会写入每一个 Litefuse 实例,各目标可配置独立的 environment。
验证
OPENCODE_LITEFUSE_DEBUG=true opencode run "Reply with exactly: ok"
tail -3 ~/.config/opencode/litefuse.log
# 期望: "plugin loaded, 1 target(s): https://litefuse.cloud (production)"
# + "turn complete session=... turn=1 api=1 tools=0"打开 https://litefuse.cloud —— 最新一条 trace 名为 opencode — Turn 1,结构如上所示。
OpenCode 在启动时从插件目录加载插件:装好插件或改完凭据后,新启动的 opencode run 或新开的会话即自动生效;已经开着的 TUI 里新建一个会话即可。
如果没有配置任何 key,插件会保持空闲(零开销),不发送任何数据。
环境变量
LITEFUSE_* 变量优先;同名 LANGFUSE_* 变量作为生态兼容的 fallback 也接受。
| 变量 | 必填 | 说明 |
|---|---|---|
LITEFUSE_PUBLIC_KEY | 是* | Litefuse 项目 public key(pk-lf-...)。 |
LITEFUSE_SECRET_KEY | 是* | Litefuse 项目 secret key(sk-lf-...)。 |
LITEFUSE_BASE_URL | 否 | 默认 https://litefuse.cloud。别名 LITEFUSE_HOST。 |
LITEFUSE_TRACING_ENVIRONMENT | 否 | env 配置目标的 trace environment。默认 production;做实验用 development,免得污染生产看板。 |
LITEFUSE_USER_ID | 否 | 覆盖 trace 的 user_id。回退到系统用户名。 |
LITEFUSE_EXTRA_TARGETS | 否 | 额外目标的 JSON 数组(与 targets 文件同构)。 |
OPENCODE_LITEFUSE_DEBUG | 否 | 设为 "true" 启用 ~/.config/opencode/litefuse.log 的详细日志。 |
OPENCODE_LITEFUSE_MAX_CHARS | 否 | span 输入/输出的截断阈值(字符数)。默认 1000000(约 1MB 文本)。 |
* 仅在不使用 ~/.config/opencode/litefuse-targets.json 时必填。targets 文件里的凭据无需任何环境变量即可工作。
Trace metadata 参考
所有 OpenCode 专属字段都是带 agent_ 前缀的打平顶层 metadata key —— 所有 Litefuse agent 集成共用同一套 key,同样的过滤器和看板处处可用。Litefuse 标准字段(sessionId、userId、tags)通过 OTel 属性保持在 trace 级。稀疏存储:没有值的字段完全不出现,绝不用 null。
Trace 级:
agent_turn_number、agent_session_id、agent_cwd、agent_model、agent_provideragent_api_calls、agent_tool_calls、agent_steps、agent_duration_ms—— 回合统计agent_image_blocks、agent_prompt_truncated、agent_prompt_orig_len—— 适用时出现
Generation observation:
agent_step_index—— 名字里的#Nagent_provider、agent_stop_reasonagent_api_duration_ms、agent_time_to_first_token_msagent_tool_call_count、agent_reasoning_tokens、agent_thinking_charsagent_input_truncated、agent_output_truncated、agent_output_orig_len—— 仅截断时出现
Tool observation:
agent_tool_name、agent_tool_call_idagent_step_index—— 工具自己的#Nagent_plan_step—— 发起本工具的 plan generation 的agent_step_indexagent_duration_ms、agent_is_erroragent_details—— OpenCode 的结构化工具结果摘要(退出码、标题等),≤ 2 KB 时以对象内嵌;过大则整体省略并记agent_details_omitted_len
Subagent 容器:
agent_subagent: true,外加与 trace 级相同的单次运行统计(agent_api_calls、agent_tool_calls、agent_steps、agent_duration_ms)
工作原理
插件订阅 OpenCode 的事件总线和几个专用 hook:
| 事件 / Hook | 用途 |
|---|---|
chat.message(user) | 开启回合:捕获 prompt;从会话历史恢复 Turn 编号 |
chat.params | 采样参数 → generation 的 model_parameters |
experimental.chat.messages.transform | 发往 provider 的完整 messages → generation 的 input(尽力而为) |
message.part.updated(step-start) | generation 开始 |
message.part.updated(text / reasoning / tool) | generation 输出块、TTFT、工具调用计数 |
message.part.updated(step-finish) | 发出 generation span:按内容命名,带 usage + 成本 |
tool.execute.before / tool.execute.after | tool span;task 工具登记为子树容器的父 |
message.part.updated(tool error 终态) | 工具失败 → level=ERROR(权威来源) |
session.compacted | 上下文压缩 event observation |
message.updated(assistant) | 模型 / provider / 错误信息 |
session.idle | 发出 root span:trace output、回合统计;flush |
回合内每个完成的 span 立即以 OTLP/HTTP JSON、fire-and-forget 加超时 POST 到 <baseUrl>/api/public/otel/v1/traces(Basic auth)。每个 span 只发送一次;执行中的步骤在完成时变为可见。
根 span 同步投递。 OpenCode 的 headless run 在 session.idle 后会立刻退出,不等待插件的事件处理器完成 —— 此时根 span(回合最后发出的 span)的异步发送会与进程拆毁竞速而丢失。为此插件用一次同步 curl(阻塞 JS 线程直到投递完成)发出回合结束的根 span 批次,保证它一定落库;回合内其他 span 仍走异步、不阻塞。这就是前置条件里需要 curl 的原因。
对于子 agent,OpenCode 的 task 工具会派生一个子会话。子会话的事件流经同一个插件实例;插件用子会话的 parentID 把它绑到父进程仍在执行的 task 工具 span 下,生成完整子树。子 agent 的 generation 各自带 usage,成本自动汇入父 trace 总成本。一个已知暂态:live 视图里子 agent 的步骤可能先于其容器出现(容器在子会话结束时才发送)—— 回合结束后树完整且嵌套正确。
插件是 fail-open 的:任何意外错误只写 ~/.config/opencode/litefuse.log 并静默返回,绝不阻塞或拖慢 OpenCode。
故障排查
Litefuse 中没有出现 trace。 查看插件日志:
tail -20 ~/.config/opencode/litefuse.log日志为空说明插件没加载 —— 确认文件在 ~/.config/opencode/plugin/litefuse.ts。文件存在但没有 plugin loaded 行?插件没找到凭据:检查 ~/.config/opencode/litefuse-targets.json 或 LITEFUSE_* 环境变量。有 sendSync failed: 或 send failed: 说明 key 或网络问题:核对 key 与 LITEFUSE_BASE_URL。
根 span 缺失(trace 有子节点但没有 AGENT 根)。 几乎都是 curl 不在 PATH 上 —— 插件靠它同步投递根 span。日志里会有 sendSync failed: ... exit=...。装上 curl 即可。
某个工具是失败的,但没标成 ERROR。 OpenCode 的 bash 工具把命令的非零退出当作成功完成(stderr 进 output、退出码进 metadata),不算工具级错误 —— 这符合 OpenCode 的语义。真正的工具错误(如 read 一个不存在的文件)会正确地标为 level=ERROR。
totalCost 是 0。 用的是免费模型(成本本就为 0),或 Litefuse 项目里没有该模型的价格条目。需要时在 Litefuse UI 的 Settings → Models 里添加 —— 不过 OpenCode 会自带成本数据,插件以 cost_details 转发,付费模型的成本会直接显示。
子 agent 没有再嵌套(孙 agent)。 OpenCode 的子会话目前不带 task 工具,所以子 agent 无法再委派 —— 插件的递归子树支持已就绪,等上游放开即可生效。
启用调试日志:
OPENCODE_LITEFUSE_DEBUG=true opencode
# 每次发送 / 跳过 / 错误都会写入 ~/.config/opencode/litefuse.log本地 + 云端双写: 在 litefuse-targets.json 里加第二个条目(例如本地自托管实例配 "environment": "test")。不可达的目标会被静默跳过,永不阻塞 OpenCode。