Vercel AI SDK
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 intoexperimental_telemetryon each call. - Evaluate traces or individual components with any
deepevalmetric. - 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/openaiNo 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.
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 span | deepeval span |
|---|---|
ai.generateText, ai.streamText, ai.generateObject, ai.streamObject, and their .doGenerate / .doStream children | LLM |
ai.toolCall | Tool |
ai.embed, ai.embedMany, and their .doEmbed children | Retriever |
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 answerThe 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.
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.tsIn 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.
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.
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(...):
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.
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/functionIdfor 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:
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:
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.
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:
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.
| Field | Type | Description |
|---|---|---|
apiKey | string | Confident AI API key. Defaults to CONFIDENT_API_KEY. |
otelEndpoint | string | OTLP endpoint override. Defaults to Confident AI's OTel URL. |
name | string | Default trace name. |
environment | string | Trace environment, e.g. "development" or "production". |
traceMetricCollection | string | Trace-level metric collection, for online evals on live traffic. |
isTestMode | boolean | Force in-process spans outside of an eval session (unit tests, local debug). |
debug | boolean | Log configure / flush diagnostics. |
Also exported:
createDeepEvalProcessors(options?)β build the span processors without registering a global provider (bring-your-ownNodeTracerProvider).forceFlush()β flush the registered provider (useful before a process exits outside an eval).DeepEvalSpanProcessorβ the processor that materializesdeepevalspans fromai.*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?
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?
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?
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?
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?
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.