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

DataCaptured asNotes
User prompttrace inputText; image-block count recorded in metadata
Each LLM API callgeneration observationplan (n tools) #N / response / think #N, named by what the model did this step
thinking / text / toolCall blocksgeneration outputBlock structure preserved, including reasoning text
Time to first tokengeneration completion_start_timeFrom the first streamed part — drives Litefuse’s TTFT metric
Sampling parametersgeneration model_parameterstemperature / topP / topK / maxOutputTokens, when the model sets them
Token usage (input, output, cache_read_input_tokens, cache_creation_input_tokens)generation usage_detailsAnthropic-style keys, from OpenCode’s per-step step-finish
Model name + OpenCode’s own costgeneration model + cost_detailsOpenCode reports authoritative cost, overriding server-side pricing
Tool executionstool observationtool: bash (grep) #N, tool: read (index.ts) #N — key info in the name, full args in input
Subagents (task tool)subtreetool (1 subagent) #Nsubagent container → the subagent’s own plan/tool/response steps; child cost rolls up into the parent trace
Tool errorstool observation, level=ERRORFrom the tool part’s terminal error state, with a status_message preview
LLM errors / abortsgeneration level=ERROR / WARNINGFrom the assistant message’s error field
Context compactioncontext compaction eventExplains the input-token drop on the next call
Turns with no final answerroot span level=WARNINGKilled or interrupted mid tool-loop
Session groupingtrace session_idOpenCode session id (ses_…); Turn numbers continue across resumed sessions
User identitytrace user_id$LITEFUSE_USER_ID, falls back to the OS username
Tagstrace tagsopencode + 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’s model attribute, so swapping models doesn’t break dashboards. OpenCode represents one LLM call as a step-start / step-finish part pair; the plugin segments generations from those, taking tokens and cost from step-finish.
  • Generation latency excludes tool time. OpenCode emits step-finish after 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 from step-finish.
  • One step counter per agent container. #N is a single chronological sequence shared by generations and tools. A tool’s agent_plan_step metadata points at the agent_step_index of the generation that requested it — the join is tool.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 task tool. That means subtrees need no cross-process traceparent protocol: the plugin binds a child session to the parent’s still-running task tool span via the child’s parentID, forming the tool (1 subagent)subagent container → 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; no null placeholders.
  • Fail-open: any unexpected error is written to ~/.config/opencode/litefuse.log and swallowed — the plugin never blocks OpenCode. Unreachable targets are skipped.

Quick start

Prerequisites

  • OpenCode installed — check with opencode --version.
  • curl on your PATH (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.ts

The 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.json

Replace 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.

VariableRequiredDescription
LITEFUSE_PUBLIC_KEYYes*Litefuse project public key (pk-lf-...).
LITEFUSE_SECRET_KEYYes*Litefuse project secret key (sk-lf-...).
LITEFUSE_BASE_URLNoDefaults to https://litefuse.cloud. Alias LITEFUSE_HOST.
LITEFUSE_TRACING_ENVIRONMENTNoTrace environment for the env-configured target. Defaults to production; use development for experiments so you don’t pollute production dashboards.
LITEFUSE_USER_IDNoOverrides the trace user_id. Falls back to the OS username.
LITEFUSE_EXTRA_TARGETSNoJSON array of extra targets (same shape as the targets file).
OPENCODE_LITEFUSE_DEBUGNoSet to "true" for verbose logging to ~/.config/opencode/litefuse.log.
OPENCODE_LITEFUSE_MAX_CHARSNoTruncation 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_provider
  • agent_api_calls, agent_tool_calls, agent_steps, agent_duration_ms — turn stats
  • agent_image_blocks, agent_prompt_truncated, agent_prompt_orig_len — when applicable

Generation observation:

  • agent_step_index — the #N in the name
  • agent_provider, agent_stop_reason
  • agent_api_duration_ms, agent_time_to_first_token_ms
  • agent_tool_call_count, agent_reasoning_tokens, agent_thinking_chars
  • agent_input_truncated, agent_output_truncated, agent_output_orig_len — only when truncated

Tool observation:

  • agent_tool_name, agent_tool_call_id
  • agent_step_index — the tool’s own #N
  • agent_plan_step — the agent_step_index of the plan generation that requested this tool
  • agent_duration_ms, agent_is_error
  • agent_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 as agent_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 / HookUsed for
chat.message (user)Starts a turn: captures the prompt; resumes Turn numbering from session history
chat.paramsSampling params → generation model_parameters
experimental.chat.messages.transformFull 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.afterTool 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.compactedContext-compaction event observation
message.updated (assistant)Model / provider / error info
session.idleEmits 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.log

An 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.log

Dual-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

Was this page helpful?