Trace OpenCode with Litefuse
OpenCode is an open-source terminal coding agent with a client/server architecture, running on Bun. This integration is an OpenCode native plugin installed at ~/.config/opencode/plugin/litefuse.ts — a single TypeScript file with zero npm dependencies. Drop it in the plugin directory and it auto-loads; no opencode.jsonc change needed.
The plugin runs inside the OpenCode server process and subscribes to its event bus and a few dedicated hooks (chat.message, message.part.updated’s step-start/step-finish, tool.execute.before/after, session.idle), producing one Litefuse trace per user turn with real per-event timestamps. Spans are sent as raw OTLP/HTTP JSON via the built-in fetch directly to Litefuse’s OTLP endpoint — no SDK, no node_modules, no build step (OpenCode loads TypeScript directly).
For AI — automatic install
If you’re chatting with OpenCode right now, paste this prompt and the agent will install it end-to-end:
Read https://litefuse.ai/SKILL.md and follow the instructions to install and configure Litefuse for OpenCode.
The skill asks for your Litefuse API keys (and walks you through signup if you don’t have an account), then completes all configuration in place. To configure it manually, read on.
What gets captured
| Data | Captured as | Notes |
|---|---|---|
| User prompt | trace input | Text; image-block count recorded in metadata |
| Each LLM API call | generation observation | plan (n tools) #N / response / think #N, named by what the model did this step |
| thinking / text / toolCall blocks | generation output | Block structure preserved, including reasoning text |
| Time to first token | generation completion_start_time | From the first streamed part — drives Litefuse’s TTFT metric |
| Sampling parameters | generation model_parameters | temperature / topP / topK / maxOutputTokens, when the model sets them |
Token usage (input, output, cache_read_input_tokens, cache_creation_input_tokens) | generation usage_details | Anthropic-style keys, from OpenCode’s per-step step-finish |
| Model name + OpenCode’s own cost | generation model + cost_details | OpenCode reports authoritative cost, overriding server-side pricing |
| Tool executions | tool observation | tool: bash (grep) #N, tool: read (index.ts) #N — key info in the name, full args in input |
Subagents (task tool) | subtree | tool (1 subagent) #N → subagent container → the subagent’s own plan/tool/response steps; child cost rolls up into the parent trace |
| Tool errors | tool observation, level=ERROR | From the tool part’s terminal error state, with a status_message preview |
| LLM errors / aborts | generation level=ERROR / WARNING | From the assistant message’s error field |
| Context compaction | context compaction event | Explains the input-token drop on the next call |
| Turns with no final answer | root span level=WARNING | Killed or interrupted mid tool-loop |
| Session grouping | trace session_id | OpenCode session id (ses_…); Turn numbers continue across resumed sessions |
| User identity | trace user_id | $LITEFUSE_USER_ID, falls back to the OS username |
| Tags | trace tags | opencode + model:<name> |
Trace structure
A turn that delegates to a subagent produces a trace shaped like this (real example):
opencode — Turn 1 (AGENT root — real turn duration)
├── plan (1 tool) #1 (generation — usage_details + cost, real latency)
├── tool (1 subagent) #2 (tool — the delegation, from the parent's view)
│ └── subagent (AGENT container — the child session)
│ ├── plan (1 tool) #1 (container-local numbering, restarts at #1)
│ ├── tool: read (note.txt) #2
│ └── subagent response (generation — the subagent's final answer)
└── response (generation — final answer, ends the turn)Design notes:
- Generations are named after what the model did this step, not which model did it (
plan (2 tools),response,think) — the model name is the generation’smodelattribute, so swapping models doesn’t break dashboards. OpenCode represents one LLM call as astep-start/step-finishpart pair; the plugin segments generations from those, taking tokens and cost fromstep-finish. - Generation latency excludes tool time. OpenCode emits
step-finishafter the step’s tools have run, so the plugin ends the generation at the last streamed-content timestamp (≈ when the LLM actually returned), keeping tool-execution time out of the LLM latency — while still taking authoritative tokens/cost fromstep-finish. - One step counter per agent container.
#Nis a single chronological sequence shared by generations and tools. A tool’sagent_plan_stepmetadata points at theagent_step_indexof the generation that requested it — the join istool.agent_plan_step == generation.agent_step_index. Each subagent container restarts at#1; hierarchy is expressed by the tree. - Subagent subtrees. OpenCode is a single server process, so one plugin instance sees the events of every session — including the child sessions spawned by the
tasktool. That means subtrees need no cross-process traceparent protocol: the plugin binds a child session to the parent’s still-runningtasktool span via the child’sparentID, forming thetool (1 subagent)→subagentcontainer → child-steps three-layer shape. The tool span deliberately wraps the container: tool-span duration − container duration = the real overhead of delegating (child session startup, result collection). - Each span is sent exactly once, when it ends — OTel spans are immutable; no provisional sends, no upserts. The trace headers (name / session / user / input / tags) ride on every span, so the trace appears in Litefuse as soon as the first observation (usually
plan #1, done within seconds) completes — visible mid-turn for long turns. - Real wall-clock timestamps taken at the moment each event fires — the timeline reflects real LLM latency, tool durations, and the gaps between them.
- Flat
agent_*metadata: every OpenCode-specific field is a top-level metadata key with one uniform prefix (agent_step_index,agent_plan_step,agent_duration_ms…) — the same dashboard filter works across every Litefuse agent integration. Sparse: fields with no value simply don’t appear; nonullplaceholders. - Fail-open: any unexpected error is written to
~/.config/opencode/litefuse.logand swallowed — the plugin never blocks OpenCode. Unreachable targets are skipped.
Quick start
Prerequisites
- OpenCode installed — check with
opencode --version. curlon yourPATH(present by default on macOS / Linux). The plugin uses it to deliver each turn’s root span synchronously — see How it works.- A Litefuse project at https://litefuse.cloud with a public + secret key.
No other dependencies: the plugin is a single file using only Node built-ins, and OpenCode loads TypeScript directly.
Download the plugin
mkdir -p ~/.config/opencode/plugin
curl -fsSL https://litefuse.ai/integrations/opencode/litefuse.ts \
-o ~/.config/opencode/plugin/litefuse.tsThe source is browsable at the same URL — feel free to read it before deploying. Files under ~/.config/opencode/plugin/ are auto-loaded by OpenCode, no opencode.jsonc change required.
Configure credentials
Create ~/.config/opencode/litefuse-targets.json (no shell-rc edits needed):
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.jsonReplace the placeholders with real values. Alternatively use the environment variables LITEFUSE_PUBLIC_KEY / LITEFUSE_SECRET_KEY / LITEFUSE_BASE_URL (the matching LANGFUSE_* names are accepted as a fallback), exported in the shell that launches OpenCode. The targets file supports multiple targets — the same trace is written to every Litefuse instance, each with its own environment.
Verify
OPENCODE_LITEFUSE_DEBUG=true opencode run "Reply with exactly: ok"
tail -3 ~/.config/opencode/litefuse.log
# Expect: "plugin loaded, 1 target(s): https://litefuse.cloud (production)"
# + "turn complete session=... turn=1 api=1 tools=0"Open https://litefuse.cloud — the latest trace is named opencode — Turn 1 with the structure shown above.
OpenCode loads plugins from the plugin directory at startup: after installing the plugin or changing credentials, a newly started opencode run or a freshly opened session picks it up automatically; in an already-open TUI, just start a new session.
If no keys are configured the plugin stays idle (zero overhead) and sends nothing.
Environment variables
LITEFUSE_* variables take precedence; the matching LANGFUSE_* variables are accepted as an ecosystem-compatible fallback.
| Variable | Required | Description |
|---|---|---|
LITEFUSE_PUBLIC_KEY | Yes* | Litefuse project public key (pk-lf-...). |
LITEFUSE_SECRET_KEY | Yes* | Litefuse project secret key (sk-lf-...). |
LITEFUSE_BASE_URL | No | Defaults to https://litefuse.cloud. Alias LITEFUSE_HOST. |
LITEFUSE_TRACING_ENVIRONMENT | No | Trace environment for the env-configured target. Defaults to production; use development for experiments so you don’t pollute production dashboards. |
LITEFUSE_USER_ID | No | Overrides the trace user_id. Falls back to the OS username. |
LITEFUSE_EXTRA_TARGETS | No | JSON array of extra targets (same shape as the targets file). |
OPENCODE_LITEFUSE_DEBUG | No | Set to "true" for verbose logging to ~/.config/opencode/litefuse.log. |
OPENCODE_LITEFUSE_MAX_CHARS | No | Truncation threshold (chars) for span input/output. Defaults to 1000000 (~1MB text). |
* Only required when not using ~/.config/opencode/litefuse-targets.json. Credentials in the targets file work with no environment variables at all.
Trace metadata reference
Every OpenCode-specific field is a flat top-level metadata key with the agent_ prefix — every Litefuse agent integration shares the same keys, so the same filters and dashboards work everywhere. Litefuse standard fields (sessionId, userId, tags) stay at the trace level via OTel attributes. Sparse: fields with no value never appear, never null.
Trace level:
agent_turn_number,agent_session_id,agent_cwd,agent_model,agent_provideragent_api_calls,agent_tool_calls,agent_steps,agent_duration_ms— turn statsagent_image_blocks,agent_prompt_truncated,agent_prompt_orig_len— when applicable
Generation observation:
agent_step_index— the#Nin the nameagent_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— only when truncated
Tool observation:
agent_tool_name,agent_tool_call_idagent_step_index— the tool’s own#Nagent_plan_step— theagent_step_indexof the plan generation that requested this toolagent_duration_ms,agent_is_erroragent_details— OpenCode’s structured tool-result summary (exit code, title, etc.), embedded as an object when ≤ 2 KB; oversized values are dropped wholesale and recorded asagent_details_omitted_len
Subagent container:
agent_subagent: true, plus the same per-run stats as the trace level (agent_api_calls,agent_tool_calls,agent_steps,agent_duration_ms)
How it works
The plugin subscribes to OpenCode’s event bus and a few dedicated hooks:
| Event / Hook | Used for |
|---|---|
chat.message (user) | Starts a turn: captures the prompt; resumes Turn numbering from session history |
chat.params | Sampling params → generation model_parameters |
experimental.chat.messages.transform | Full provider messages → generation input (best-effort) |
message.part.updated (step-start) | A generation begins |
message.part.updated (text / reasoning / tool) | Generation output blocks, TTFT, tool-call counting |
message.part.updated (step-finish) | Emits the generation span: named by content, with usage + cost |
tool.execute.before / tool.execute.after | Tool span; a task tool is registered as a subtree container’s parent |
message.part.updated (tool error state) | Tool failure → level=ERROR (the authoritative source) |
session.compacted | Context-compaction event observation |
message.updated (assistant) | Model / provider / error info |
session.idle | Emits the root span: trace output, turn stats; flush |
Each completed span during a turn is POSTed immediately as OTLP/HTTP JSON, fire-and-forget with a timeout, to <baseUrl>/api/public/otel/v1/traces (Basic auth). Each span is sent once; in-progress steps become visible when they complete.
The root span is delivered synchronously. OpenCode’s headless run exits right after session.idle without awaiting plugin event handlers — at that point the root span (the last span of the turn) would race process teardown and be lost on an async send. So the plugin emits the turn-end root-span batch with one synchronous curl (which blocks the JS thread until delivery completes), guaranteeing it lands; the rest of the turn’s spans still go async and never block. This is why curl is a prerequisite.
For subagents, OpenCode’s task tool spawns a child session. The child’s events flow through the same plugin instance; the plugin binds the child to the parent’s still-running task tool span via the child’s parentID, producing a full subtree. The subagent’s generations carry their own usage, so cost rolls up into the parent trace automatically. One known transient: in the live view a subagent’s steps can appear before their container (the container is sent when the child session ends) — after the turn completes the tree is correct and properly nested.
The plugin is fail-open: any unexpected error only writes to ~/.config/opencode/litefuse.log and returns silently, never blocking or slowing OpenCode.
Troubleshooting
No trace in Litefuse. Check the plugin log:
tail -20 ~/.config/opencode/litefuse.logAn empty log means the plugin didn’t load — confirm the file is at ~/.config/opencode/plugin/litefuse.ts. File present but no plugin loaded line? The plugin found no credentials: check ~/.config/opencode/litefuse-targets.json or the LITEFUSE_* environment variables. A sendSync failed: or send failed: line means a key or network issue: verify the keys and LITEFUSE_BASE_URL.
Root span missing (the trace has children but no AGENT root). Almost always curl isn’t on the PATH — the plugin relies on it to deliver the root span synchronously. The log will show sendSync failed: ... exit=.... Install curl.
A tool failed but isn’t marked ERROR. OpenCode’s bash tool treats a non-zero exit as a successful completion (stderr goes to output, exit code to metadata), not a tool-level error — that matches OpenCode’s semantics. Genuine tool errors (e.g. read of a missing file) are correctly marked level=ERROR.
totalCost is 0. Either a free model (cost really is 0), or your Litefuse project has no pricing entry for the model. Add one under Settings → Models in the Litefuse UI if needed — though OpenCode supplies cost data that the plugin forwards as cost_details, so paid models show cost directly.
Subagents don’t nest (no grandchildren). OpenCode’s child sessions don’t currently get the task tool, so a subagent can’t delegate further — the plugin’s recursive subtree support is ready for when that changes upstream.
Enable debug logging:
OPENCODE_LITEFUSE_DEBUG=true opencode
# Every send / skip / error is written to ~/.config/opencode/litefuse.logDual-write to local + cloud: add a second entry to litefuse-targets.json (e.g. a self-hosted instance with "environment": "test"). Unreachable targets are skipped silently and never block OpenCode.
Resources
- OpenCode website
- OpenCode plugins docs
- Litefuse Cloud
- Plugin source:
litefuse.ts