Vercel AI SDK
Trace AI SDK calls, model steps, and tools with @telemetry-dev/ai-sdk.
@telemetry-dev/ai-sdk uses the AI SDK telemetry integration API. It sends OpenTelemetry spans through its private provider and does not change your global OpenTelemetry setup.
Install
npm install @telemetry-dev/ai-sdk aipnpm add @telemetry-dev/ai-sdk aiyarn add @telemetry-dev/ai-sdk aibun add @telemetry-dev/ai-sdk aiThe package operates with ai >= 6.0.111 < 8. Use the package entry for your installed AI SDK major version.
Set the API key
Create a project at telemetry.dev. Then copy an API key from the project setup page. Keys look like td_live_....
export TELEMETRY_DEV_API_KEY=td_live_...
Without an API key, telemetryDev() returns an empty integration and does not send telemetry.
AI SDK 7
Add telemetryDev() to the telemetry.integrations array for each call:
import { generateText } from "ai";
import { telemetryDev } from "@telemetry-dev/ai-sdk";
const { text } = await generateText({
model,
prompt: "Summarize the incident report.",
runtimeContext: { userId: "u_123", sessionId: "s_456", tenant: "acme" },
telemetry: {
functionId: "summarize-incident",
includeRuntimeContext: { userId: true, sessionId: true, tenant: true },
integrations: [telemetryDev()],
},
});
The AI SDK does not send runtimeContext keys to integrations by default. Add each necessary key to telemetry.includeRuntimeContext.
The integration maps the included keys as follows:
| Context key | Span attribute |
|---|---|
userId |
user.id |
sessionId |
gen_ai.conversation.id |
| Other included keys | td.metadata.<key> |
Global registration
AI SDK 7 also accepts one global, concurrency-safe integration:
import { registerTelemetry } from "ai";
import { telemetryDev } from "@telemetry-dev/ai-sdk";
registerTelemetry(telemetryDev());
The integration keeps state by the AI SDK callId. Overlapping calls from one integration instance get separate spans and never share state.
AI SDK 7 also calls the integration context wrappers executeLanguageModelCall and executeTool. These wrappers keep provider requests, tool calls, and nested AI SDK calls in the correct span context.
Captured data
| AI SDK operation | Captured data |
|---|---|
generateText and streamText |
A root chat or invoke_agent span, one chat span per model step, messages, models, finish reasons, tokens, sampling options, and errors |
generateObject and streamObject |
A chat operation with gen_ai.output.type: "json", JSON input and output, usage, and errors |
Local tool execute calls |
An execute_tool span with the tool name, call ID, arguments, result, duration, and errors |
embed, embedMany, and rerank |
embeddings or rerank operations with model and usage data |
| Stream timing | gen_ai.client.operation.time_to_first_chunk for text and object operations on AI SDK 7 |
Provider-executed tools do not produce an execute_tool span. The AI SDK only calls tool hooks for local tools that have an execute function.
AI SDK 7 records input and output content by default. Set the AI SDK recordInputs or recordOutputs telemetry option to false to omit payloads. IDs, models, and usage remain.
For streams, drain the stream or await result.response. The AI SDK calls the terminal telemetry hook only after the stream settles.
Media wrappers
The AI SDK telemetry hooks do not cover image, speech, transcription, or video calls. Wrap the functions from your installed ai package instead:
import {
experimental_generateSpeech,
experimental_transcribe,
generateImage,
} from "ai";
import {
wrapGenerateImage,
wrapGenerateSpeech,
wrapTranscribe,
} from "@telemetry-dev/ai-sdk";
const options = { apiKey: process.env.TELEMETRY_DEV_API_KEY };
const tracedGenerateImage = wrapGenerateImage(generateImage, options);
const tracedGenerateSpeech = wrapGenerateSpeech(
experimental_generateSpeech,
options,
);
const tracedTranscribe = wrapTranscribe(experimental_transcribe, options);
wrapGenerateVideo wraps experimental_generateVideo in the same way. With AI SDK 6, import the wrappers from @telemetry-dev/ai-sdk/v6.
Each call makes a generate_content span with output type image, speech, text, or video. The span records the model and provider, image token usage when the AI SDK reports it, the image or video count, the transcription duration, and the speech input character count. Binary inputs and outputs are never captured.
The wrappers do not inherit options from telemetryDev(). Pass the same options as the second argument, or set the environment variables.
Batch wrappers
wrapStartBatch, wrapGetBatchStatus, wrapCancelBatch, and wrapGetBatchResults wrap the experimental AI SDK batch functions in the same way. They require ai >= 7.0.55 < 8 and are exported only from @telemetry-dev/ai-sdk.
Each network call gets its own span, with operation batch.submit, batch.status, batch.cancel, or batch.results. No span stays open for the life of the batch job. A results span stays open while you consume the returned stream or async iterable, so it can report the item count. If you never consume or cancel the results stream, its span does not finish.
AI SDK 6
Import the /v6 entry and add the integration to experimental_telemetry.integrations:
import { generateText } from "ai";
import { telemetryDev } from "@telemetry-dev/ai-sdk/v6";
const { text } = await generateText({
model,
prompt: "Summarize the incident report.",
experimental_telemetry: {
functionId: "summarize-incident",
metadata: { userId: "u_123", sessionId: "s_456", tenant: "acme" },
integrations: [telemetryDev()],
},
});
AI SDK 6 reads identity from experimental_telemetry.metadata. It does not use the AI SDK 7 runtime-context filter.
| Difference | AI SDK 7 | AI SDK 6 |
|---|---|---|
| Package entry | @telemetry-dev/ai-sdk |
@telemetry-dev/ai-sdk/v6 |
| Configuration | telemetry.integrations or registerTelemetry() |
experimental_telemetry.integrations |
| Operations | Text, object, embed, and rerank operations | generateText, streamText, and Agent only |
| TTFT | Captured for text and object operations | Not available from the hooks |
| Concurrent calls | One shared instance is safe | A shared global instance is not safe |
| Content controls | Honors recordInputs and recordOutputs |
Always records message and output content |
| Thrown errors | Flushes an error trace | Does not flush because onFinish does not run |
| Context wrappers | Includes model-call and tool context wrappers | No context wrappers |
Use a new telemetryDev() instance for each AI SDK 6 call. Do not register one shared instance for overlapping calls.
Options
Explicit options take precedence over environment variables and defaults.
| Option | Type | Environment variable | Default |
|---|---|---|---|
apiKey |
string |
TELEMETRY_DEV_API_KEY |
No key. The integration is inactive. |
baseUrl |
string |
TELEMETRY_DEV_BASE_URL |
https://ingest.telemetry.dev |
environment |
string |
TELEMETRY_DEV_ENVIRONMENT |
production |
serviceName |
string |
OTEL_SERVICE_NAME |
unknown_service |
fetch |
typeof fetch |
None | globalThis.fetch |
waitUntil |
(promise: Promise<unknown>) => void |
None | The terminal hook waits for export. |
onError |
(error: unknown) => void |
None | No callback |
The integration reports export errors through onError. Instrumentation errors do not enter the model call.
Serverless runtimes
waitUntil is the package’s only serverless-specific option. Pass the export promise to the platform lifetime function:
telemetryDev({ waitUntil: (promise) => ctx.waitUntil(promise) });
Without waitUntil, the terminal hook waits for the export. You do not need an instrumentation.ts file for this integration.
The package has a private BasicTracerProvider and does not register it globally. On runtimes without process.env, pass apiKey directly.