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

# Observability and Tracing for Vercel AI SDK in Next.js and Node.js

> Trace every generateText, streamText, generateObject, and tool call in the Vercel AI SDK with Laminar. Open-source, OpenTelemetry-native observability for AI SDK apps.

## Overview

The [Vercel AI SDK](https://ai-sdk.dev/) exposes OpenTelemetry hooks via `experimental_telemetry`. Laminar is OpenTelemetry-native, so you get full traces of `generateText`, `streamText`, `generateObject`, `streamObject`, tool calls, and provider-level request and response payloads with a single tracer wired in.

What Laminar captures:

* Prompts, messages, and system instructions sent to the model.
* Model output, reasoning, and structured object results.
* Tool calls, arguments, and tool results.
* Token counts, latency, and cost per call.
* Provider identity (OpenAI, Anthropic, Google, Mistral, and others) and model name.

This page covers both Node.js and Next.js setups. If you are using the AI SDK inside Next.js, scroll to [Next.js setup](#nextjs-setup): the order of imports and file placement matters.

## Node.js setup

<Steps>
  <Step title="Install">
    ```bash theme={null}
    npm install @lmnr-ai/lmnr@latest ai@latest @ai-sdk/openai@latest
    ```

    Swap `@ai-sdk/openai` for any provider adapter you use ([full list](https://ai-sdk.dev/providers/ai-sdk-providers)).
  </Step>

  <Step title="Set environment variables">
    ```bash theme={null}
    # .env
    LMNR_PROJECT_API_KEY=your-laminar-project-api-key
    OPENAI_API_KEY=your-openai-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.
  </Step>

  <Step title="Initialize Laminar">
    Call `Laminar.initialize()` once at the entry point of your application, before any AI SDK call runs.

    ```typescript theme={null}
    import { Laminar } from '@lmnr-ai/lmnr';
    import 'dotenv/config';

    Laminar.initialize();
    ```
  </Step>

  <Step title="Pass the tracer to AI SDK calls">
    Enable `experimental_telemetry` and hand it Laminar's tracer. Do this on every `generate*` and `stream*` call you want traced.

    ```typescript theme={null}
    import { openai } from '@ai-sdk/openai';
    import { generateText } from 'ai';
    import { getTracer } from '@lmnr-ai/lmnr';

    const { text } = await generateText({
      model: openai('gpt-5-mini'),
      prompt: 'What is laminar flow?',
      experimental_telemetry: {
        isEnabled: true,
        tracer: getTracer(),
      },
    });
    ```

    The same pattern works for `streamText`, `generateObject`, `streamObject`, and `embed`.
  </Step>
</Steps>

## Next.js setup

<Steps>
  <Step title="Install">
    ```bash theme={null}
    npm install @lmnr-ai/lmnr@latest ai@latest @ai-sdk/openai@latest
    ```
  </Step>

  <Step title="Set environment variables">
    Add your keys to `.env.local`:

    ```bash theme={null}
    # .env.local
    LMNR_PROJECT_API_KEY=your-laminar-project-api-key
    OPENAI_API_KEY=your-openai-api-key
    ```
  </Step>

  <Step title="Update next.config.ts">
    Tell Next.js to treat Laminar as an external server package. Laminar depends on OpenTelemetry, which uses Node-specific APIs Next.js cannot bundle.

    ```typescript next.config.ts theme={null}
    const nextConfig = {
      serverExternalPackages: ['@lmnr-ai/lmnr'],
    };

    export default nextConfig;
    ```

    More on this option in the [Next.js docs](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages).
  </Step>

  <Step title="Initialize Laminar in instrumentation.ts">
    Create `instrumentation.ts` at the project root. Next.js auto-loads this file before any route or server component runs.

    ```typescript instrumentation.ts theme={null}
    export async function register() {
      if (process.env.NEXT_RUNTIME === 'nodejs') {
        const { Laminar } = await import('@lmnr-ai/lmnr');

        Laminar.initialize({
          projectApiKey: process.env.LMNR_PROJECT_API_KEY,
        });
      }
    }
    ```

    <Note>
      Laminar only runs in the `nodejs` runtime. The guard skips Edge runtime routes automatically.
    </Note>

    <Note>
      If you already use `@vercel/otel` or `@sentry/nextjs`, initialize them first, then Laminar. See [Coexisting with @vercel/otel](#coexisting-with-vercelotel) below.
    </Note>
  </Step>

  <Step title="Use the AI SDK in a route handler">
    ```typescript app/api/chat/route.ts theme={null}
    import { openai } from '@ai-sdk/openai';
    import { streamText } from 'ai';
    import { getTracer } from '@lmnr-ai/lmnr';

    export async function POST(req: Request) {
      const { messages } = await req.json();

      const result = streamText({
        model: openai('gpt-5-mini'),
        system: 'You are a helpful assistant.',
        messages,
        experimental_telemetry: {
          isEnabled: true,
          tracer: getTracer(),
        },
      });

      return result.toTextStreamResponse();
    }
    ```
  </Step>
</Steps>

## Tracing a multi-step agent

The AI SDK runs the tool-calling loop for you: the model calls a tool, the SDK runs the tool's `execute`, feeds the result back, and repeats until the model stops calling tools or a stop condition trips. Laminar records every step as a span under one trace, so the transcript shows each tool call and its result in order.

Two shapes are common. `generateText` with `tools` and `stopWhen` is the workhorse. `ToolLoopAgent` is a thin wrapper if you want to reuse the same tools and instructions across many calls.

### generateText with tools

```typescript theme={null}
import { openai } from '@ai-sdk/openai';
import { generateText, stepCountIs, tool } from 'ai';
import { getTracer } from '@lmnr-ai/lmnr';
import { z } from 'zod';

const result = await generateText({
  model: openai('gpt-5-mini'),
  system:
    'You are a concise travel assistant. Call tools to look up weather and local time before answering.',
  prompt:
    'I have meetings in Paris and Tokyo tomorrow. What is the weather and local time in each city right now?',
  tools: {
    weather: tool({
      description: 'Get the current weather for a city.',
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => {
        const res = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=j1`);
        const data = await res.json();
        const c = data.current_condition[0];
        return { tempC: Number(c.temp_C), condition: c.weatherDesc[0].value };
      },
    }),
    timezone: tool({
      description: 'Get the current local time for a city.',
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => {
        const res = await fetch(
          `https://timeapi.io/api/time/current/zone?timeZone=${encodeURIComponent(
            `Europe/${city}`,
          )}`,
        );
        return res.ok ? await res.json() : { error: 'unknown city' };
      },
    }),
  },
  stopWhen: stepCountIs(5),
  experimental_telemetry: {
    isEnabled: true,
    tracer: getTracer(),
    functionId: 'travel-assistant',
    metadata: { env: 'production' },
  },
});

console.log(result.text);
console.log(`Completed in ${result.steps.length} steps`);
```

`stopWhen: stepCountIs(5)` caps the loop at five model-and-tool rounds. You can combine conditions (`stopWhen: [stepCountIs(20), hasToolCall('finalize')]`) or pass a custom predicate.

### ToolLoopAgent

Reuse the same configuration across calls by constructing a `ToolLoopAgent` once:

```typescript theme={null}
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent, stepCountIs } from 'ai';
import { getTracer } from '@lmnr-ai/lmnr';
import { weather, timezone } from './tools';

const travelAgent = new ToolLoopAgent({
  model: openai('gpt-5-mini'),
  instructions:
    'You are a concise travel assistant. Call tools to look up weather and local time before answering.',
  tools: { weather, timezone },
  stopWhen: stepCountIs(5),
  experimental_telemetry: {
    isEnabled: true,
    tracer: getTracer(),
    functionId: 'travel-agent',
  },
});

const result = await travelAgent.generate({
  prompt: 'Weather and local time in Paris and Tokyo?',
});
```

Both shapes produce the same trace structure in Laminar: one parent span per call, nested tool-call spans, and the final model output in the transcript.

## See what happened in a trace

Open the trace in Laminar and you get the transcript view: system prompt, user messages, model output, tool calls, and tool results laid out as a conversation. Sub-agents collapse to their input and final output so you read what mattered, not a tree of span names.

<Frame caption="Vercel AI SDK trace in Laminar: the generateText call with tool invocations, the model response, and each step shown as a transcript entry.">
  <img src="https://mintcdn.com/laminarai/UAs7ELXtUmJOPOAK/images/tutorials/vercel-ai-sdk-trace.png?fit=max&auto=format&n=UAs7ELXtUmJOPOAK&q=85&s=6277f30d555b50c7b1bc7c10c4b416f8" alt="Vercel AI SDK trace in Laminar" width="1512" height="982" data-path="images/tutorials/vercel-ai-sdk-trace.png" />
</Frame>

Tool calls appear as nested spans with their arguments and return values captured. More on the trace UX: [Viewing traces](/platform/viewing-traces).

## Track outcomes with Signals

Traces answer *what happened on this run*. **[Signals](/signals/introduction) answer the cross-trace question**: *how often does the agent recommend a product that wasn't in stock, when does a tool call return an empty result, which generateText calls fan out into more steps than expected*. 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](/platform/sql-editor), [cluster](/signals/clusters), and [alert](/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 AI SDK traces arrive.
</Note>

## Query across traces

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

## Grouping calls inside one route

If a single route makes multiple LLM calls, wrap them in `observe` to group them under one parent span.

```typescript app/api/chat/route.ts theme={null}
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { getTracer, observe } from '@lmnr-ai/lmnr';
import { NextResponse } from 'next/server';

export const POST = async (req: Request) => {
  const { topic } = await req.json();

  const result = await observe(
    { name: 'POST /api/chat' },
    async () => {
      const tracer = getTracer();

      const { text: outline } = await generateText({
        model: openai('gpt-5-mini'),
        prompt: `Outline an article about ${topic}.`,
        experimental_telemetry: { isEnabled: true, tracer },
      });

      const { text: draft } = await generateText({
        model: openai('gpt-5-mini'),
        prompt: `Write a draft based on: ${outline}`,
        experimental_telemetry: { isEnabled: true, tracer },
      });

      return { outline, draft };
    },
  );

  return NextResponse.json(result);
};
```

You also get grouping for free if another tracing library (for example `@vercel/otel` or `@sentry/nextjs`) is already wrapping your handler.

See the full [observe reference](/tracing/structure/observe-decorator) for session IDs, user IDs, metadata, and tags.

## Coexisting with @vercel/otel

If you already register a tracer provider with `@vercel/otel`, do not call `Laminar.initialize()` (that would register a second provider). Plug Laminar's span processor into the existing one instead:

```typescript instrumentation.ts theme={null}
import { registerOTel } from '@vercel/otel';

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { LaminarSpanProcessor, initializeLaminarInstrumentations } =
      await import('@lmnr-ai/lmnr');

    registerOTel({
      serviceName: 'my-service',
      spanProcessors: [new LaminarSpanProcessor()],
      instrumentations: initializeLaminarInstrumentations(),
    });
  }
}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="I don't see any traces in Laminar">
    * Confirm `LMNR_PROJECT_API_KEY` is set in the runtime environment, not just your shell.
    * `Laminar.initialize()` must run before the first AI SDK call. In Next.js, that means it lives in `instrumentation.ts`.
    * `experimental_telemetry` is opt-in per call. If you forget to pass `{ isEnabled: true, tracer: getTracer() }`, the call is not traced.
    * Edge runtime is not supported. Make sure your route runs in `nodejs`.
  </Accordion>

  <Accordion title="Build fails with OpenTelemetry / require errors">
    Add `@lmnr-ai/lmnr` to `serverExternalPackages` in `next.config.ts`. OpenTelemetry uses Node-specific APIs that Next.js cannot bundle.
  </Accordion>

  <Accordion title="Using Next.js < 15">
    `instrumentation.ts` is experimental before Next.js 15. Enable it in `next.config.js`:

    ```javascript next.config.js theme={null}
    module.exports = {
      experimental: { instrumentationHook: true },
    };
    ```
  </Accordion>

  <Accordion title="Mixing the AI SDK with a direct OpenAI or Anthropic client">
    Direct SDK calls are auto-instrumented by `Laminar.initialize()` in Node.js. In Next.js, imports inside `instrumentation.ts` are not visible to the rest of the app, so auto-instrumentation may miss them. Call `Laminar.patch({ OpenAI, anthropic })` where you construct the client:

    ```typescript lib/llm-clients.ts theme={null}
    import { OpenAI } from 'openai';
    import * as anthropic from '@anthropic-ai/sdk';
    import { Laminar } from '@lmnr-ai/lmnr';

    Laminar.patch({ OpenAI, anthropic });

    export const openai = new OpenAI();
    export const claude = new anthropic.Anthropic();
    ```
  </Accordion>
</AccordionGroup>

## What's next

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

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

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

  <Card title="Tracing structure" href="/tracing/structure/overview">
    Sessions, user IDs, metadata, and tags.
  </Card>

  <Card title="OpenAI" href="/tracing/integrations/openai">
    Mixing AI SDK with the OpenAI SDK directly? Trace it here.
  </Card>

  <Card title="Anthropic" href="/tracing/integrations/anthropic">
    Mixing AI SDK with the Anthropic SDK directly? Trace it here.
  </Card>
</CardGroup>
