Vercel AI SDK 6 : AI SDK UI – チャットボット再開ストリーム

useChat はページのリロード後に進行中のストリームを再開することをサポートしています。この機能を仕様して長時間かかる生成を備えたアプリケーションを構築できます。

Vercel AI SDK 6 : AI SDK UI – チャットボット再開ストリーム

作成 : Masashi Okumura (@classcat.com)
作成日時 : 02/18/2026
バージョン : ai@6.0.90

* 本記事は ai-sdk.dev/docs の以下のページを参考にしています :

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

 

Vercel AI SDK 6.x : AI SDK UI – チャットボット再開ストリーム

useChat はページのリロード後に進行中のストリームを再開することをサポートしています。この機能を仕様して長時間かかる生成を備えたアプリケーションを構築できます。

 

ストリーム再開の仕組み

ストリーム再開は、アプリケーション内でメッセージとアクティブなストリームの永続性を必要とします。AI SDK はストレージに接続するためのツールを提供しますが、ストレージは開発者自身でセットアップする必要があります。

AI SDK は以下を提供します :

  • useChat の resume オプションは、アクティブなストリームに自動的に再接続します。

  • consumeSseStream コールバックを介した送信ストリームへのアクセス

  • 再開エンドポイントへの自動的な HTTP リクエスト

開発者が構築するもの :

  • 各チャットにどのストリームが属するか追跡するストレージ

  • UIMessage ストリームを保存するための Redis

  • 2 つの API エンドポイント: ストリームを作成するための POST、それらを再開するための GET

  • Redis ストレージを管理するための resumable-stream との統合

 

前提条件

チャットアプリケーションで再開可能な (resumable) ストリームを実装するには、以下が必要です :

  1. resumable-stream パッケージ – ストリーム用の publisher/subscriber メカニズムの処理

  2. Redis インスタンス – ストリームデータの保存 (e.g. Vercel 経由の Redis)

  3. 永続レイヤー – 各チャットに対してどのストリーム ID がアクティブか追跡 (e.g. データベース)

 

実装

1. クライアント側: ストリーム再開を有効にする

useChat フックの resume オプションを使用して、ストリーム再開を有効にします。resume が True の場合、フックはマウント時にアクティブなストリームへ再接続を自動的に試行します :

app/chat/[chatId]/chat.tsx

'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, type UIMessage } from 'ai';

export function Chat({
  chatData,
  resume = false,
}: {
  chatData: { id: string; messages: UIMessage[] };
  resume?: boolean;
}) {
  const { messages, sendMessage, status } = useChat({
    id: chatData.id,
    messages: chatData.messages,
    resume, // Enable automatic stream resumption
    transport: new DefaultChatTransport({
      // You must send the id of the chat
      prepareSendMessagesRequest: ({ id, messages }) => {
        return {
          body: {
            id,
            message: messages[messages.length - 1],
          },
        };
      },
    }),
  });

  return <div>{/* Your chat UI */}</div>;
}

resume を有効にすると、useChat フックはマウント時に /api/chat/[id]/stream に GET リクエストを送り、アクティブなストリームを確認して再開します。

再開可能なストリームを作成するための POST ハンドラの作成から始めましょう。

 

2. POST ハンドラの作成

POST ハンドラは consumeSseStream コールバックを使用して再開可能なストリームを作成します :

app/api/chat/route.ts

import { openai } from '@ai-sdk/openai';
import { readChat, saveChat } from '@util/chat-store';
import {
  convertToModelMessages,
  generateId,
  streamText,
  type UIMessage,
} from 'ai';
import { after } from 'next/server';
import { createResumableStreamContext } from 'resumable-stream';

export async function POST(req: Request) {
  const {
    message,
    id,
  }: {
    message: UIMessage | undefined;
    id: string;
  } = await req.json();

  const chat = await readChat(id);
  let messages = chat.messages;

  messages = [...messages, message!];

  // Clear any previous active stream and save the user message
  saveChat({ id, messages, activeStreamId: null });

  const result = streamText({
    model: 'openai/gpt-5-mini',
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse({
    originalMessages: messages,
    generateMessageId: generateId,
    onFinish: ({ messages }) => {
      // Clear the active stream when finished
      saveChat({ id, messages, activeStreamId: null });
    },
    async consumeSseStream({ stream }) {
      const streamId = generateId();

      // Create a resumable stream from the SSE stream
      const streamContext = createResumableStreamContext({ waitUntil: after });
      await streamContext.createNewResumableStream(streamId, () => stream);

      // Update the chat with the active stream ID
      saveChat({ id, activeStreamId: streamId });
    },
  });
}

 

3. GET ハンドラの実装

/api/chat/[id]/stream に以下のような GET ハンドラを作成します :

  1. ルート・パラメータからチャット ID を読み取る

  2. チャットデータをロードしてアクティブなストリームを確認する

  3. アクティブなストリームがない場合には 204 (No Content) を返す

  4. 一つが見つかれば、既存のストリームを再開する

app/api/chat/[id]/stream/route.ts

import { readChat } from '@util/chat-store';
import { UI_MESSAGE_STREAM_HEADERS } from 'ai';
import { after } from 'next/server';
import { createResumableStreamContext } from 'resumable-stream';

export async function GET(
  _: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;

  const chat = await readChat(id);

  if (chat.activeStreamId == null) {
    // no content response when there is no active stream
    return new Response(null, { status: 204 });
  }

  const streamContext = createResumableStreamContext({
    waitUntil: after,
  });

  return new Response(
    await streamContext.resumeExistingStream(chat.activeStreamId),
    { headers: UI_MESSAGE_STREAM_HEADERS },
  );
}

ℹ️ The after function from Next.js allows work to continue after the response has been sent. This ensures that the resumable stream persists in Redis even after the initial response is returned to the client, enabling reconnection later.

 

How it works

リクエストのライフライクル

上の図は再開可能なストリームのライフサイクル全体を示しています :

  1. ストリームの作成 : 新しいメッセージを送信すると、POST ハンドラは streamText を使用してレスポンスを生成します。consumeSseStream コールバックは一意な ID を持つ再開可能なストリームを作成し、resumable-stream パッケージを介して Redis に保存します。

  2. ストリーム追跡 : 永続性レイヤーは activeStreamId をチャットデータに保存します。

  3. クライアント再接続 : クライアントが再接続 (ページのリロード) すると、resume オプションは /api/chat/[id]/stream への GET リクエストをトリガーします。

  4. ストリームのリカバリ : GET ハンドラは activeStreamId を確認し、resumeExistingStream を使用して再接続します。アクティブなストリームがなければ、204 (No Content) レスポンスを返します。

  5. 完了クリーンアップ : ストリームが終了すると、onFinish コールバックは activeStreamId を null に設定してクリアします。

 

以上