---
title: "Usage Patterns"
description: "streamText, generateText, multi-step agents, RAG, and multiple models — every common AI SDK pattern wired into evlog."
canonical_url: "https://www.evlog.dev/use-cases/ai-sdk/usage"
---
# 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.

On Next.js, Nuxt/Nitro, SvelteKit, Hono, React Router, and oRPC, evlog defers wide-event emit for streaming responses (for example `text/event-stream` and AI SDK UI streams) until the body finishes, so late `ai` metadata stays on the same request event.

## streamText

The most common pattern is streaming chat with full observability:

<framework-tabs>

```typescript [Nuxt]server/api/chat.post.ts
import { useLogger } from 'evlog'
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()
})
```

```typescript [Nitro]routes/api/chat.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'

export default defineHandler(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()
})
```

```typescript [Next.js]app/api/chat/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'

export const POST = withEvlog(async (request: Request) => {
  const log = useLogger()

  const ai = createAILogger(log)
  const { messages } = await request.json()

  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()
})
```

```typescript [Hono]src/index.ts
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/chat', async (c) => {
  const log = c.get('log')

  const ai = createAILogger(log)
  const { messages } = await c.req.json()

  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()
})
```

```typescript [Express]src/index.ts
import { streamText } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/chat', async (req, res) => {
  const log = req.log

  const ai = createAILogger(log)
  const { messages } = req.body

  log.set({ action: 'chat', messagesCount: messages.length })

  const result = streamText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    messages,
    onFinish: ({ text }) => {
      saveConversation(text)
    },
  })

  result.pipeTextStreamToResponse(res)
})
```

</framework-tabs>

The middleware never touches your `onFinish` callback, so your code runs as usual.

## generateText

Synchronous generation. The middleware captures the result automatically:

<framework-tabs>

```typescript [Nuxt]server/api/summarize.post.ts
import { useLogger } from 'evlog'
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 }
})
```

```typescript [Nitro]routes/api/summarize.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
import { generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

export default defineHandler(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 }
})
```

```typescript [Next.js]app/api/summarize/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
import { generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

export const POST = withEvlog(async () => {
  const log = useLogger()

  const ai = createAILogger(log)

  const result = await generateText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    prompt: 'Summarize this document',
  })

  return Response.json({ text: result.text })
})
```

```typescript [Hono]src/index.ts
import { generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/summarize', async (c) => {
  const log = c.get('log')

  const ai = createAILogger(log)

  const result = await generateText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    prompt: 'Summarize this document',
  })

  return c.json({ text: result.text })
})
```

```typescript [Express]src/index.ts
import { generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/summarize', async (req, res) => {
  const log = req.log

  const ai = createAILogger(log)

  const result = await generateText({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    prompt: 'Summarize this document',
  })

  return res.json({ text: result.text })
})
```

</framework-tabs>

## Trace a multi-step agent

The middleware fires for each step automatically. Steps, tool calls, and tokens are accumulated across the agent loop:

<framework-tabs>

```typescript [Nuxt]server/api/agent.post.ts
import { useLogger } from 'evlog'
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
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,
  })
})
```

```typescript [Nitro]routes/api/agent.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
import { createAILogger } from 'evlog/ai'

export default defineHandler(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,
  })
})
```

```typescript [Next.js]app/api/agent/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
import { createAILogger } from 'evlog/ai'

export const POST = withEvlog(async (request: Request) => {
  const log = useLogger()

  const { messages } = await request.json()
  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,
  })
})
```

```typescript [Hono]src/index.ts
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/agent', async (c) => {
  const log = c.get('log')

  const { messages } = await c.req.json()
  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,
  })
})
```

```typescript [Express]src/index.ts
import { ToolLoopAgent, pipeAgentUIStreamToResponse, stepCountIs } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/agent', async (req, res) => {
  const log = req.log

  const { messages } = req.body
  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),
  })

  await pipeAgentUIStreamToResponse({
    response: res,
    agent,
    uiMessages: messages,
  })
})
```

</framework-tabs>

Wide event after a 3-step agent run:

```json [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
  }
}
```

<tip>

Pair this with [`createEvlogIntegration`](https://www.evlog.dev/use-cases/ai-sdk/telemetry) to also capture per-tool execution timing and the agent's total wall time.

</tip>

## RAG (embed + generate)

Embedding models use a different type that cannot be wrapped with middleware. Use `captureEmbed` instead:

<framework-tabs>

```typescript [Nuxt]server/api/rag.post.ts
import { useLogger } from 'evlog'
import { embed, generateText } from 'ai'
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 }
})
```

```typescript [Nitro]routes/api/rag.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
import { embed, generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

export default defineHandler(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 }
})
```

```typescript [Next.js]app/api/rag/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
import { embed, generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

export const POST = withEvlog(async () => {
  const log = useLogger()

  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 Response.json({ text: result.text })
})
```

```typescript [Hono]src/index.ts
import { embed, generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/rag', async (c) => {
  const log = c.get('log')

  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 c.json({ text: result.text })
})
```

```typescript [Express]src/index.ts
import { embed, generateText } from 'ai'
import { createAILogger } from 'evlog/ai'

app.post('/api/rag', async (req, res) => {
  const log = req.log

  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 res.json({ text: result.text })
})
```

</framework-tabs>

For `embedMany`, pass the batch count:

```typescript
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 })
```

## Wrap more than one model

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):

<code-group>

```typescript [server/api/chat.post.ts]
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 })
```

```json [Wide Event]
{
  "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
  }
}
```

</code-group>

## Pass a model object from any provider

`wrap()` accepts model objects from provider SDKs: both `LanguageModelV3` (AI SDK v6) and `LanguageModelV4` (AI SDK v7):

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

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

---

- [Overview](https://www.evlog.dev/use-cases/ai-sdk/overview)
- [Options](https://www.evlog.dev/use-cases/ai-sdk/options)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
