---
title: "Access Metadata"
description: "Read AI metadata from your handler — persist it, surface it to end-users, bill against it, or stream incremental progress to the client."
canonical_url: "https://www.evlog.dev/use-cases/ai-sdk/metadata"
---
# Access Metadata

> Read AI metadata from your handler — persist it, surface it to end-users, bill against it, or stream incremental progress to the client.

The wide event already contains the full `ai` metadata, but you often want the same data inside your handler: to persist it, surface it to end-users, bill against it, or stream incremental progress to the client.

`AILogger` exposes three methods for that, with no need to touch internal state.

## `getMetadata()`: final snapshot

Returns a structured `AIMetadata` object that mirrors the `ai` field on the wide event. Safe to call at any point, including after the run completes or inside the AI SDK's `onFinish`:

```typescript [server/api/chat.post.ts]
import { useLogger } from 'evlog'
import { createAILogger } from 'evlog/ai'
import { generateText } from 'ai'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const ai = createAILogger(log, {
    cost: { 'claude-sonnet-4.6': { input: 3, output: 15 } },
  })

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

  const metadata = ai.getMetadata()

  await db.aiRuns.insert({
    userId: event.context.userId,
    model: metadata.model,
    inputTokens: metadata.inputTokens,
    outputTokens: metadata.outputTokens,
    estimatedCost: metadata.estimatedCost,
    finishReason: metadata.finishReason,
    responseId: metadata.responseId,
  })

  return { ok: true }
})
```

The snapshot is a fresh copy: mutating it never affects the underlying state or subsequent calls.

## `getEstimatedCost()`: quick cost check

Convenience for `getMetadata().estimatedCost`. Returns the cost in dollars, or `undefined` if no `cost` map was provided or the model is not in the map.

```typescript
const ai = createAILogger(log, {
  cost: { 'claude-sonnet-4.6': { input: 3, output: 15 } },
})

await generateText({ model: ai.wrap('anthropic/claude-sonnet-4.6'), prompt })

const cost = ai.getEstimatedCost()
console.log(`This call cost $${cost?.toFixed(4)}`)
```

## `onUpdate(callback)`: incremental updates

Subscribe to metadata updates. The callback fires every time the underlying state flushes:

- Once per step in multi-step agent runs
- Once per `captureEmbed` call
- On model errors
- On `createEvlogIntegration`'s `onEnd` (v7) or `onFinish` (v6)

Each invocation receives a fresh snapshot. Returns an unsubscribe function. Subscriber errors are isolated and never break the AI flow.

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

  ai.onUpdate((metadata) => {
    pushToClient(event, {
      type: 'ai-progress',
      step: metadata.steps,
      tokens: metadata.totalTokens,
      cost: metadata.estimatedCost,
    })
  })

  const agent = new ToolLoopAgent({
    model: ai.wrap('anthropic/claude-sonnet-4.6'),
    tools: { searchWeb, queryDatabase },
    stopWhen: stepCountIs(5),
  })

  return createAgentUIStreamResponse({ agent, uiMessages: messages })
})
```

For one-off cleanup:

```typescript
const off = ai.onUpdate((metadata) => { /* ... */ })
// later
off()
```

## `AIMetadata` shape

`AIMetadata` is a public type alias for the snapshot returned by `getMetadata()` and passed to `onUpdate` listeners. It has the same shape as the `ai` field on the wide event.

```typescript
import type { AIMetadata, AIMetadataListener } from 'evlog/ai'

function handleProgress(metadata: AIMetadata) {
  console.log(`${metadata.calls} calls, $${metadata.estimatedCost ?? 0}`)
}

