Vercel AI SDK(React)実装ガイド
Vercel AI SDK の React 統合で LLM チャット UI を組むパターン。ストリーミングの基本と、型付きトランスポートの組み方までを 1 ページに統合。
Vercel AI SDK で React のチャット UI を作る
Vercel AI SDK(ai + @ai-sdk/react)で useChat を最小ボイラープレートで使い、サーバー側 streamText とつなぐ TypeScript 型安全パターン、tool calling、よく詰まる点を実装メモにまとめる。
検証日: 2026-05-10
使用バージョン:
ai@6.x/@ai-sdk/react@3.x対象: React でチャット UI を作る、LLM の streaming レスポンスを扱う、tool calling を組み込む
ヘッダ付き auth / 型付き UIMessage / 独自 transport が必要な場面は /articles/ai-sdk-react-typed-transport/ を参照。
触って試す
このデモは mock streaming(実 API なし)。
useChatと同じ UX を再現しています。
最小サンプル(クライアント)
useChat を 1 行呼ぶだけで messages / input / handleSubmit が揃う。送信先 endpoint は api で指定:
"use client"; // Next.js 13+ の app router 想定
import { useChat } from "@ai-sdk/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: "/api/chat",
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button disabled={isLoading}>Send</button>
</form>
</div>
);
}
useChat が messages 配列の状態管理 + streaming 受信 + 入力フォームの hook をまとめて提供。
最小サンプル(サーバー、Next.js Route Handler)
streamText でモデルにメッセージを渡し、toDataStreamResponse() で useChat が読める Streaming レスポンス形式に変換:
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai("gpt-4o"),
messages,
system: "あなたは親切なアシスタント。",
});
return result.toUIMessageStreamResponse();
}
toUIMessageStreamResponse() が SSE で useChat に必要な形式を返す。
モデルプロバイダの切替
@ai-sdk/openai を別プロバイダに差し替えるだけ:
| プロバイダ | パッケージ |
|---|---|
| OpenAI | @ai-sdk/openai |
| Anthropic Claude | @ai-sdk/anthropic |
| Google Gemini | @ai-sdk/google |
| Mistral | @ai-sdk/mistral |
| AWS Bedrock | @ai-sdk/amazon-bedrock |
| Ollama(ローカル) | ollama-ai-provider |
import { anthropic } from "@ai-sdk/anthropic";
const result = await streamText({ model: anthropic("claude-sonnet-4-6"), messages });
UI 側はモデル変更を意識しなくて良い(provider 抽象が効く)。
tool calling(関数呼び出し)
tools オブジェクトに description + Zod schema + execute を定義。LLM が必要と判断すれば関数が呼ばれる:
import { z } from "zod";
const result = await streamText({
model: openai("gpt-4o"),
messages,
tools: {
getWeather: {
description: "都市の現在気温を取得",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
const r = await fetch(`https://api.example.com/weather?q=${city}`);
return r.json();
},
},
},
});
Zod schema で引数の型を縛る → LLM が誤った JSON を返した場合は SDK 側で safeParse + retry。
ストリームの抜き取り(low-level)
UI を自作したい時、useChat を使わず生 stream を扱う:
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// SSE をパース
}
useChat に不満が出た時のフォールバック。普通は useChat で十分。
つまずいたポイント
api: "/api/chat"の path が違うと無音で動かない:Network タブで 200 OK + SSE が来ているか確認- CORS:同一オリジンなら問題なし、別ドメインなら server 側で
Access-Control-Allow-Origin設定 - API key を client に置くな:
process.env.OPENAI_API_KEYをサーバー側 (.env.server) のみに。クライアント露出はアカウント停止リスク messagesの構造変更:v3 系でparts配列に変わった場面がある(text 以外に tool result / image を持つ)。m.contentだけで描画すると一部の挙動が漏れる- Stream を途中で止める:
stop()を呼べる(useChatから取れる)。長いレスポンスの cancel UX に必要
評価
| 観点 | 評価 | コメント |
|---|---|---|
| 学習コスト | ◎ | useChat で 5 行のクライアント |
| プロバイダ抽象 | ◎ | OpenAI / Anthropic / Gemini を差し替え可 |
| TypeScript | ◎ | tool 引数 / 戻り値が end-to-end 型推論 |
| streaming UX | ◎ | SSE / 文字単位 streaming が標準 |
| バンドル | ○ | 30-50KB(プロバイダごとに小分け) |
向く / 向かないケース
- 向く: チャット UI、AI assistant、tool calling、多モデル横断、Next.js / SvelteKit / Nuxt
- 向かない: ベクター検索など LLM 以外の AI 機能(直接 API を叩く)
- 向かない: ストリームを使わないシンプルな one-shot 生成(普通の fetch で十分)
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…
Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門
FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…
【POD】React & Gatsby開発入門
Vercel AI SDK で auth ヘッダ + 型付き UIMessage を扱う
AI SDK の useChat を DefaultChatTransport で拡張し、Firebase ID トークンを毎回付ける auth パターン、独自の type 付き request body、UIMessage の generic 型共有、tool calling まで踏み込んだ実装メモ。
検証日: 2026-05-10
使用バージョン:
ai@6.x/@ai-sdk/react@3.x対象: useChat 入門は通っている、認証や複数チャットサービスを扱う本番アプリで踏み込みたい人
入門記事は別途。本稿は「1 つの API に複数のチャットサービスを乗せる」「毎リクエストに認証を付ける」「メッセージ型を server / client で共有する」など、本番運用で必要になる中級パターン。
1. DefaultChatTransport で送信前に介入
useChat はデフォルトで POST /api/chat を叩くが、auth ヘッダの付与・request body のカスタマイズが必要なら DefaultChatTransport を渡して prepareSendMessagesRequest で介入する。
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
export function useImageFeatureChat(args: { imageUrl: string; locale: string }) {
const { user } = useFirebaseUser(); // 例: 認証コンテキスト
return useChat<MyChatUIMessage>({
transport: new DefaultChatTransport({
api: "/api/chat",
prepareSendMessagesRequest: async ({ messages }) => {
const idToken = await user?.getIdToken();
if (!idToken) throw new Error("ログインが必要");
return {
body: {
messages,
type: "image-feature-extraction", // ← サーバ側でディスパッチに使う識別子
...args,
},
headers: { Authorization: `Bearer ${idToken}` },
};
},
}),
});
}
ポイント:
prepareSendMessagesRequestは async:Firebase のgetIdToken()のように Promise で来る token を待って付与できるbodyに独自フィールドを追加:type/argsを一緒に送れば、1 つの API endpoint で複数サービスをディスパッチ可能headersでAuthorization:JWT / OAuth token を毎回付与- エラーは throw:認証失敗時に throw すれば、useChat の
errorstate に伝播する
2. Server 側: 1 つの endpoint で複数サービスをディスパッチ
req.body 内の serviceId を見て、それぞれ別の system prompt / モデル / tools セットを streamText に渡す:
import { streamText, type UIMessage } from "ai";
import { z } from "zod";
import { google } from "@ai-sdk/google";
const RequestSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("image-feature-extraction"),
imageUrl: z.string().url(),
locale: z.string(),
messages: z.array(z.any()),
}),
z.object({
type: z.literal("review-summarization"),
locationId: z.string(),
messages: z.array(z.any()),
}),
]);
export async function POST(req: Request) {
const auth = req.headers.get("authorization");
const userId = await verifyIdToken(auth); // 自前
const body = RequestSchema.parse(await req.json());
switch (body.type) {
case "image-feature-extraction": {
const result = await streamText({
model: google("gemini-2.5-flash"),
system: `画像から特徴量を抽出する...(locale=${body.locale})`,
messages: body.messages as UIMessage[],
});
return result.toUIMessageStreamResponse();
}
case "review-summarization": {
const result = await streamText({
model: google("gemini-2.5-pro"),
system: "レビューを要約する...",
messages: body.messages as UIMessage[],
});
return result.toUIMessageStreamResponse();
}
}
}
ポイント:
z.discriminatedUnion("type", [...])でサービスごとの引数を型付きで束ねる- switch で出口を分岐、それぞれ別モデル / 別 system prompt
- 共通の auth verification を 1 箇所で
toUIMessageStreamResponse()でクライアントuseChat互換の SSE を返す
3. UIMessage 型を server / client で共有
@ai-sdk/react の useChat は generic で UIMessage 型を絞れる。tool calling の戻り値や custom annotations を含めたい時に効く。
import type { UIMessage } from "ai";
export type ImageFeatureChatUIMessage = UIMessage<
{
// metadata 部分(annotations 用)
extractedFeatures?: { tag: string; confidence: number }[];
},
{
// tool 結果の型
"extract-features": { features: { tag: string; confidence: number }[] };
}
>;
const { messages, sendMessage } = useChat<ImageFeatureChatUIMessage>({ /* ... */ });
// messages の各 entry が ImageFeatureChatUIMessage として型推論される
messages.forEach((m) => {
if (m.role === "assistant") {
// m.metadata.extractedFeatures を補完で出る
m.parts.forEach((p) => {
if (p.type === "tool-extract-features") {
// tool の結果が型付きで取れる
}
});
}
});
server / client で 同じ generic 型を import すれば、API 仕様変更時に両側に型エラーが波及する。
4. tool calling の typed 化
server 側で tools に Zod schema を渡し、execute 関数で実装する。LLM が誤った JSON を返した場合は SDK 内で safeParse + retry を試みる。
import { tool } from "ai";
const result = await streamText({
model: google("gemini-2.5-flash"),
messages,
tools: {
extractFeatures: tool({
description: "画像から特徴量タグを抽出",
inputSchema: z.object({
imageUrl: z.string().url(),
maxTags: z.number().int().min(1).max(20).default(10),
}),
execute: async ({ imageUrl, maxTags }) => {
const features = await runVisionModel(imageUrl, maxTags);
return { features }; // → クライアント側で {tag, confidence}[] に narrow される
},
}),
},
});
UI 側では m.parts に tool-extractFeatures の entry が混ざってくるので、type narrowing で取り出す。
5. ストリーム中断と error handling
長文生成の途中でユーザがキャンセルしたい場合:
const { messages, sendMessage, status, stop, error } = useChat({ /* ... */ });
// 状態
status === "streaming" // 受信中
status === "submitted" // 送信したがまだ stream 開始前
status === "ready" // 完了 / 待機中
status === "error" // エラー
// 中断
<button onClick={stop} disabled={status !== "streaming"}>停止</button>
// エラー
{error && <div className="text-red-600 text-sm">{error.message}</div>}
サーバ側で例外が起きると stream は途中で切れて error state にメッセージが入る。ユーザに retry を促す UI を出す。
6. プロバイダ抽象を使い分ける
複数プロバイダを切り替える典型パターン:
import { google } from "@ai-sdk/google";
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
const models = {
cheap: google("gemini-2.5-flash"),
smart: openai("gpt-4o"),
long_context: anthropic("claude-sonnet-4-6"),
} as const;
const result = await streamText({ model: models[picked], messages });
tools / messages の部分は変わらない。コスト最適化(cheap → smart に escalate) や 特化(long_context は要約用) を 1 行差で切り替えられる。
つまずいたポイント
prepareSendMessagesRequestの型が緩い:messagesはUIMessage[]だが、戻り値のbodyはunknown寄りなのでsatisfiesを使って型を縛るUIMessageのparts型変更:v3 系でm.contentが deprecate、m.parts配列に rendering の責任が移った。古い記事のm.content直アクセスは動かないtoUIMessageStreamResponse()のヘッダ:カスタム CORS / cache-control を足したい時はresult.toUIMessageStreamResponse({ headers: { ... } })statusがstreamingのままで止まる:server 側で例外を catch せずに throw すると stream が hang。route handler 全体を try / catchtoolの inputSchema は Zod 必須:standard schema 系(valibot 等)でも動くが、SDK のドキュメントは Zod 想定
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…
Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス
Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…
Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門
FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発
新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…