> ## Documentation Index
> Fetch the complete documentation index at: https://laminar.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenCode observability

> Trace OpenCode coding agent sessions: every model turn and tool call, grouped per session and readable as a conversation in Laminar.

## Overview

[OpenCode](https://opencode.ai/) is an open-source AI coding agent. You can use it as a CLI or run it as a server and drive sessions from your own code to build products on top of the agent. Laminar makes those runs observable with the `@lmnr-ai/opencode-plugin` plugin, which works with OpenCode V2 and with OpenCode V1 (1.4 and newer).

What Laminar captures:

* Every conversation turn, one trace per turn.
* LLM prompts, responses, token counts, latency, and cost.
* Tool calls (`bash`, `read`, `write`, `edit`, custom tools) with arguments and results.
* Subagents, nested under the tool call that started them.
* When you send a prompt from inside `observe`, the server-side turn nests under your caller span, so the entire call stack lands in a single trace.

Each trace carries the OpenCode session ID as its Laminar session ID, so every turn in the same OpenCode conversation shows up under one session in Laminar.

## Calling OpenCode from TypeScript

Laminar ships two pieces that cooperate:

* **`@lmnr-ai/opencode-plugin`** runs inside the OpenCode server and emits the turn, LLM, and tool-call spans.
* **`@lmnr-ai/lmnr`** runs in your code. Your prompt carries the current span context to the server, and the plugin parents the turn under it. On V2 you pass the context in the prompt's `metadata`; on V1, `@lmnr-ai/lmnr` patches `@opencode-ai/sdk` and injects it for you.

The result: one trace per turn that spans from your `observe` block into the server's `opencode turn` span and all the way down to each LLM call.

<Steps>
  <Step title="Add the Laminar plugin to your OpenCode config">
    The plugin is what actually emits the turn and tool spans. Add it to the OpenCode config next to your code. The key differs between versions:

    <Tabs>
      <Tab title="OpenCode V2">
        ```json opencode.jsonc theme={null}
        {
          "$schema": "https://opencode.ai/config.json",
          "plugins": ["@lmnr-ai/opencode-plugin"]
        }
        ```
      </Tab>

      <Tab title="OpenCode V1">
        ```json opencode.json theme={null}
        {
          "$schema": "https://opencode.ai/config.json",
          "plugin": ["@lmnr-ai/opencode-plugin"]
        }
        ```
      </Tab>
    </Tabs>

    V2 reads `plugins` and V1 reads `plugin`. Using the other version's key means the plugin never loads.

    <Note>
      OpenCode reads config from several locations, and a file in the current project takes precedence over the global one in `~/.config/opencode/`. If you want tracing on by default everywhere, put the plugin entry in the global file. See OpenCode's plugin docs for [V2](https://opencode.ai/v2/docs/plugins/) and [V1](https://opencode.ai/docs/config/#plugins) for the full rules.
    </Note>
  </Step>

  <Step title="Set LMNR_PROJECT_API_KEY">
    The plugin reads `LMNR_PROJECT_API_KEY` from the environment of the process that runs the OpenCode server, so export it in the shell where you start the server. On V1, `opencode.createOpencode()` starts the server from your TypeScript process, so that is the same shell. The plugin also reads `.env` files in the project directory.

    ```bash theme={null}
    export LMNR_PROJECT_API_KEY=your-laminar-project-api-key
    ```

    To get the project API key, go to the Laminar dashboard, click the project settings,
    and generate a project API key. This is available both in the cloud and in the self-hosted version of Laminar.

    Specify the key at `Laminar` initialization. If not specified,
    Laminar will look for the key in the `LMNR_PROJECT_API_KEY` environment variable.

    <Tip>
      Self-hosting Laminar? Also set `LMNR_BASE_URL` and `LMNR_GRPC_PORT` to point at your instance. For the OSS default, that's `http://localhost` and `8001`. On V2 you can instead pass `projectApiKey` and `baseUrl` as plugin options: `{ "package": "@lmnr-ai/opencode-plugin", "options": { "baseUrl": "..." } }` in place of the plain plugin name.
    </Tip>
  </Step>

  <Step title="Send prompts from inside observe">
    <Tabs>
      <Tab title="OpenCode V2">
        Install the Laminar SDK and OpenCode's [JavaScript client](https://opencode.ai/v2/docs/build/client/), and start the server with `opencode serve`:

        ```bash theme={null}
        npm install @lmnr-ai/lmnr@latest @opencode/client@latest
        ```

        Pass the current span context as `lmnrSpanContext` in the prompt's `metadata`:

        ```typescript {1,4,11,15,21} theme={null}
        import { Laminar, observe } from "@lmnr-ai/lmnr";
        import { OpenCode } from "@opencode/client";

        Laminar.initialize();

        const client = OpenCode.make({ baseUrl: "http://localhost:4096" });
        const session = await client.session.create({
          location: { directory: "/workspace" },
        });

        await observe({ name: "my-agent-step" }, async () => {
          await client.session.prompt({
            sessionID: session.id,
            text: "Create a Python factorial function and test it.",
            metadata: { lmnrSpanContext: Laminar.serializeLaminarSpanContext() },
          });
          // Wait for the turn to finish so it stays inside this span.
          await client.session.wait({ sessionID: session.id });
        });

        await Laminar.shutdown();
        ```
      </Tab>

      <Tab title="OpenCode V1">
        Install the Laminar SDK and `@opencode-ai/sdk`:

        ```bash theme={null}
        npm install @lmnr-ai/lmnr@latest @opencode-ai/sdk@latest
        ```

        Initialize Laminar with the `opencode` module. The instrumentation injects the span context into every `client.session.prompt` call made inside `observe`:

        ```typescript {1,2,5-9,16} theme={null}
        import * as opencode from "@opencode-ai/sdk";
        import { Laminar, observe } from "@lmnr-ai/lmnr";

        // Initialize Laminar once at the entry point.
        Laminar.initialize({
          instrumentModules: {
            opencode,
          },
        });

        const { client, server } = await opencode.createOpencode();

        try {
          const sessionRes = await client.session.create({ body: { title: "agent run" } });

          await observe({ name: "my-agent-step" }, async () => {
            await client.session.prompt({
              path: { id: sessionRes.data.id },
              body: {
                model: { providerID: "anthropic", modelID: "claude-haiku-4-5" },
                parts: [{ type: "text", text: "Create a Python factorial function and test it." }],
              },
            });
          });
        } finally {
          server.close();
          await Laminar.shutdown();
        }
        ```

        <Note>
          If you import `@opencode-ai/sdk` before `Laminar.initialize()` (common when the project has `"type": "module"` in `package.json`), your local reference won't be auto-wrapped. Pass the module via `instrumentModules` as shown above; the instrumentation attaches to the SDK's `Session` class and every new `OpencodeClient` picks it up.
        </Note>
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Trace structure

Because your prompt hands the span context to the server-side plugin, the trace comes back as a single tree: your `observe` span is the root, the server's turn span is its child, and every model call and tool call hangs off the turn. There is no separate trace for the server: everything the OpenCode process does on this turn shows up nested under the TypeScript code that kicked it off.

On V2 the turn span is `opencode turn` and each model call is an `ai.llm <provider>:<model>` span. The screenshots below come from a V1 run, where the turn wraps `session.llm` spans instead.

The tree view makes that nesting obvious:

<Frame caption="Tree view: the outer my-agent-step span from the TypeScript caller is the parent of the server's turn, which wraps every session.llm, model call, and tool call (write, bash) the agent made on this turn.">
  <img src="https://mintcdn.com/laminarai/RPYdoMdfsv6y-QUL/images/traces/opencode-sdk-tree.png?fit=max&auto=format&n=RPYdoMdfsv6y-QUL&q=85&s=49b3d3f299b3b9ac0c003f9e555dfa22" alt="OpenCode SDK trace in Laminar, tree view" width="1512" height="982" data-path="images/traces/opencode-sdk-tree.png" />
</Frame>

Switch to transcript view on the same trace to read the conversation: user prompt, model output, tool calls, and tool results read as a dialogue instead of a span tree. More on the trace UX: [Viewing traces](/docs/platform/viewing-traces).

<Frame caption="Same trace in transcript view: the turn reads as a dialogue with the model responses and tool invocations inline.">
  <img src="https://mintcdn.com/laminarai/RPYdoMdfsv6y-QUL/images/traces/opencode-sdk.png?fit=max&auto=format&n=RPYdoMdfsv6y-QUL&q=85&s=a02fcf07cc031eaf77bf414bd5b72e31" alt="OpenCode SDK trace in Laminar, transcript view" width="1512" height="982" data-path="images/traces/opencode-sdk.png" />
</Frame>

## Using the OpenCode CLI

The plugin works the same way on a standalone `opencode` CLI run: add it to your OpenCode config, export `LMNR_PROJECT_API_KEY`, and every conversation turn gets traced. There's no TypeScript caller to be the parent span, so each turn becomes its own top-level trace.

<Steps>
  <Step title="Install the OpenCode CLI">
    Follow [OpenCode's install guide](https://opencode.ai/docs/#install). You do not need to install the plugin package yourself; OpenCode resolves it from npm.
  </Step>

  <Step title="Configure the plugin and key">
    Use the same plugin entry as in the section above (`plugins` on V2, `plugin` on V1), and export `LMNR_PROJECT_API_KEY` in the shell you launch `opencode` from. You can also put the key in a `.env` file in the project directory; the plugin loads it automatically.
  </Step>

  <Step title="Run opencode">
    ```bash theme={null}
    opencode
    ```

    When the plugin starts, it logs `[laminar] tracing initialized → https://api.lmnr.ai` on V2, or `Laminar tracing initialized → https://api.lmnr.ai` on V1. On V1 the plugin also turns on OpenCode's experimental OpenTelemetry flag for you, so you don't need to touch the OTel config. V2 doesn't have that flag, and the plugin builds its spans from OpenCode's event stream instead.
  </Step>
</Steps>

<Frame caption="CLI run traced on its own: the turn span is the root, wrapping the model response, the write tool call, and the bash tool call for a Python factorial task.">
  <img src="https://mintcdn.com/laminarai/RPYdoMdfsv6y-QUL/images/traces/opencode.png?fit=max&auto=format&n=RPYdoMdfsv6y-QUL&q=85&s=2f162a740c3e78eee8490498b5a40352" alt="OpenCode CLI trace in Laminar" width="1512" height="982" data-path="images/traces/opencode.png" />
</Frame>

## Track outcomes with Signals

Traces answer *what happened on this turn*. **[Signals](/docs/signals/introduction) answer the cross-trace question**: *how often does the agent edit a file it wasn't asked to, when does the bash tool run a destructive command, which turns burn tokens without touching a file*. A Signal pairs a plain-language prompt with a JSON output schema. Laminar runs it live on new traces (Triggers) or backfills it across history (Jobs) and records a structured event every time it matches. From there you [query](/docs/platform/sql-editor), [cluster](/docs/signals/clusters), and [alert](/docs/signals/alerts) on events across every trace.

<Note>
  Every new project ships with a **Failure Detector** Signal that categorizes issues on any trace over 1000 tokens. Open it from the Signals sidebar to see events as soon as your OpenCode traces arrive.
</Note>

## Query across traces

* **[SQL editor](/docs/platform/sql-editor)** for ad-hoc queries across traces, spans, signals, and evals.
* **SQL API** for programmatic access from scripts and pipelines.
* **[CLI](/docs/platform/cli)** (`lmnr-cli sql query`) for terminal-driven queries and piping JSON into shell tools or coding agents.
* **[MCP server](/docs/platform/mcp)** to query Laminar directly from OpenCode, Claude Code, Cursor, or Codex.

## Troubleshooting

<AccordionGroup>
  <Accordion title="I don't see any traces in Laminar">
    * `LMNR_PROJECT_API_KEY` must be set in the environment of the process that launches the OpenCode server, not just in the process that creates the OpenCode client. The plugin runs on the server side and reads the key there.
    * Check the OpenCode log for `tracing initialized → ...`. If you see `LMNR_PROJECT_API_KEY not set` instead, the plugin loaded but had no key.
    * Check the config key for your OpenCode version: V2 reads `"plugins"`, V1 reads `"plugin"`. The other version's key silently does nothing.
  </Accordion>

  <Accordion title="Traces from my TypeScript caller aren't linked to the server turn">
    * Make sure both sides are on a current `@lmnr-ai/lmnr` (install with `@latest`). The caller-side instrumentation and the plugin's context extraction need to match up.
    * The server process also needs the plugin in its OpenCode config. Without it, only your caller-side span reaches Laminar.
    * Send the prompt from inside `observe` (or another active span). There is no context to pass otherwise.
    * On V2, pass `metadata: { lmnrSpanContext: Laminar.serializeLaminarSpanContext() }` with the prompt. Only V1's `@opencode-ai/sdk` gets the context injected automatically.
  </Accordion>

  <Accordion title="Self-hosting Laminar">
    Export `LMNR_BASE_URL` and `LMNR_GRPC_PORT` in the same environment as `LMNR_PROJECT_API_KEY`. For a local OSS instance, that's `LMNR_BASE_URL=http://localhost` and `LMNR_GRPC_PORT=8001`. The plugin passes these through when it initializes Laminar.
  </Accordion>
</AccordionGroup>

## What's next

<CardGroup cols={2}>
  <Card title="Viewing traces" href="/docs/platform/viewing-traces">
    Read the transcript view, filter, and search across traces.
  </Card>

  <Card title="Signals" href="/docs/signals/introduction">
    Detect behaviors and failures across every OpenCode turn, then query, cluster, and alert on them.
  </Card>

  <Card title="SQL editor and MCP server" href="/docs/platform/sql-editor">
    Query traces programmatically from the UI, API, or your IDE.
  </Card>

  <Card title="Tracing structure" href="/docs/tracing/structure/overview">
    Sessions, metadata, and tags for deeper control.
  </Card>
</CardGroup>

## Related integrations

<CardGroup cols={2}>
  <Card title="Claude Agent SDK" href="/docs/integrations/claude-agent-sdk">
    Running OpenCode alongside the Claude Agent SDK? Trace both here.
  </Card>

  <Card title="Claude Code" href="/docs/integrations/claude-code">
    Trace Claude Code sessions with a one-command plugin.
  </Card>

  <Card title="Codex" href="/docs/integrations/codex">
    Trace Codex CLI sessions with a one-command plugin.
  </Card>

  <Card title="Pi" href="/docs/integrations/pi">
    Trace Pi coding agent sessions with a one-command extension.
  </Card>

  <Card title="Anthropic" href="/docs/integrations/anthropic">
    Trace the Anthropic SDK directly in TypeScript and Python.
  </Card>

  <Card title="All integrations" href="/docs/integrations">
    Browse every provider, framework, coding agent, and browser integration Laminar supports.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.