How to trace the Vercel AI SDK
The AI SDK records its calls as OpenTelemetry spans. Register its telemetry, add the OpenInference span processor, and export to mcpd: every generateText call arrives as a run with its prompt, tool calls, tokens and cost.
Last updated
When you finish, each generateText call is one trace in mcpd:
the call, every model step with its input, output, tokens and cost, and every
tool call with its arguments and result. You need Node.js 22 or later, AI SDK
7, and a running mcpd daemon on port 4317 (see the
Quickstart).
1. Install the packages
@ai-sdk/otel holds the AI SDK's OpenTelemetry integration.
@arizeai/openinference-vercel rewrites its spans in the
OpenInference conventions, which is where mcpd finds each step's input and
output.
install
npm install ai @ai-sdk/openai @ai-sdk/otel zod \
@arizeai/openinference-vercel \
@opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto 2. Register the telemetry
Start the OpenTelemetry SDK with the OpenInference processor in front of the
OTLP exporter, then register the AI SDK's LegacyOpenTelemetry
integration. Telemetry is on for every call once an integration is
registered.
tracing.ts
// tracing.ts: import this once, before the first AI SDK call.
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { OpenInferenceBatchSpanProcessor } from '@arizeai/openinference-vercel'
import { registerTelemetry } from 'ai'
import { LegacyOpenTelemetry } from '@ai-sdk/otel'
export const sdk = new NodeSDK({
serviceName: 'support-agent',
spanProcessors: [new OpenInferenceBatchSpanProcessor({ exporter: new OTLPTraceExporter() })],
})
sdk.start()
registerTelemetry(new LegacyOpenTelemetry()) 3. Point the exporter at mcpd
The OTLP exporter reads its endpoint from the environment. Name port 4317: mcpd serves OTLP there, and the HTTP exporter would otherwise try 4318.
environment
# The base URL. The exporter adds /v1/traces itself.
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Only when mcpd runs with --ingest-key.
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY" 4. Name the call
Your calls need no change. Add telemetry.functionId so the run
has a name you can filter on. Without it the root span is named
ai.generateText for every call.
agent.ts
import { sdk } from './tracing.ts'
import { generateText, stepCountIs, tool } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
const { text } = await generateText({
model: openai('gpt-4.1-mini'),
system: 'You answer questions about orders.',
prompt: 'Where is order 4417?',
tools: {
lookupOrder: tool({
description: 'Look up where an order is.',
inputSchema: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => ({ orderId, depot: 'Leeds' }),
}),
},
stopWhen: stepCountIs(5),
// Names the run: the root span becomes "ai.generateText support-agent".
telemetry: { functionId: 'support-agent' },
})
console.log(text)
// A script exits before the batch is sent unless you shut down. A server does not need this.
await sdk.shutdown() 5. Read the trace
Open localhost:4317 and select the newest run in Runs, or read it from the API:
read the run
TRACE=$(curl -s "http://localhost:4317/v1/api/traces?limit=1" | jq -r '.traces[0].trace_id')
curl -s "http://localhost:4317/v1/api/traces/$TRACE" \
| jq -r '.spans[] | [.name, .kind, .model, .usage.input_tokens, .cost.total_cost] | @tsv' $ curl -s "http://localhost:4317/v1/api/traces/$TRACE" | jq -r '.spans[] | [.name, .kind, .model, .usage.input_tokens, .cost.total_cost] | @tsv'ai.generateText support-agent agent_run gpt-4.1-mini ai.generateText.doGenerate model_generation gpt-4.1-mini 812 0.000384ai.toolCall tool_call ai.generateText.doGenerate model_generation gpt-4.1-mini 812 0.000384 | AI SDK span | mcpd kind | What it holds |
|---|---|---|
ai.generateText | agent_run | The whole call: the prompt and the final text |
ai.generateText.doGenerate | model_generation | One request to the provider, with model, tokens and cost |
ai.toolCall | tool_call | One tool execution, with its input and result |
The cost of the run is the sum of its model steps. Each step is priced from its token counts and mcpd's price book, as Cost accounting explains.
Troubleshooting
| What you see | Why, and what to do |
|---|---|
| The run's tokens and cost are twice the sum of its model steps | The process registered OpenTelemetry from @ai-sdk/otel. Register LegacyOpenTelemetry instead. |
| Spans arrive with an empty input and output |
The OpenInference processor is missing, so the prompt stays in AI SDK attributes such as
ai.prompt.messages. They are under Attributes in the span panel,
but full-text search does not read them.
|
| A prompt served from the provider's cache costs as much as an uncached one |
The AI SDK reports cached tokens as ai.usage.cachedInputTokens, a name mcpd does
not read yet. mcpd prices those tokens at the full input rate, so the figure is an upper
bound when caching is on.
|
| No trace arrives from a script | The process exited first. Call await sdk.shutdown() before it ends. |
| The exporter logs 401 or 403 | The daemon has ingest keys. Set OTEL_EXPORTER_OTLP_HEADERS as in step 3. |
Related guides
- How to log OpenAI API calls with tokens and cost
- How to group LLM traces into conversations
- How to track LLM cost per user
mcpd is free and open source. Start free at mcpd.sh , or run the daemon yourself.