opencode
Trace opencode agent turns, model calls, tools, subagents, usage, cost, and session events.
@telemetry-dev/opencode is an opencode plugin. Add it to your opencode config, set an API key, and each prompt appears in telemetry.dev as a trace with its model calls, tool calls, subagents, token usage, and cost.
By default the plugin sends the text of your prompts and every tool call’s arguments and results, which include file contents and command output. It doesn’t send the model’s response text. You can turn content off with two options; see What leaves your machine.
Before you start
- opencode 1.15.11 or later, on its standard plugin API. opencode’s beta V2 plugin API doesn’t run this plugin.
- A telemetry.dev project and its API key. Keys start with
td_live_.
Set up
Set the API key
Set the key in the environment that starts opencode:
export TELEMETRY_DEV_API_KEY=td_live_...
export TELEMETRY_DEV_ENVIRONMENT=developmentKeep the key out of opencode.json, which is often committed to a repository.
Add the plugin
Add the package to the plugin list in opencode.json. Use the project’s opencode.json for one project, or ~/.config/opencode/opencode.json for all of them:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@telemetry-dev/opencode"]
}opencode installs npm plugins with Bun when it starts, so you don’t need to install the package yourself.
Start opencode
Restart opencode so it loads the plugin.
To pass options, use a [name, options] pair instead of the plain name. Options in opencode.json must be JSON values:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@telemetry-dev/opencode", { "captureInput": false, "captureOutput": false }]
]
}
Load the plugin from a file instead
Use a plugin file when you need an option that is a function, such as mask. Don’t also list the package in opencode.json: opencode would load it twice and record every span twice.
Add the package as a dependency of the opencode config directory. opencode runs bun install there when it starts:
{
"dependencies": {
"@telemetry-dev/opencode": "^0.1.9"
}
}
Then create the plugin file. opencode loads every file in .opencode/plugins/, or in ~/.config/opencode/plugins/ for all projects:
import { telemetryDevPlugin } from "@telemetry-dev/opencode";
export const TelemetryDev = telemetryDevPlugin({
mask: (value, { key }) => (key === "password" ? "[REDACTED]" : value),
});
Check that data arrives
Send a prompt and wait until opencode finishes it. Then open the trace explorer. The plugin sends each prompt’s spans when the session goes idle.
If no trace appears, check that TELEMETRY_DEV_API_KEY is set in the shell that started opencode. Without a key the plugin does nothing and reports no error.
What is recorded
Each prompt produces one trace:
invoke_agent
├── chat {model}
│ ├── execute_tool {tool}
│ └── execute_tool task
│ └── invoke_agent
└── chat {model}
invoke_agentrecords the text of the prompt, the agent name, and the opencode session ID.chat {model}records the provider, model, finish reason, token usage, the cost opencode reports, and errors. Reasoning tokens count as output tokens; the span also keeps separate reasoning, cache-read, and cache-write counts.execute_tool {tool}records the tool call ID, arguments, result, duration, and error.- Task subagents appear as an
invoke_agentspan under the tool call that started them. - Session creation, compaction, and errors become logs.
The plugin doesn’t record the model’s response text: the opencode event for a finished message carries usage and metadata but not the final text.
What leaves your machine
By default the plugin sends:
- The text of your prompts (
captureInput). - Tool arguments (
captureInput), such as shell commands and file paths. - Tool results (
captureOutput): the tool’s title, output, and metadata, such as file contents and command output.
Set both options to false to stop sending this content, either in opencode.json as shown above or in a plugin file. In a plugin file, mask can also change or drop individual values before they are sent.
These are sent whatever the options:
- Session IDs, the agent name, provider and model names, token counts, cost, finish reasons, and timings.
- The error text of failed tools. This is what opencode returns to the model, so it can contain file or command content.
- The message of
session.errorevents, on spans and as theopencode.error.messagelog attribute, and provider error messages onchatspans.
mask doesn’t apply to these error messages in version 0.1.9.
telemetry.dev’s privacy settings can also drop recognized prompt and response content before storage, but only after it has left your machine.
How it runs
A chat.message event starts the invoke_agent span. The span ends when the session goes idle (a session.status event with type idle, or the older session.idle event) or when opencode disposes the plugin.
A session.error event doesn’t end the trace, because opencode can compact the context and continue the same prompt. If compaction follows, the plugin drops the error. Otherwise it applies the error when the session goes idle.
When opencode disposes the plugin, it closes open spans, marks those without an error as incomplete, and waits for the last export.
Configuration
| Variable | Default | Description |
|---|---|---|
TELEMETRY_DEV_API_KEY |
None | Ingest API key. Without one, the plugin does nothing. |
TELEMETRY_DEV_BASE_URL |
https://ingest.telemetry.dev |
OTLP ingest base URL. |
TELEMETRY_DEV_ENVIRONMENT |
production |
Environment on exported telemetry. |
OTEL_SERVICE_NAME |
opencode |
OpenTelemetry service name. |
Options override the environment variables. Options in opencode.json override options passed to telemetryDevPlugin().
| Option | Type | Default | Description |
|---|---|---|---|
captureInput |
boolean |
true |
Send prompt text and tool arguments. |
captureOutput |
boolean |
true |
Send tool results. |
mask |
(value, { key }) => unknown |
None | Change or drop captured content before it is sent. Plugin file only. |
maxAttributeLength |
number |
65536 |
Maximum length of a content attribute. |
agentName |
string |
opencode |
Value of gen_ai.agent.name. |
apiKey |
string |
TELEMETRY_DEV_API_KEY |
Ingest API key. |
baseUrl |
string |
TELEMETRY_DEV_BASE_URL |
Ingest base URL. |
environment |
string |
TELEMETRY_DEV_ENVIRONMENT |
Deployment environment. |
serviceName |
string |
OTEL_SERVICE_NAME |
Service name. |
enabled |
boolean |
true |
false turns the plugin off completely. |
onError |
(error) => void |
None | Receive plugin errors. The plugin never throws them into opencode. |
The plugin also accepts the other TypeScript SDK init() options, except registerGlobal, which it always sets to false. The first plugin instance to start in a process sets the options for that process.
Limitations
- When
opencode servereceivesSIGTERM, opencode doesn’t dispose plugins, so the last batch of spans can be lost. - opencode reports token usage as totals on each finished message, not per step.
- Tool errors don’t reach the
tool.execute.afterhook. The plugin rebuilds failed tool spans frommessage.part.updatedevents.