πŸ”₯ DeepEval 4.0 just got released. Read the announcement.
Orchestration Frameworks

Vercel AI SDK

OTel Instrumentation
Evals in CI/CD
Evals with Traceability

Vercel AI SDK is a TypeScript toolkit for calling LLMs β€” generateText, streamText, tools, structured output, and embeddings β€” with built-in OpenTelemetry spans for every step.

The deepeval integration registers an OpenTelemetry processor that turns those ai.* spans into traces you can inspect and evaluate. No rewrite of your AI SDK calls beyond enabling telemetry.

deepeval's Vercel AI SDK integration enables you to:

  • Trace every AI SDK call β€” configureAiSdkTracing(...) once, then pass the tracer into experimental_telemetry on each call.
  • Evaluate traces or individual components with any deepeval metric.
  • Run evals from scripts or CI/CD β€” same instrumentation, different surfaces.
  • Customize trace and span data through configure options, AI SDK telemetry metadata, and deepeval's span-staging helpers.

Getting Started

Installation

npm install deepeval ai @ai-sdk/openai

No Confident AI account is needed to evaluate locally. Traces are built in-process either way; a CONFIDENT_API_KEY only adds the upload, so run npx deepeval login when you want them in a shared dashboard too.

Instrument and evaluate

Call configureAiSdkTracing(...) once at startup, then pass the returned tracer into experimental_telemetry on every AI SDK call you want traced.

