Every pattern below uses the same createAILogger(log) setup. Wrap the model with ai.wrap() and the middleware accumulates tokens, tools, and timing on the wide event automatically.
streamText
The most common pattern — streaming chat with full observability:
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const ai = createAILogger(log)
const { messages } = await readBody(event)
log.set({ action: 'chat', messagesCount: messages.length })
const result = streamText({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
messages,
onFinish: ({ text }) => {
saveConversation(text)
},
})
return result.toTextStreamResponse()
})
The middleware never touches your onFinish callback — your code runs as usual.
generateText
Synchronous generation. The middleware captures the result automatically:
import { generateText } from 'ai'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const ai = createAILogger(log)
const result = await generateText({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
prompt: 'Summarize this document',
})
return { text: result.text }
})
Multi-step Agents
The middleware fires for each step automatically. Steps, tool calls, and tokens are accumulated across the agent loop:
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const { messages } = await readBody(event)
const ai = createAILogger(log, {
toolInputs: { maxLength: 500 },
})
const agent = new ToolLoopAgent({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
tools: { searchWeb, queryDatabase },
stopWhen: stepCountIs(5),
})
return createAgentUIStreamResponse({
agent,
uiMessages: messages,
})
})
Wide event after a 3-step agent run:
{
"ai": {
"calls": 3,
"steps": 3,
"model": "claude-sonnet-4.6",
"provider": "anthropic",
"inputTokens": 4500,
"outputTokens": 1200,
"totalTokens": 5700,
"finishReason": "stop",
"toolCalls": [
{ "name": "searchWeb", "input": { "query": "TypeScript 6.0 features" } },
{ "name": "queryDatabase", "input": { "sql": "SELECT * FROM docs WHERE topic = 'typescript'" } },
{ "name": "searchWeb", "input": { "query": "TypeScript 6.0 release date" } }
],
"responseId": "msg_01XFDUDYJgAACzvnptvVoYEL",
"stepsUsage": [
{ "model": "claude-sonnet-4.6", "inputTokens": 1200, "outputTokens": 300, "toolCalls": ["searchWeb"] },
{ "model": "claude-sonnet-4.6", "inputTokens": 1500, "outputTokens": 400, "toolCalls": ["queryDatabase", "searchWeb"] },
{ "model": "claude-sonnet-4.6", "inputTokens": 1800, "outputTokens": 500 }
],
"msToFirstChunk": 312,
"msToFinish": 8200,
"tokensPerSecond": 146
}
}
createEvlogIntegration to also capture per-tool execution timing and the agent's total wall time.RAG (embed + generate)
Embedding models use a different type that cannot be wrapped with middleware. Use captureEmbed instead:
import { embed, generateText } from 'ai'
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const ai = createAILogger(log)
const { embedding, usage } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
})
ai.captureEmbed({
usage,
model: 'text-embedding-3-small',
dimensions: 1536,
})
const docs = await findSimilar(embedding)
const result = await generateText({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
prompt: buildPrompt(docs),
})
return { text: result.text }
})
For embedMany, pass the batch count:
const { embeddings, usage } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: documents,
})
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })
Multiple Models
Wrap each model separately — they share the same accumulator. When more than one model is used, the wide event includes both model (last model) and models (all unique models):
const ai = createAILogger(log)
const fast = ai.wrap('anthropic/claude-haiku-4.5')
const smart = ai.wrap('anthropic/claude-sonnet-4.6')
const classification = await generateText({ model: fast, prompt: classifyPrompt })
const response = await generateText({ model: smart, prompt: detailedPrompt })
{
"ai": {
"calls": 2,
"model": "claude-sonnet-4.6",
"models": ["claude-haiku-4.5", "claude-sonnet-4.6"],
"provider": "anthropic",
"inputTokens": 450,
"outputTokens": 300,
"totalTokens": 750
}
}
Model Object Support
wrap() also accepts model objects from provider SDKs if you prefer explicit imports:
import { anthropic } from '@ai-sdk/anthropic'
const model = ai.wrap(anthropic('claude-sonnet-4.6'))
Overview
Capture token usage, tool calls, model info, and streaming metrics from the Vercel AI SDK into wide events. Wrap your model and get full AI observability with one line.
Options
Configure tool input capture (with redaction and truncation), enable cost estimation per model, and handle errors during AI calls.