Skip to main content

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.

Overview

The Vercel AI SDK 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: the order of imports and file placement matters.

Node.js setup

1

Install

npm install @lmnr-ai/lmnr@latest ai@latest @ai-sdk/openai@latest
Swap @ai-sdk/openai for any provider adapter you use (full list).
2

Set environment variables

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

Initialize Laminar

Call Laminar.initialize() once at the entry point of your application, before any AI SDK call runs.
import { Laminar } from '@lmnr-ai/lmnr';
import 'dotenv/config';

Laminar.initialize();
4

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

Next.js setup

1

Install

npm install @lmnr-ai/lmnr@latest ai@latest @ai-sdk/openai@latest
2

Set environment variables

Add your keys to .env.local:
# .env.local
LMNR_PROJECT_API_KEY=your-laminar-project-api-key
OPENAI_API_KEY=your-openai-api-key
3

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.
next.config.ts
const nextConfig = {
  serverExternalPackages: ['@lmnr-ai/lmnr'],
};

export default nextConfig;
More on this option in the Next.js docs.
4

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.
instrumentation.ts
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,
    });
  }
}
Laminar only runs in the nodejs runtime. The guard skips Edge runtime routes automatically.
If you already use @vercel/otel or @sentry/nextjs, initialize them first, then Laminar. See Coexisting with @vercel/otel below.
5

Use the AI SDK in a route handler

app/api/chat/route.ts
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();
}

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

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:
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.
Vercel AI SDK trace in Laminar
Tool calls appear as nested spans with their arguments and return values captured. More on the trace UX: Viewing traces.

Track outcomes with Signals

Traces answer what happened on this run. Signals 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, cluster, and alert on events across every trace.
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.

Query across traces

  • SQL editor for ad-hoc queries across traces, spans, signals, and evals.
  • SQL API for programmatic access from scripts and pipelines.
  • CLI (lmnr-cli sql query) for terminal-driven queries and piping JSON into shell tools or coding agents.
  • MCP server 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.
app/api/chat/route.ts
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 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:
instrumentation.ts
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

  • 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.
Add @lmnr-ai/lmnr to serverExternalPackages in next.config.ts. OpenTelemetry uses Node-specific APIs that Next.js cannot bundle.
instrumentation.ts is experimental before Next.js 15. Enable it in next.config.js:
next.config.js
module.exports = {
  experimental: { instrumentationHook: true },
};
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:
lib/llm-clients.ts
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();

What’s next

Viewing traces

Read the transcript view, filter, and search across traces.

Signals

Detect behaviors and failures across every run, then query, cluster, and alert on them.

SQL editor and MCP server

Query traces programmatically from the UI, API, or your IDE.

Tracing structure

Sessions, user IDs, metadata, and tags.

OpenAI

Mixing AI SDK with the OpenAI SDK directly? Trace it here.

Anthropic

Mixing AI SDK with the Anthropic SDK directly? Trace it here.