ai-sdk-agent.ts
import { generateText, tool, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

import { configureAiSdkTracing } from "deepeval/integrations/ai-sdk";
import { EvaluationDataset, Golden } from "deepeval/dataset";
import { TaskCompletionMetric } from "deepeval/metrics";

const tracer = configureAiSdkTracing({ name: "weather-app" });

const ask = (question: string) =>
  generateText({
    model: openai("gpt-4o-mini"),
    prompt: question,
    tools: {
      getWeather: tool({
        description: "Get the current weather for a city.",
        inputSchema: z.object({ city: z.string() }),
        execute: async ({ city }) => ({
          weather: city.toLowerCase() === "tokyo" ? "Sunny, 72F" : "Rainy, 55F",
        }),
      }),
    },
    stopWhen: stepCountIs(2),
    experimental_telemetry: {
      isEnabled: true,
      tracer,
    },
  });

// Goldens are the inputs you want to evaluate.
const dataset = new EvaluationDataset({
  goldens: [new Golden({ input: "What's the weather in Tokyo?" })],
});

// The `TaskCompletionMetric` is passed into the `evalsIterator`.
for await (const golden of dataset.evalsIterator({
  metrics: [new TaskCompletionMetric()],
})) {
  await ask((golden as Golden).input);
}

Done βœ…. You've run your first eval with full traceability into the Vercel AI SDK via deepeval.

What gets traced

Each top-level AI SDK call (generateText, streamText, generateObject, streamObject, embed, embedMany) that has telemetry enabled produces a trace. Inside it, Vercel’s ai.* spans map to deepeval span types:

AI SDK spandeepeval span
ai.generateText, ai.streamText, ai.generateObject, ai.streamObject, and their .doGenerate / .doStream childrenLLM
ai.toolCallTool
ai.embed, ai.embedMany, and their .doEmbed childrenRetriever
Trace                              ← what the user observes
└── LLM: ai.generateText            ← one generateText(...) call
    β”œβ”€β”€ LLM: ai.generateText.doGenerate   ← model chooses a tool
    β”œβ”€β”€ Tool: getWeather                  ← tool input + output
    └── LLM: ai.generateText.doGenerate   ← final answer

The trace and its component spans are independently evaluable. Every tool call is also collected onto the trace's toolsCalled so trace-level metrics can reason about tool use.

There is no agent span β€” AI SDK's root is the generation call itself. Stage LLM / tool / retriever metrics with nextLlmSpan / nextToolSpan / nextRetrieverSpan, or score the whole run with trace-level metrics on evalsIterator.

Running evals

There are two surfaces for running evals against an AI SDK app. Pick by where you want results to surface β€” your terminal during development, or your CI pipeline as a pass/fail gate.

In CI/CD (Vitest)

Use the toPass() matcher. The golden is the subject; task produces the trace judged against it. A failing metric fails the test, which fails the build.

ai-sdk-agent.test.ts
import { it, expect } from "vitest";
import { EvaluationDataset, Golden } from "deepeval/dataset";
import { TaskCompletionMetric } from "deepeval/metrics";
import { ask } from "./ai-sdk-agent";
import "deepeval/vitest";

const dataset = new EvaluationDataset({
  goldens: [
    new Golden({ input: "What's the weather in Tokyo?" }),
    new Golden({ input: "What's the weather in London?" }),
  ],
});

it.each(dataset.goldens as Golden[])(
  "completes the weather task",
  async (golden) => {
    await expect(golden).toPass([new TaskCompletionMetric()], {
      task: (g) => ask(g.input),
    });
  },
);

Run it with:

npx deepeval test run ai-sdk-agent.test.ts

In a script

Use EvaluationDataset + evalsIterator(...). Each Golden becomes one AI SDK run, and metrics passed to the iterator score the resulting trace end-to-end.

ai-sdk-agent.ts
const dataset = new EvaluationDataset({
  goldens: [
    new Golden({ input: "What's the weather in Tokyo?" }),
    new Golden({ input: "What's the weather in London?" }),
  ],
});

for await (const golden of dataset.evalsIterator({
  metrics: [new TaskCompletionMetric()],
})) {
  await ask((golden as Golden).input);
}

Applying metrics to components

Passing metrics=[...] to evalsIterator evaluates the overall AI SDK run. To evaluate a component instead, stage metrics onto the next span the processor opens.

LLM calls

Wrap the call in nextLlmSpan(...). The processor drains the staged metric onto the first LLM span it opens inside the callback β€” typically the root ai.generateText / ai.streamText span.

ai-sdk-agent.ts
import { AnswerRelevancyMetric } from "deepeval/metrics";
import { nextLlmSpan } from "deepeval/tracing";
...

for await (const golden of dataset.evalsIterator()) {
  await nextLlmSpan({ metrics: [new AnswerRelevancyMetric()] }, () =>
    ask((golden as Golden).input),
  );
}

Tool calls

Inside a tool execute body your code is the tool span, so reach it with updateCurrentSpan(...):

ai-sdk-agent.ts
import { updateCurrentSpan } from "deepeval/tracing";
import { tool } from "ai";
import { z } from "zod";
...

const getWeather = tool({
  description: "Get the current weather for a city.",
  inputSchema: z.object({ city: z.string() }),
  execute: async ({ city }) => {
    updateCurrentSpan({
      metadata: { operation: "getWeather" },
    });
    return {
      weather: city.toLowerCase() === "tokyo" ? "Sunny, 72F" : "Rainy, 55F",
    };
  },
});

nextToolSpan(...) stages the same fields from outside the tool, on the same one-shot semantic as nextLlmSpan.

Retriever calls (embeddings)

ai.embed / ai.embedMany become retriever spans, so nextRetrieverSpan(...) stages a metric (or a Confident AI metricCollection) on the first one in the callback.

ai-sdk-agent.ts
import { embed } from "ai";
import { openai } from "@ai-sdk/openai";
import { nextRetrieverSpan } from "deepeval/tracing";
...

for await (const golden of dataset.evalsIterator()) {
  await nextRetrieverSpan({ metricCollection: "embeddings_v1" }, () =>
    embed({
      model: openai.embedding("text-embedding-3-small"),
      value: (golden as Golden).input,
      experimental_telemetry: { isEnabled: true, tracer },
    }),
  );
}

Customizing trace and span data

AI SDK owns the span lifecycle, so customization happens at configure time, per-call telemetry metadata, or the span-staging boundary.

  • Use configureAiSdkTracing({...}) for defaults that apply to every trace.
  • Use experimental_telemetry.metadata / functionId for values that change per call.
  • Use nextLlmSpan(...) / nextToolSpan(...) / nextRetrieverSpan(...) to stage component-level fields onto the next span the processor opens.
  • Use updateCurrentSpan(...) inside a tool body, where your code already sits on the span.

Configure-level defaults:

ai-sdk-agent.ts
const tracer = configureAiSdkTracing({
  name: "weather-app",
  environment: "development",
  traceMetricCollection: "online_weather_v1",
});

Per-call telemetry β€” functionId becomes the trace name; recognized metadata keys become trace fields:

ai-sdk-agent.ts
await generateText({
  model: openai("gpt-4o-mini"),
  prompt: "What's the weather in Tokyo?",
  experimental_telemetry: {
    isEnabled: true,
    tracer,
    functionId: "weather-ask",
    metadata: {
      threadId: "thread-123",
      userId: "user-xyz",
      tags: ["production"],
      traceName: "weather-ask",
    },
  },
});

Recognized metadata keys: threadId, userId, testCaseId, turnId, tags, context, traceName, traceMetricCollection, metricCollection, expectedOutput, sessionId, promptAlias, promptCommitHash. Other keys fall through to span metadata.

Advanced patterns

Score every matching span with setTracingContext

Where next*Span(...) is one-shot, setTracingContext(...) is scope-wide: it applies to every matching span in the callback. Use this when a tool loop makes several model calls and you want all of them scored.

ai-sdk-agent.ts
import { AnswerRelevancyMetric, ToolCorrectnessMetric } from "deepeval/metrics";
import { setTracingContext } from "deepeval/tracing";
...

await setTracingContext(
  {
    llmSpanContext: {
      metrics: [new AnswerRelevancyMetric()],
      toolsMetrics: [new ToolCorrectnessMetric()],
    },
  },
  () => ask("What's the weather in Tokyo?"),
);

toolsMetrics and toolsMetricCollection are declared on llmSpanContext but target the tool spans in scope.

Streaming

streamText / streamObject produce the same span types as their non-streaming counterparts. Consume the stream fully so the span can end with a complete output:

ai-sdk-agent.ts
import { streamText } from "ai";
...

const { textStream } = streamText({
  model: openai("gpt-4o-mini"),
  prompt: "Count from 1 to 5.",
  experimental_telemetry: { isEnabled: true, tracer },
});

for await (const chunk of textStream) {
  // consume chunks so the span completes
}

API reference

configureAiSdkTracing({...}) accepts the following options and returns an OpenTelemetry Tracer to pass into experimental_telemetry.

FieldTypeDescription
apiKeystringConfident AI API key. Defaults to CONFIDENT_API_KEY.
otelEndpointstringOTLP endpoint override. Defaults to Confident AI's OTel URL.
namestringDefault trace name.
environmentstringTrace environment, e.g. "development" or "production".
traceMetricCollectionstringTrace-level metric collection, for online evals on live traffic.
isTestModebooleanForce in-process spans outside of an eval session (unit tests, local debug).
debugbooleanLog configure / flush diagnostics.

Also exported:

  • createDeepEvalProcessors(options?) β€” build the span processors without registering a global provider (bring-your-own NodeTracerProvider).
  • forceFlush() β€” flush the registered provider (useful before a process exits outside an eval).
  • DeepEvalSpanProcessor β€” the processor that materializes deepeval spans from ai.* OTel spans.

For native tracing helpers (observe, updateCurrentTrace, updateCurrentSpan, nextLlmSpan) see the tracing reference.

FAQs

Do I need an API key to evaluate an AI SDK app locally?
No. Without a CONFIDENT_API_KEY, the processor still builds every span in-process, so metrics, datasets, evalsIterator() and toPass() all work. The key buys one thing: uploading the trace to Confident AI.
Why don't I see any spans?
AI SDK telemetry is opt-in. Confirm you called configureAiSdkTracing(...) once and passed experimental_telemetry: { isEnabled: true, tracer } into the generateText / streamText / embed call. Without both, no ai.* spans are emitted.
Can I gate CI/CD on my AI SDK app's metrics?
Yes. Use expect(golden).toPass([...], { task: (g) => ask(g.input) }) in a Vitest test and run npx deepeval test run so a failing metric fails the build.
Is this the same as the Vercel AI SDK model page?
No. This page instruments your app's generateText / tools / embeddings for tracing and evals. The models page wraps an AI SDK LanguageModel as the judge behind a metric (AISDKModel).
Can I monitor an AI SDK app in production?
Yes. Keep configureAiSdkTracing(...) registered, pass telemetry on each call, and set threadId / userId in experimental_telemetry.metadata; those live traces support online evals on real traffic when a traceMetricCollection is set.

On this page