AI SDK 5.0が来た!型安全なデータストリーミングで何が変わる?

Vercel AI SDK 5.0の新機能を徹底解説。型安全なデータパーツ、カスタムトランスポート、DefaultChatTransportなど、実装例と共に紹介します。

AI SDK 5.0が来た!型安全なデータストリーミングで何が変わる?

2025 年、VercelがAI SDK 5.0をリリースしました。今回のメジャーアップデートでは、型安全性の強化、カスタムトランスポートのサポート、新しいエージェント制御機能など、開発者体験を大幅に向上させる機能が満載です。

なぜAI SDK 5.0が注目されるのか

従来の AI SDK 4.x では、ストリーミングデータの型安全性が不十分で、ランタイムチェックとタイプアサーションだらけのコードになりがちでした。また、WebSocket などの代替通信プロトコルを使用する場合、カスタム実装が困難でした。

AI SDK 5.0 はこれらの課題を根本的に解決し、エンドツーエンドの型安全性柔軟なトランスポートを実現しています。

主要な新機能

1. 型安全なデータパーツ

AI SDK 5.0 最大の特徴は、任意のデータを型安全にストリーミングできる「データパーツ」機能です。

// types/message.ts
import { UIMessage } from '@ai-sdk/ui-utils';

// カスタムデータパーツの型定義
type StatusUpdate = {
  type: 'data-status';
  data: {
    message: string;
    progress: number;
  };
};

type DocumentUpdate = {
  type: 'data-document';
  data: {
    content: string;
    version: number;
  };
};

// カスタムUIMessageの定義
export type CustomUIMessage = UIMessage<{
  dataParts: StatusUpdate | DocumentUpdate;
  metadata?: {
    responseTime?: number;
    model?: string;
  };
}>;

サーバー側では、これらの型を使用して安全にデータをストリーミングできます:

// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { createUIMessageStream } from '@ai-sdk/ui-utils';
import { CustomUIMessage } from '@/types/message';

export async function POST(req: Request) {
  const { messages } = await req.json();

  // 型付きUIメッセージストリームの作成
  const uiStream = createUIMessageStream<CustomUIMessage>();

  // モデルレスポンスのストリーミング開始
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    onFinish: async ({ text, usage }) => {
      // メタデータの追加
      uiStream.metadata({
        responseTime: Date.now(),
        model: 'gpt-4o',
      });
    },
  });

  // カスタムデータパーツのストリーミング
  (async () => {
    // ステータス更新のストリーミング
    await uiStream.part({
      type: 'data-status',
      data: {
        message: '処理中...',
        progress: 0,
      },
    });

    // プログレッシブな更新
    for (let i = 20; i <= 100; i += 20) {
      await new Promise((resolve) => setTimeout(resolve, 500));
      await uiStream.part({
        type: 'data-status',
        data: {
          message: `処理中... ${i}%`,
          progress: i,
        },
      });
    }

    // ドキュメント更新のストリーミング
    await uiStream.part({
      type: 'data-document',
      data: {
        content: '更新されたドキュメントコンテンツ',
        version: 1,
      },
    });

    // モデル結果をUIストリームにパイプ
    result.pipeTextStreamToResponse(uiStream);
  })();

  return uiStream.toResponse();
}

クライアント側では、完全な型安全性を維持しながらデータを受信できます:

// app/page.tsx
'use client';

import { useChat } from '@ai-sdk/react';
import { CustomUIMessage } from '@/types/message';

