AI SDK

Usage Patterns

streamText, generateText, multi-step agents, RAG, and multiple models — every common AI SDK pattern wired into evlog.

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:

server/api/chat.post.ts
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:

server/api/summarize.post.ts
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:

server/api/agent.post.ts
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:

Wide Event
{
  "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
  }
}
Pair this with 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:

server/api/rag.post.ts
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 })

Model Object Support

wrap() also accepts model objects from provider SDKs if you prefer explicit imports:

server/api/chat.post.ts
import { anthropic } from '@ai-sdk/anthropic'

const model = ai.wrap(anthropic('claude-sonnet-4.6'))