const listener: AIMetadataListener = handleProgress
ai.onUpdate(listener)
```

## Every field the middleware records

Every field that may show up under `ai.*`:

<table>
<thead>
  <tr>
    <th>
      Wide event field
    </th>
    
    <th>
      Source
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        ai.calls
      </code>
    </td>
    
    <td>
      Call count
    </td>
    
    <td>
      Number of AI calls in this request
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.model
      </code>
    </td>
    
    <td>
      <code>
        response.modelId
      </code>
    </td>
    
    <td>
      Model that served the response
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.models
      </code>
    </td>
    
    <td>
      All model IDs
    </td>
    
    <td>
      Array of all models used (only when > 1)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.provider
      </code>
    </td>
    
    <td>
      <code>
        model.provider
      </code>
    </td>
    
    <td>
      Provider (<code>
        anthropic
      </code>
      
      , <code>
        openai
      </code>
      
      , <code>
        google
      </code>
      
      , etc.)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.inputTokens
      </code>
    </td>
    
    <td>
      <code>
        usage.inputTokens.total
      </code>
    </td>
    
    <td>
      Total input tokens across all calls
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.outputTokens
      </code>
    </td>
    
    <td>
      <code>
        usage.outputTokens.total
      </code>
    </td>
    
    <td>
      Total output tokens across all calls
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.totalTokens
      </code>
    </td>
    
    <td>
      Computed
    </td>
    
    <td>
      <code>
        inputTokens + outputTokens
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.cacheReadTokens
      </code>
    </td>
    
    <td>
      <code>
        usage.inputTokens.cacheRead
      </code>
    </td>
    
    <td>
      Tokens served from prompt cache
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.cacheWriteTokens
      </code>
    </td>
    
    <td>
      <code>
        usage.inputTokens.cacheWrite
      </code>
    </td>
    
    <td>
      Tokens written to prompt cache
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.reasoningTokens
      </code>
    </td>
    
    <td>
      <code>
        usage.outputTokens.reasoning
      </code>
    </td>
    
    <td>
      Reasoning tokens (extended thinking)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.finishReason
      </code>
    </td>
    
    <td>
      <code>
        finishReason.unified
      </code>
    </td>
    
    <td>
      Why generation ended (<code>
        stop
      </code>
      
      , <code>
        tool-calls
      </code>
      
      , etc.)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.toolCalls
      </code>
    </td>
    
    <td>
      Content / stream chunks
    </td>
    
    <td>
      <code>
        string[]
      </code>
      
       of tool names by default, or <code>
        Array<{ name, input }>
      </code>
      
       when <code>
        toolInputs
      </code>
      
       is enabled
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.responseId
      </code>
    </td>
    
    <td>
      <code>
        response.id
      </code>
    </td>
    
    <td>
      Provider-assigned response ID (e.g. Anthropic's <code>
        msg_...
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.prompt
      </code>
    </td>
    
    <td>
      First call's <code>
        params.prompt
      </code>
    </td>
    
    <td>
      Formatted text of the prompt sent to the first model call (only when the <code>
        prompt
      </code>
      
       option is enabled)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.output
      </code>
    </td>
    
    <td>
      Content / stream chunks
    </td>
    
    <td>
      Text generated by the last model call (only when the <code>
        output
      </code>
      
       option is enabled)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.steps
      </code>
    </td>
    
    <td>
      Step count
    </td>
    
    <td>
      Number of LLM calls (only when > 1)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.stepsUsage
      </code>
    </td>
    
    <td>
      Per-step accumulation
    </td>
    
    <td>
      Per-step token and tool call breakdown (only when > 1 step)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.msToFirstChunk
      </code>
    </td>
    
    <td>
      Stream timing
    </td>
    
    <td>
      Time to first text chunk (streaming only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.msToFinish
      </code>
    </td>
    
    <td>
      Stream timing
    </td>
    
    <td>
      Total stream duration (streaming only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.tokensPerSecond
      </code>
    </td>
    
    <td>
      Computed
    </td>
    
    <td>
      Output tokens per second (streaming only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.error
      </code>
    </td>
    
    <td>
      Error capture
    </td>
    
    <td>
      Error message if a model call fails
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.tools
      </code>
    </td>
    
    <td>
      Telemetry integration
    </td>
    
    <td>
      Per-tool <code>
        { name, durationMs, success, error? }
      </code>
      
       (requires <code>
        createEvlogIntegration
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.totalDurationMs
      </code>
    </td>
    
    <td>
      Telemetry integration
    </td>
    
    <td>
      Total generation wall time (requires <code>
        createEvlogIntegration
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.embedding
      </code>
    </td>
    
    <td>
      <code>
        captureEmbed
      </code>
    </td>
    
    <td>
      <code>
        { model?, tokens, dimensions?, count? }
      </code>
      
       — embedding metadata
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.estimatedCost
      </code>
    </td>
    
    <td>
      Computed
    </td>
    
    <td>
      Estimated cost in dollars (requires <code>
        cost
      </code>
      
       option)
    </td>
  </tr>
</tbody>
</table>

---

- [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.