export default function Chat() {
  const { messages, input, handleSubmit, handleInputChange } =
    useChat<CustomUIMessage>({
      onData: (data) => {
        // 一時的なデータパーツをonDataコールバックで処理
        if (data.type === 'data-status') {
          console.log('Status:', data.data.message, data.data.progress);
        }
      }
    });

  return (
    <div className="flex flex-col w-full max-w-md mx-auto">
      {messages.map(message => (
        <div key={message.id} className="mb-4">
          <strong>{message.role}: </strong>

          {/* 型安全にパーツをレンダリング */}
          {message.parts.map((part, index) => {
            switch (part.type) {
              case 'text':
                return <span key={index}>{part.text}</span>;

              case 'data-status':
                return (
                  <div key={index} className="bg-blue-100 p-2 rounded">
                    ステータス: {part.data.message} ({part.data.progress}%)
                  </div>
                );

              case 'data-document':
                return (
                  <div key={index} className="bg-green-100 p-2 rounded">
                    ドキュメント v{part.data.version}: {part.data.content}
                  </div>
                );

              default:
                return null;
            }
          })}

          {/* メタデータの表示 */}
          {message.metadata && (
            <div className="text-sm text-gray-500">
              応答時間: {message.metadata.responseTime}ms
              モデル: {message.metadata.model}
            </div>
          )}
        </div>
      ))}

      <form onSubmit={handleSubmit} className="mt-4">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="メッセージを送信..."
          className="w-full p-2 border rounded"
        />
      </form>
    </div>
  );
}

2. DefaultChatTransportでカスタムエンドポイントを使う

AI SDK 5.0 では、DefaultChatTransport を使ってデフォルトの /api/chat 以外のエンドポイントを簡単に指定できるようになりました。独自の API パスや外部サービスのエンドポイントを使用する場合に便利です。

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

const MyChat = () => {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      // カスタムエンドポイントを指定(デフォルトは /api/chat)
      api: '/api/custom-chat',
      // 外部APIを使用する場合の例
      // api: 'https://api.example.com/v1/chat',
      headers: () => ({
        Authorization: `Bearer ${getAuthToken()}`,
        'X-User-ID': getCurrentUserId(),
        'X-Session-ID': getSessionId(),
      }),
      body: () => ({
        sessionId: getCurrentSessionId(),
        preferences: getUserPreferences(),
        context: {
          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
          locale: navigator.language,
        }
      }),
      credentials: () => 'include',
      // メッセージ送信前の準備処理
      prepareSendMessagesRequest({ messages, id }) {
        // 最後のメッセージのみを送信
        return {
          body: {
            message: messages[messages.length - 1],
            id,
            timestamp: Date.now()
          }
        };
      },
    }),
  });

  // 使用例
  return (
    // チャットUI
  );
};

複数のエンドポイントを切り替える例

開発環境と本番環境で異なるエンドポイントを使用したい場合や、複数の AI サービスを切り替えたい場合の実装例:

// 環境ごとのエンドポイント設定
const getApiEndpoint = () => {
  if (process.env.NODE_ENV === 'development') {
    return '/api/chat-dev';
  }
  if (process.env.NEXT_PUBLIC_USE_EXTERNAL_API === 'true') {
    return 'https://api.openai.com/v1/chat/completions';
  }
  return '/api/chat'; // デフォルトエンドポイント
};

// 動的にエンドポイントを切り替える
const chat = useChat({
  transport: new DefaultChatTransport({
    api: getApiEndpoint(),
    // URLパラメータを追加する場合
    api: `/api/chat/${modelId}?version=v2`,
  }),
});

WebSocketトランスポートの実装例

DefaultChatTransport の代わりに、WebSocket ベースのカスタムトランスポートを実装することも可能です:

// lib/websocket-transport.ts
import { Transport } from '@ai-sdk/ui-utils';

export class WebSocketTransport implements Transport {
  private ws: WebSocket | null = null;
  private messageQueue: any[] = [];
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;

  constructor(
    private url: string,
    private options?: {
      reconnectDelay?: number;
      heartbeatInterval?: number;
    }
  ) {}

