Summary
Running @braintrust/trace-opencode@0.0.8 against opencode CLI 1.14.48 (the version bundled in cloudflare/sandbox:0.10.1-opencode), the plugin loads correctly and the root OpenCode: … span lands in Braintrust with a duration — but zero LLM spans are emitted for any turn. Braintrust dashboards show LLM calls: 0, Input: —, Output: — for every session.
Root cause
opencode 1.14.x emits a different event stream than the plugin handles. I captured a full event-receive histogram from the plugin's own event hook over a real ~25-minute session (5 opencode sub-sessions, lots of tool activity):
event_type count plugin handler?
───────────────────────────────── ───── ─────────────────────────────────────
message.part.delta 548 NO (catchall "unhandled event")
session.status 122 NO (catchall)
session.diff 61 NO (catchall)
file.watcher.updated 16 NO (catchall)
file.edited 16 NO (catchall)
todo.updated 10 NO (catchall)
session.updated 5 NO (catchall)
session.idle 5 YES (closes turn span ✓)
tui.toast.show 1 NO (catchall)
message.updated 0 needed by handleMessageUpdated to emit LLM span
message.part.updated 0 needed to accumulate llmOutputParts (text + tool calls + reasoning)
session.created 0 plugin works around this via chat.message lazy-init ✓
Cross-check from the plugin's own I/O ndjson (LOG_TO_FILE=auto):
784 \"hook\":\"event\"
222 \"hook\":\"tool.execute.before\"
220 \"hook\":\"tool.execute.after\"
112 \"hook\":\"experimental.chat.system.transform\"
10 \"hook\":\"chat.message\" # = 5 calls × (input+output)
So chat.message fires correctly (5 opencode sessions × 1 hook call each — the lazy-init root-span path in the chat.message handler runs). What's broken is everything after the turn span gets created:
dist/index.js's handleMessageUpdated is gated on message.updated, which opencode 1.14.x never sends. The LLM span is emitted from this handler with the message's time.completed, tokens, providerID/modelID, and accumulated text — none of which the plugin can collect.
handleMessagePartUpdated is gated on message.part.updated, which is also never sent. The plugin's state.llmOutputParts / state.llmToolCalls / state.llmReasoningParts maps stay empty for every turn.
opencode now streams partials via message.part.delta (token-by-token) and never publishes a single "message complete" or "part complete" event the plugin recognises.
Reproduction
npm install @braintrust/trace-opencode@0.0.8.
- Run inside
cloudflare/sandbox:0.10.1-opencode (opencode 1.14.48), with the plugin registered in ~/.config/opencode/opencode.json:
{
\"plugin\": [\"@braintrust/trace-opencode\"]
}
- Set
BRAINTRUST_API_KEY, TRACE_TO_BRAINTRUST=true, BRAINTRUST_PROJECT=…, BRAINTRUST_DEBUG=true, LOG_TO_FILE=auto.
- Start a session, fire any prompt, let it finish.
- Observe in Braintrust: root span lands, has a duration, but
LLM calls: 0.
grep 'service=braintrust-trace unhandled event' $XDG_DATA_HOME/opencode/log/*.log | awk '{print $NF}' | sort | uniq -c → reproduces the histogram above.
Suggested fix direction
The minimal path (without changing the public schema in Braintrust):
- Accumulate output via
message.part.delta. Treat the delta.field=\"text\" payload as an append into state.llmOutputParts.get(messageID). Tool-call deltas land via partial JSON in delta too — buffer and parse on completion.
- Use
session.idle (or a synthesized terminal marker per-message) as the LLM-completion event. When session.idle fires for a session that has a currentTurnSpanId, emit one LLM span with the buffered output, using currentTurnStartTime for start and wallClock.now() for end (token counts may be unavailable — set tokens: undefined instead of 0 so the dashboard doesn't show "0 tokens").
- Stop logging the catchall
unhandled event … at INFO — opencode 1.14+ sends thousands of these per session and they're swamping the log file.
I'd be happy to draft a PR if helpful. Reference data and full plugin-decision dump available; happy to share if it'd help triage.
Environment
@braintrust/trace-opencode: 0.0.8 (latest as of writing)
- opencode CLI: 1.14.48 (from
cloudflare/sandbox:0.10.1-opencode base image)
@opencode-ai/sdk: 1.16.0 (host)
- Runtime: Cloudflare Workers + Sandbox containers, hosting opencode in server mode and driving it via
client.session.promptAsync
Summary
Running
@braintrust/trace-opencode@0.0.8against opencode CLI 1.14.48 (the version bundled incloudflare/sandbox:0.10.1-opencode), the plugin loads correctly and the rootOpenCode: …span lands in Braintrust with a duration — but zero LLM spans are emitted for any turn. Braintrust dashboards showLLM calls: 0,Input: —,Output: —for every session.Root cause
opencode 1.14.x emits a different event stream than the plugin handles. I captured a full event-receive histogram from the plugin's own
eventhook over a real ~25-minute session (5 opencode sub-sessions, lots of tool activity):Cross-check from the plugin's own I/O ndjson (
LOG_TO_FILE=auto):So
chat.messagefires correctly (5 opencode sessions × 1 hook call each — the lazy-init root-span path in thechat.messagehandler runs). What's broken is everything after the turn span gets created:dist/index.js'shandleMessageUpdatedis gated onmessage.updated, which opencode 1.14.x never sends. The LLM span is emitted from this handler with the message'stime.completed,tokens,providerID/modelID, and accumulated text — none of which the plugin can collect.handleMessagePartUpdatedis gated onmessage.part.updated, which is also never sent. The plugin'sstate.llmOutputParts/state.llmToolCalls/state.llmReasoningPartsmaps stay empty for every turn.opencode now streams partials via
message.part.delta(token-by-token) and never publishes a single "message complete" or "part complete" event the plugin recognises.Reproduction
npm install @braintrust/trace-opencode@0.0.8.cloudflare/sandbox:0.10.1-opencode(opencode 1.14.48), with the plugin registered in~/.config/opencode/opencode.json:{ \"plugin\": [\"@braintrust/trace-opencode\"] }BRAINTRUST_API_KEY,TRACE_TO_BRAINTRUST=true,BRAINTRUST_PROJECT=…,BRAINTRUST_DEBUG=true,LOG_TO_FILE=auto.LLM calls: 0.grep 'service=braintrust-trace unhandled event' $XDG_DATA_HOME/opencode/log/*.log | awk '{print $NF}' | sort | uniq -c→ reproduces the histogram above.Suggested fix direction
The minimal path (without changing the public schema in Braintrust):
message.part.delta. Treat thedelta.field=\"text\"payload as an append intostate.llmOutputParts.get(messageID). Tool-call deltas land via partial JSON indeltatoo — buffer and parse on completion.session.idle(or a synthesized terminal marker per-message) as the LLM-completion event. Whensession.idlefires for a session that has acurrentTurnSpanId, emit one LLM span with the buffered output, usingcurrentTurnStartTimeforstartandwallClock.now()forend(token counts may be unavailable — settokens: undefinedinstead of0so the dashboard doesn't show "0 tokens").unhandled event …at INFO — opencode 1.14+ sends thousands of these per session and they're swamping the log file.I'd be happy to draft a PR if helpful. Reference data and full plugin-decision dump available; happy to share if it'd help triage.
Environment
@braintrust/trace-opencode: 0.0.8 (latest as of writing)cloudflare/sandbox:0.10.1-opencodebase image)@opencode-ai/sdk: 1.16.0 (host)client.session.promptAsync