---
title: "Streaming"
description: "How the handler streams, bounds tool loops, maps errors, and exposes post-turn telemetry."
---

`createChatHandler` streams the model response through the Vercel AI SDK UI
message protocol. The widget currently commits partial updates at a fixed 50 ms
throttle.

> **Note**
>
> `streamingThrottleMs` remains in the public type, but `<ChatWidget>` does not
> currently forward it to the internal interface. Treat 50 ms as fixed until a
> release wires the prop through.

## Bound tool loops

```ts
import { stepCountIs } from 'ai';

createChatHandler({
  getUserId: getChatUserId,
  model,
  store,
  stopWhen: stepCountIs(5),
});
```

`stopWhen` accepts an AI SDK stop condition. The default is bounded; use
`stepCountIs(1)` to prevent chaining.

## Error mapping

```ts
createChatHandler({
  getUserId: getChatUserId,
  model,
  store,
  onError: (error) => isAbortError(error)
    ? 'Response stopped.'
    : 'Something went wrong. Please try again.',
});
```

`onError` controls user-facing text. Server error logging remains enabled unless
`logErrors: false` is set. Disable it only when another sink captures the same
failures.

## Post-turn telemetry

`onChatFinish` runs after persistence. It can inspect AI SDK usage and provider
metadata; its own failures are logged and swallowed.

```ts
createChatHandler({
  getUserId: getChatUserId,
  model,
  store,
  onChatFinish: async ({ ctx, usage, providerMetadata }) => {
    await analytics.track({
      userId: ctx.userId,
      conversationId: ctx.conversationId,
      usage,
      providerMetadata,
    });
  },
});
```

Do not save the turn again in this hook. See
[Streaming reliability](/docs/guides/streaming-reliability) for proxy buffering,
timeouts, and deployment probes.