  async connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.url);

      this.ws.onopen = () => {
        console.log('WebSocket接続確立');
        this.reconnectAttempts = 0;
        this.flushMessageQueue();
        this.startHeartbeat();
        resolve();
      };

      this.ws.onerror = (error) => {
        console.error('WebSocketエラー:', error);
        reject(error);
      };

      this.ws.onclose = () => {
        console.log('WebSocket接続終了');
        this.stopHeartbeat();
        this.attemptReconnect();
      };
    });
  }

  async send(message: any): Promise<ReadableStream> {
    // WebSocket接続がない場合はキューに追加
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      this.messageQueue.push(message);
      await this.connect();
    }

    // メッセージ送信
    this.ws!.send(JSON.stringify(message));

    // レスポンスストリームの作成
    return new ReadableStream({
      start(controller) {
        if (!this.ws) return;

        this.ws.onmessage = (event) => {
          const data = JSON.parse(event.data);

          // データをストリームにエンキュー
          controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`));

          // ストリーミング完了の判定
          if (data.done) {
            controller.close();
          }
        };
      },
    });
  }

  private flushMessageQueue(): void {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.ws!.send(JSON.stringify(message));
    }
  }

  private attemptReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('最大再接続試行回数に達しました');
      return;
    }

    this.reconnectAttempts++;
    const delay = this.options?.reconnectDelay || 1000;

    setTimeout(() => {
      console.log(`再接続試行 ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
      this.connect();
    }, delay * this.reconnectAttempts);
  }

  private heartbeatTimer: NodeJS.Timer | null = null;

  private startHeartbeat(): void {
    const interval = this.options?.heartbeatInterval || 30000;
    this.heartbeatTimer = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
      }
    }, interval);
  }

  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  disconnect(): void {
    this.stopHeartbeat();
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// 使用例
const chat = useChat({
  transport: new WebSocketTransport('wss://api.example.com/chat', {
    reconnectDelay: 1000,
    heartbeatInterval: 30000,
  }),
});

3. 強化されたツール機能

AI SDK 5.0 では、ツールの型安全性が大幅に向上し、入力の自動ストリーミングとエラー処理が改善されました。

import { tool, streamText } from 'ai';
import { z } from 'zod';
import { openai } from '@ai-sdk/openai';

// 天気取得ツールの定義
const weatherTool = tool({
  name: 'weather',
  description: '指定された都市の天気情報を取得',
  inputSchema: z.object({
    city: z.string().describe('都市名'),
    unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    condition: z.string(),
    humidity: z.number(),
    windSpeed: z.number(),
  }),
  execute: async ({ city, unit }) => {
    // 実際のAPI呼び出し
    const response = await fetch(`/api/weather?city=${city}&unit=${unit}`);
    const data = await response.json();

    return {
      temperature: data.temp,
      condition: data.weather,
      humidity: data.humidity,
      windSpeed: data.wind,
    };
  },
});

// 株価取得ツール
const stockPriceTool = tool({
  name: 'stockPrice',
  description: '株価情報を取得',
  inputSchema: z.object({
    symbol: z.string().describe('銘柄コード'),
    exchange: z.enum(['TSE', 'NYSE', 'NASDAQ']).optional(),
  }),
  outputSchema: z.object({
    price: z.number(),
    change: z.number(),
    changePercent: z.number(),
    volume: z.number(),
  }),
  execute: async ({ symbol, exchange }) => {
    // 株価API呼び出し
    const data = await fetchStockPrice(symbol, exchange);
    return data;
  },
});

// ツールを使用したストリーミング
const result = await streamText({
  model: openai('gpt-4o'),
  messages: [{ role: 'user', content: '東京の天気とトヨタの株価を教えて' }],
  tools: {
    weather: weatherTool,
    stockPrice: stockPriceTool,
  },
  // ツール呼び出しループの制御
  stopWhen: ({ toolCallCount }) => toolCallCount >= 3,
  // 各ステップの前処理
  prepareStep: ({ step, toolCallCount }) => {
    console.log(`ステップ ${step}: ツール呼び出し回数 ${toolCallCount}`);

    // 動的にパラメータを調整
    return {
      temperature: step === 1 ? 0.7 : 0.5,
      maxTokens: 1000,
    };
  },
  // ツールエラーの処理
  onToolError: ({ tool, error }) => {
    console.error(`ツール ${tool} でエラー:`, error);
    // エラーをLLMに返す
    return `${tool}の実行中にエラーが発生しました: ${error.message}`;
  },
});

// ツール呼び出しの入力は自動的にストリーミングされる
for await (const chunk of result.textStream) {
  console.log(chunk);
}

4. エージェント制御機能

AI SDK 5.0 では、エージェントの実行を細かく制御できる新しいプリミティブが導入されました。

import { Agent, streamAgent } from 'ai';
import { openai } from '@ai-sdk/openai';

// エージェントの定義
const researchAgent = new Agent({
  name: 'ResearchAgent',
  description: '調査タスクを実行するエージェント',
  model: openai('gpt-4o'),
  tools: {
    search: searchTool,
    analyze: analyzeTool,
    summarize: summarizeTool,
  },
  // エージェントの振る舞いを定義
  systemPrompt: `あなたは優秀な調査エージェントです。
    与えられたトピックについて包括的な調査を行い、
    信頼できる情報源から情報を収集して分析します。`,

  // 実行制御
  maxSteps: 10,
  stopWhen: ({ stepCount, toolCallCount }) => {
    // 条件を満たしたら停止
    return stepCount >= 5 || toolCallCount >= 8;
  },

  // 各ステップの準備
  prepareStep: ({ step, history }) => {
    // 履歴に基づいて次のステップを調整
    if (history.some((h) => h.tool === 'search')) {
      return {
        temperature: 0.3, // より確定的に
        systemPrompt: '収集した情報を分析してください',
      };
    }
    return { temperature: 0.7 };
  },
});

// エージェントの実行
const result = await streamAgent({
  agent: researchAgent,
  messages: [{ role: 'user', content: 'AI SDKの最新トレンドについて調査して' }],
  onStepStart: ({ step, tool }) => {
    console.log(`ステップ ${step} 開始: ツール ${tool}`);
  },
  onStepComplete: ({ step, result }) => {
    console.log(`ステップ ${step} 完了:`, result);
  },
});

// 結果のストリーミング
for await (const chunk of result.stream) {
  if (chunk.type === 'text') {
    process.stdout.write(chunk.text);
  } else if (chunk.type === 'tool-result') {
    console.log('\nツール結果:', chunk.result);
  }
}

5. フレームワークサポートの拡大

AI SDK 5.0 では、React以外のフレームワークでも同等の機能が利用可能になりました。

Vue.jsでの使用例

<template>
  <div class="chat-container">
    <div v-for="message in messages" :key="message.id" class="message">
      <strong>{{ message.role }}:</strong>
      <span v-for="(part, index) in message.parts" :key="index">
        <template v-if="part.type === 'text'">
          {{ part.text }}
        </template>
        <template v-else-if="part.type === 'data-status'">
          <div class="status-update">{{ part.data.message }} ({{ part.data.progress }}%)</div>
        </template>
      </span>
    </div>

    <form @submit.prevent="handleSubmit">
      <input v-model="input" placeholder="メッセージを入力..." :disabled="status !== 'ready'" />
      <button type="submit">送信</button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { useChat } from '@ai-sdk/vue';
import type { CustomUIMessage } from '@/types/message';

const { messages, input, handleSubmit, status } = useChat<CustomUIMessage>({
  api: '/api/chat',
  onData: (data) => {
    console.log('データ受信:', data);
  },
});
</script>

Svelteでの使用例

<script lang="ts">
  import { useChat } from '@ai-sdk/svelte';
  import type { CustomUIMessage } from '$lib/types/message';

  const chat = useChat<CustomUIMessage>({
    api: '/api/chat'
  });

  $: messages = chat.messages;
  $: input = chat.input;
  $: status = chat.status;
</script>

<div class="chat-container">
  {#each $messages as message}
    <div class="message">
      <strong>{message.role}:</strong>
      {#each message.parts as part}
        {#if part.type === 'text'}
          <span>{part.text}</span>
        {:else if part.type === 'data-status'}
          <div class="status">
            {part.data.message} ({part.data.progress}%)
          </div>
        {/if}
      {/each}
    </div>
  {/each}

  <form on:submit|preventDefault={chat.handleSubmit}>
    <input
      bind:value={$input}
      placeholder="メッセージを入力..."
      disabled={$status !== 'ready'}
    />
    <button type="submit">送信</button>
  </form>
</div>

マイグレーションガイド

AI SDK 4.x から 5.0 への移行は、自動化ツールを使用して簡単に行えます。

# 自動マイグレーション
npx @ai-sdk/codemod upgrade

# 手動インストール
npm install ai@latest @ai-sdk/openai@latest

主な破壊的変更

  1. パラメータ名の変更

    • maxTokensmaxOutputTokens
    • toolsinputSchema を持つ tool 定義へ
  2. メッセージ構造の変更

    • UIMessage と ModelMessage の明確な分離
    • パーツベースのメッセージ構造
  3. トランスポートアーキテクチャ

    • API オプションの廃止
    • トランスポートベースの設定へ移行

パフォーマンスの最適化

AI SDK 5.0 では、以下の最適化が可能です:

// 1. 選択的なデータストリーミング
const uiStream = createUIMessageStream({
  // 不要なデータをフィルタリング
  filter: (part) => part.type !== 'debug',
  // バッファリングの設定
  bufferSize: 10,
  // 圧縮の有効化
  compress: true,
});

// 2. メモリ効率的なストリーミング
const result = await streamText({
  model: openai('gpt-4o'),
  messages,
  // チャンクサイズの最適化
  streamOptions: {
    chunkSize: 1024,
    // 部分的な結果の早期フラッシュ
    flushInterval: 100,
  },
});

// 3. コネクションプーリング
const transport = new DefaultChatTransport({
  api: '/api/chat',
  // Keep-Aliveの有効化
  keepalive: true,
  // タイムアウトの設定
  timeout: 30000,
  // リトライ戦略
  retry: {
    attempts: 3,
    delay: 1000,
    backoff: 2,
  },
});

実践的な使用例:リアルタイムコラボレーションチャット

最後に、AI SDK 5.0 の機能を活用した実践的な例を紹介します。

// app/api/collaborative-chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { createUIMessageStream } from '@ai-sdk/ui-utils';

type CollaborativeMessage = UIMessage<{
  dataParts:
    | { type: 'user-typing'; data: { userId: string; text: string } }
    | { type: 'user-joined'; data: { userId: string; username: string } }
    | { type: 'document-update'; data: { content: string; version: number } }
    | { type: 'ai-thinking'; data: { status: string } };
  metadata?: {
    userId: string;
    timestamp: number;
    sessionId: string;
  };
}>;

export async function POST(req: Request) {
  const { messages, userId, sessionId } = await req.json();

  const uiStream = createUIMessageStream<CollaborativeMessage>();

  // 他のユーザーに通知
  await broadcastToSession(sessionId, {
    type: 'user-typing',
    data: { userId, text: messages[messages.length - 1].content },
  });

  // AIの思考状態をストリーミング
  uiStream.part({
    type: 'ai-thinking',
    data: { status: 'プロンプトを分析中...' },
  });

  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    tools: {
      updateDocument: documentUpdateTool,
      notifyUsers: notifyUsersTool,
    },
    onFinish: async ({ text, usage, toolResults }) => {
      // メタデータを追加
      uiStream.metadata({
        userId,
        timestamp: Date.now(),
        sessionId,
      });

      // ドキュメントの更新を配信
      if (toolResults?.updateDocument) {
        uiStream.part({
          type: 'document-update',
          data: toolResults.updateDocument,
        });
      }
    },
  });

  result.pipeTextStreamToResponse(uiStream);

  return uiStream.toResponse();
}

まとめ

AI SDK 5.0 は、以下の点で開発者体験を大幅に向上させています:

  • 型安全性の徹底: エンドツーエンドの型安全性により、ランタイムエラーを削減
  • 柔軟なトランスポート: DefaultChatTransport でカスタムエンドポイントの指定や様々な通信方式に対応
  • 強化されたツール機能: 自動ストリーミングとエラー処理の改善
  • エージェント制御: 細かな実行制御が可能に
  • マルチフレームワーク対応: React以外でも同等の機能を利用可能

これらの機能により、より堅牢でスケーラブルな AI アプリケーションの開発が可能になりました。特に型安全性とカスタムトランスポートの導入は、エンタープライズレベルのアプリケーション開発において大きな価値を提供します。

参考文献

📖 公式ドキュメント

📚 関連記事

BP

BitPluse Team

Building the future of software development, one line at a time.

Keep Learning

Explore more articles and expand your knowledge

View All Articles