Vercel AI SDK
Trace the Vercel AI SDK with Neatlogs (TypeScript).
Wrap the ai module with wrapAISDK to capture generateText, streamText, generateObject, streamObject, embed, embedMany, and rerank — including tool calls, token counts, and model names.
Neatlogs supports AI SDK v6 and v7 (ai >=6 <8) through the same wrapAISDK(ai) API. The wrapper selects v6's experimental_telemetry or v7's telemetry option automatically.
Prerequisites
npm install neatlogs@latest ai @ai-sdk/openaiAI SDK v6 works with Node.js 18+. AI SDK v7 and its OpenTelemetry adapter require Node.js 22+. neatlogs installs @ai-sdk/otel automatically as an optional dependency; install @ai-sdk/otel@^1 explicitly only if your package-manager configuration disables optional dependencies.
Usage
import { init, shutdown } from 'neatlogs';
import { wrapAISDK } from 'neatlogs/ai';
import * as ai from 'ai';
import { openai } from '@ai-sdk/openai';
async function main() {
await init({ apiKey: process.env.NEATLOGS_API_KEY, workflowName: 'ai-sdk-demo' });
const { generateText } = wrapAISDK(ai);
const { text } = await generateText({
model: openai('gpt-4o-mini'),
prompt: 'In one sentence, what is the Vercel AI SDK?',
});
console.log(text);
await shutdown();
}
main().catch(console.error);wrapAISDK returns the same module shape in v6 and v7, so you destructure and call the functions exactly as before. generateText / generateObject / streamText / streamObject get a parent WORKFLOW span with LLM and TOOL spans nested under it; embed / embedMany / rerank are captured as CHAIN spans.
Agents are covered too: construct ToolLoopAgent (or v6's Experimental_Agent alias) from the wrapped module, for example const { ToolLoopAgent } = wrapAISDK(ai), and the agent's model and tool calls receive the same telemetry configuration.
wrapAISDK works regardless of which provider you plug into the AI SDK (OpenAI, Azure, Anthropic, Google, …) — it instruments the SDK functions, not the provider.
For streamText / streamObject, output and finish-reason metadata are captured from the AI SDK's onFinish callback (the tokens arrive after the call returns). Any onFinish/onError you pass is preserved and still runs. Structured output is mapped to the LLM child, model reasoning is retained as thinking, and v7 execute_tool spans retain the complete tool name, nested arguments, and result.
Instrument one call directly
createAITelemetry() has the same API in both majors; use the option name expected by the installed AI SDK:
import { createAITelemetry } from 'neatlogs/ai';
import { generateText } from 'ai';
// AI SDK v6
await generateText({
model,
prompt,
experimental_telemetry: createAITelemetry({ metadata: { userId } }),
});
// AI SDK v7
await generateText({
model,
prompt,
telemetry: createAITelemetry({ metadata: { userId } }),
});Sessions & end-user
Say you're building a chatbot on the Vercel AI SDK and want every turn's generateText or streamText generation to roll up under one session and point back to the person chatting. Since wrapAISDK already opens the trace root, wrap the turn in identify() and the generation picks up the session and end-user:
// Same sessionId every turn → one session; endUserId attributes it to your user.
await identify(
{ sessionId: `conv_${conversationId}`, endUserId: userId },
async () => {
await generateText({
model: openai('gpt-4o-mini'),
prompt: message,
});
},
);Not every run is a conversation. A standalone workflow — a one-off job, not a back-and-forth — is a single trace with no turns to group. You still attribute it to the customer it ran for, sourcing the id and any metadata from your own user or request object:
A standalone workflow must not carry a sessionId. Omit it entirely — Neatlogs sets sessionId = traceId, so the run is its own single-turn session, fully attributed to the end-user. Reusing one sessionId across unrelated runs wrongly folds them into a single multi-turn conversation.
async function summarize(document, user) {
// Standalone workflow — attribute to the end-user; do NOT pass sessionId.
return identify(
{ endUserId: String(user.id), endUserMetadata: { plan: user.plan } },
async () => {
const { text } = await generateText({
model: openai('gpt-4o-mini'),
prompt: `Summarize:\n${document}`,
});
return text;
},
);
}Sessions and End-User Identity walk through the complete picture.
