Skip to content
mcpd
no daemon

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'
one generateText call with a tool
$ 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 spanmcpd kindWhat it holds
ai.generateTextagent_runThe whole call: the prompt and the final text
ai.generateText.doGeneratemodel_generationOne request to the provider, with model, tokens and cost
ai.toolCalltool_callOne 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 seeWhy, 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.

mcpd is free and open source. Start free at mcpd.sh , or run the daemon yourself.