- AI ⚙️
Vercel AI SDK入門|generateText・streamText
Vercel AI SDK(npm のパッケージ名は ai)は、TypeScript から OpenAI・Anthropic・Google などの LLM を同じ書き方で呼べるライブラリです。 文をまとめて受け取るなら generateText、少しずつ受け取るなら streamText を使います。
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai'; // 環境変数 OPENAI_API_KEY を読む
const { text } = await generateText({
model: openai('gpt-6-luna'),
instructions: '短く答えます。',
prompt: 'AI SDKを一言で',
});
console.log(text);
| やりたいこと | 書き方(v7) | 見出し |
|---|---|---|
| まとめて受け取る | generateText({ model, prompt }) |
generateText |
| 少しずつ受け取る | streamText(...) → textStream |
streamText |
| 決まった形の JSON | generateText({ output: Output.object({ schema }) }) |
構造化出力 |
| 関数を呼ばせる | tool({ inputSchema, execute }) + stopWhen |
ツール |
| チャット画面を作る | Route Handler + useChat |
Next.js |
| 失敗に備える | maxRetries・onError・APICallError |
エラー |
| API なしでテスト | MockLanguageModelV4(ai/test) |
テスト |
確認環境:ai 7.0.138(npm の最新版)・@ai-sdk/openai 4.0.92・@ai-sdk/anthropic 4.0.79・@ai-sdk/openai-compatible 3.0.68・@ai-sdk/react 4.0.141・zod 4.6.7・Next.js 16.4.0・React 19.3.0・Node.js 22.22.2(Linux)。SDK が送る URL と JSON、結果のプロパティ、ストリームの区切り、ツールの往復、例外と再試行の回数は、baseURL を手元の擬似エンドポイント(OpenAI と Anthropic の API の形を返す小さな HTTP サーバー)に向けて確かめました。返答の文は擬似エンドポイントが返した固定の文です。Route Handler は next build → next start で動かし、useChat の画面は Chromium で操作して確かめました。既定値と非推奨の情報はパッケージの型定義(index.d.ts)で、仕様は ai-sdk.dev で確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vercel AI SDK入門|generateText・streamText」
インストールとパッケージ構成
npm i ai @ai-sdk/openai zod # 本体・OpenAI・スキーマ
npm i @ai-sdk/anthropic # Anthropic(Claude)も使う
npm i @ai-sdk/react # React の useChat
| パッケージ | 中身 | 手元の版 |
|---|---|---|
ai |
generateText・streamText・tool・Output・UI 用のストリーム |
7.0.138 |
@ai-sdk/openai |
openai・createOpenAI |
4.0.92 |
@ai-sdk/anthropic |
anthropic・createAnthropic |
4.0.79 |
@ai-sdk/google |
Google(Gemini) | 4.0.94 |
@ai-sdk/amazon-bedrock・@ai-sdk/azure |
Bedrock・Azure OpenAI | 5.0.114・4.0.100 |
@ai-sdk/openai-compatible |
OpenAI 互換のサーバー(社内 LLM・LM Studio など) | 3.0.68 |
@ai-sdk/react |
useChat・useCompletion |
4.0.141 |
@ai-sdk/vue・@ai-sdk/svelte・@ai-sdk/angular |
各フレームワークの UI 部品 | 4.0.138 など |
ai 7.0.138 の package.json は "engines": {"node": ">=22"} でした。移行ガイドでも、v7 は Node.js 22 以上が必要で、Node.js 18・20 は対象外です。また、v7 のパッケージは ESM(import で読む形式)だけになりました。zod は ^3.25.76 || ^4.1.8 が peer 依存です。
プロバイダーの書き方と環境変数
| 書き方 | 送り先・読む環境変数 |
|---|---|
openai('モデル名') |
Responses API(POST /v1/responses)。OPENAI_API_KEY・OPENAI_BASE_URL |
openai.chat('モデル名') |
Chat Completions(POST /v1/chat/completions) |
anthropic('モデル名') |
Messages API(POST /v1/messages)。ANTHROPIC_API_KEY・ANTHROPIC_BASE_URL |
createOpenAI({ baseURL, apiKey }) |
送り先とキーを指定したプロバイダーを作る |
createOpenAICompatible({ name, baseURL }) |
OpenAI 互換のサーバー |
'openai/gpt-6-luna'(文字列) |
Vercel の AI Gateway 経由。AI_GATEWAY_API_KEY |
model に文字列を渡すと、Vercel の AI Gateway に送られます。 キーが無いと次のエラーになりました。各社に直接送るなら、上のようにプロバイダーの関数で作ったモデルを渡します。
GatewayAuthenticationError: Unauthenticated request to AI Gateway.
To authenticate, set the AI_GATEWAY_API_KEY environment variable with your API key.
Alternatively, you can use a provider module instead of the AI Gateway.
モデル名と料金は、各社の公式ドキュメントで確かめます。OpenAI の API 自体は関連記事「OpenAI API Python入門|Responses APIのコピペ集」で扱っています。
generateText:まとめて受け取る
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const result = await generateText({
model: openai('gpt-6-luna'),
instructions: '短く答えます。', // v6 までの system
prompt: 'AI SDKを一言で',
});
console.log(result.text);
console.log(result.finishReason); // 'stop'
console.log(result.usage.totalTokens);
console.log(result.steps.length);
手元の結果と、送られた JSON です。instructions は Responses API の developer のメッセージになっていました。
AI SDKは、TypeScriptでLLMを呼ぶためのライブラリです。
stop
32
1
POST /v1/responses
{"model": "gpt-6-luna", "input": [{"role": "developer", "content": "短く答える"}, {"role": "user", "content": [{"type": "input_text", "text": "AI SDKを一言で"}]}]}
| よく使う結果のプロパティ | 中身 |
|---|---|
text |
最後のステップの文 |
finishReason |
stop・length・tool-calls・content-filter・error・other |
usage |
トークン数(v7 では全ステップの合計) |
steps |
ステップごとの結果(ツールを使うと複数) |
toolCalls・toolResults |
ツールの呼び出しと結果 |
output |
構造化出力の結果(output を指定したとき) |
responseMessages |
モデルとツールが足したメッセージ(履歴に積む用) |
usage には inputTokens・outputTokens・totalTokens に加えて、inputTokenDetails.cacheReadTokens・outputTokenDetails.reasoningTokens も入っていました。
よく使う引数
| 引数 | 意味 | 既定(型定義) |
|---|---|---|
instructions |
システムの指示 | なし |
prompt / messages |
文か、会話のメッセージの配列 | - |
maxOutputTokens |
出力の上限 | プロバイダーの既定 |
temperature |
ランダムさ | プロバイダーの既定 |
maxRetries |
再試行の回数 | 2 |
abortSignal |
途中で止める | なし |
providerOptions |
各社だけの設定 | なし |
推論モデルとして扱われる名前では、temperature は送られず、警告が出ました。 openai.chat('gpt-6-luna') に temperature: 0, maxOutputTokens: 100 を渡したとき、送られたのは max_completion_tokens: 100 だけでした。
Warning: AI SDK Warning (openai.chat / gpt-6-luna): The feature "temperature" is not supported. temperature is not supported for reasoning models
Anthropic に切り替える
import { anthropic } from '@ai-sdk/anthropic';
const { text } = await generateText({
model: anthropic('claude-sonnet-4-6'),
instructions: '短く答えます。',
prompt: 'AI SDKを一言で',
});
変えたのは model の1行だけです。送られたのは Messages API の形で、instructions は system に、max_tokens はモデルに合わせた値(ここでは 128000)になっていました。
POST /v1/messages
{"model": "claude-sonnet-4-6", "max_tokens": 128000, "system": [{"type": "text", "text": "短く答える"}], "messages": [{"role": "user", "content": [{"type": "text", "text": "AI SDKを一言で"}]}]}
Claude の API 自体は、関連記事「Claude API Python入門|Anthropic SDKコピペレシピ集」で扱っています。
v7 では messages に system を入れるとエラー
await generateText({
model: openai('gpt-6-luna'),
messages: [{ role: 'system', content: '...' }, { role: 'user', content: 'hi' }],
});
InvalidPromptError: Invalid prompt: System messages are not allowed in the prompt or messages fields. Use the instructions option instead.
移行ガイドでは、信頼できる履歴だけに allowSystemInMessages: true を付ける方法が案内されています。利用者の入力から system を差し込まれないための既定です。
streamText:少しずつ受け取る
import { streamText } from 'ai';
const result = streamText({
model: openai('gpt-6-luna'),
prompt: 'AI SDKを一言で',
onEnd({ text, finishReason, usage }) { // v6 までの onFinish
console.log('終了', finishReason, usage.totalTokens);
},
});
for await (const delta of result.textStream) {
process.stdout.write(delta);
}
console.log(await result.text); // 全文(Promise)
streamText には await を付けません。擬似エンドポイントから3つに分けて返したとき、textStream から "AI SDKは、T"・"ypeScriptでLLM"・"を呼ぶためのライブラリです。" の3つが届き、その後に onEnd が stop 32 で呼ばれました。
すべてのイベントを見る:stream
ツールの呼び出しや終了の理由も扱うときは、result.stream(v6 までの fullStream)を使います。
for await (const part of result.stream) {
if (part.type === 'text-delta') process.stdout.write(part.text);
if (part.type === 'error') console.error(part.error);
}
文だけを返したときに届いた type の順番です。
start → start-step → text-start → text-delta ×3 → text-end → finish-step → finish
HTTP のレスポンスにする
| 関数(v7) | 返す形 | 使いどころ |
|---|---|---|
createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) }) |
SSE(text/event-stream) |
useChat と組み合わせる |
createTextStreamResponse({ stream: toTextStream({ stream: result.stream }) }) |
文字だけ(text/plain; charset=utf-8) |
文を流すだけの API |
pipeUIMessageStreamToResponse(...)・pipeTextStreamToResponse(...) |
Node.js の ServerResponse に書く |
Express など |
UI 用のレスポンスは、次のヘッダーと SSE の本文でした。
200 cache-control: no-cache | connection: keep-alive | content-type: text/event-stream | x-accel-buffering: no | x-vercel-ai-ui-message-stream: v1
data: {"type":"start"}
data: {"type":"start-step"}
data: {"type":"text-start","id":"msg_003",...}
data: {"type":"text-delta","id":"msg_003","delta":"AI SDKは、T"}
...
data: {"type":"finish","finishReason":"stop"}
data: [DONE]
v6 までの result.toUIMessageStreamResponse()・toTextStreamResponse() も 7.0.138 で動きましたが、型定義では「次のメジャー版で削除」と書かれた非推奨でした。
構造化出力:Output.object
v7 では、決まった形の JSON は generateText に output: Output.object({ schema }) を付けて受け取ります。 generateObject は非推奨になりました。
import { generateText, Output } from 'ai';
import { z } from 'zod';
const Ticket = z.object({
category: z.enum(['障害', '質問', '要望']),
priority: z.enum(['高', '中', '低']),
summary: z.string().describe('30文字以内の要約'),
});
const { output } = await generateText({
model: openai('gpt-6-luna'),
output: Output.object({ schema: Ticket }),
prompt: 'VPNに朝からつながらず仕事が止まっています',
});
console.log(output); // 型は { category: '障害' | '質問' | '要望'; ... }
{ category: '障害', priority: '高', summary: 'VPNに接続できない' }
送られた text.format:
{"type": "json_schema", "strict": true, "name": "response", "schema": {"type": "object", "properties": {"category": {"type": "string", "enum": ["障害", "質問", "要望"]}, ..., "summary": {"type": "string", "description": "30文字以内の要約"}}, "required": ["category", "priority", "summary"], "additionalProperties": false}}
zod のスキーマは JSON Schema に変えて送られ、.describe() は description になりました。Output には object・array・choice・json・text がありました。
| 書き方 | 結果 |
|---|---|
generateText({ output: Output.object(...) }) |
result.output に型つきのオブジェクト |
streamText({ output: Output.object(...) }) |
partialOutputStream で途中の形が届く |
generateObject({ schema })(非推奨) |
動くが DeprecationWarning(下) |
[AISDK_DEP_GENERATE_OBJECT] DeprecationWarning: AI SDK Warning: Deprecated: "generateObject". Use generateText with an output setting instead.
streamText で受けると、partialOutputStream から {} → {"category":"障害"} → 全部そろった形、の順に届きました。返答がスキーマに合わないときは NoObjectGeneratedError(No object generated: response did not match schema.)になり、error.text にモデルが返した元の文が入っていました。構造化出力の方式の違いは、関連記事「LLMの構造化出力とJSON|4方式の違いとPydantic検証の実行結果」で、zod は関連記事「Zodの使い方|v4対応のAPI早見表とエラー出力つき実例3つ」で扱っています。
ツール呼び出し:tool と stopWhen
ツールは、モデルに呼ばせる関数です。description(説明)・inputSchema(引数の形)・execute(実行する処理)で作ります。
import { generateText, tool, isStepCount } from 'ai';
import { z } from 'zod';
const getWeather = tool({
description: '都市名を受け取り、今日の天気を返す',
inputSchema: z.object({ city: z.string().describe('都市名') }),
execute: async ({ city }) => ({ city, weather: '晴れ', maxTemp: 22 }), // 本当は天気 API を呼ぶ
});
const result = await generateText({
model: openai('gpt-6-luna'),
tools: { get_weather: getWeather },
stopWhen: isStepCount(5), // ツール → 回答 を最大5ステップ
prompt: '東京の天気は?',
});
console.log(result.text);
console.log(result.steps.length);
stopWhen を付けないと、ツールを実行したところ(1ステップ)で止まり、text は空でした。 generateText の stopWhen の既定は isStepCount(1) です(型定義)。
| 指定 | steps | text | finishReason |
|---|---|---|---|
stopWhen なし |
1 | "" |
tool-calls(toolResults に結果だけ入る) |
stopWhen: isStepCount(5) |
2 | ツールの結果を使った回答 | stop |
2ステップのときに送られたリクエストです。1回目にツールの定義が、2回目にツールの呼び出しと結果が足されていました。
1回目 {"model": "gpt-6-luna", "input": [{"role": "user", ...}], "tools": [{"type": "function", "name": "get_weather", "description": "都市名を受け取り、今日の天気を返す", "parameters": {"type": "object", "properties": {"city": {"type": "string", "description": "都市名"}}, "required": ["city"], "additionalProperties": false}, "strict": false}], "tool_choice": "auto"}
2回目 "input": [..., {"type": "function_call", "call_id": "call_007", "name": "get_weather", "arguments": "{\"city\":\"東京\"}"}, {"type": "function_call_output", "call_id": "call_007", "output": "{\"city\":\"東京\",\"weather\":\"晴れ\",\"maxTemp\":22}"}]
usage.totalTokens は 64 で、2ステップ分の合計でした(v7 で、usage が全ステップの合計に変わった)。responseMessages は assistant,tool,assistant の3件で、会話の履歴に積むときはこれを足します。
| v6 までの名前 | v7 の名前 | 7.0.138 |
|---|---|---|
stepCountIs(5) |
isStepCount(5) |
同じ関数の別名として残っている |
system |
instructions |
非推奨として残っている |
onFinish・onStepFinish |
onEnd・onStepEnd |
非推奨として残っている |
fullStream |
stream |
非推奨として残っている |
parameters(tool) |
inputSchema |
- |
needsApproval(tool) |
toolApproval |
非推奨(移行ガイド) |
エージェントのクラス:ToolLoopAgent
設定を1か所にまとめて使い回すときは ToolLoopAgent を使います。ToolLoopAgent の stopWhen の既定は isStepCount(20) です(型定義)。
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: openai('gpt-6-luna'),
instructions: 'あなたは天気案内です。',
tools: { get_weather: getWeather },
});
const result = await agent.generate({ prompt: '東京の天気は?' }); // agent.stream(...) もある
console.log(result.steps.length, result.text); // 2 ...
ツールの仕組みは関連記事「Function Calling(tool use)の仕組みとPython実装」で、エージェントの考え方は関連記事「AIエージェントとは|仕組みとワークフローとの違い・作り方入門」で扱っています。
Next.js:Route Handler と useChat でチャット画面
サーバー側の Route Handler で streamText を UI 用のストリームにして返し、画面側の useChat で受け取ります。 Next.js 16.4.0 の App Router で next build が通り、next start で動いたコードです。
app/api/chat/route.ts
import {
streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse,
tool, isStepCount, type UIMessage,
} from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
export const maxDuration = 30; // Vercel で関数が動ける秒数
const tools = {
get_weather: tool({
description: '都市名を受け取り、今日の天気を返す',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, weather: '晴れ', maxTemp: 22 }),
}),
};
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai('gpt-6-luna'),
instructions: 'あなたは社内ヘルプデスクです。短く答えます。',
messages: await convertToModelMessages(messages), // v7 では Promise を返す
tools,
stopWhen: isStepCount(5),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream, tools }),
});
}
app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
export default function Page() {
const [input, setInput] = useState('');
const { messages, sendMessage, status, error, stop } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
return (
<main>
{messages.map((m) => (
<div key={m.id}>
<b>{m.role === 'user' ? 'あなた' : 'AI'}:</b>
{m.parts.map((part, i) => {
if (part.type === 'text') return <span key={i}>{part.text}</span>;
if (part.type === 'tool-get_weather') return <pre key={i}>{JSON.stringify(part.output)}</pre>;
return null;
})}
</div>
))}
{error && <p role="alert">エラーが起きました</p>}
<form onSubmit={(e) => { e.preventDefault(); if (!input.trim()) return; sendMessage({ text: input }); setInput(''); }}>
<input value={input} onChange={(e) => setInput(e.target.value)} disabled={status !== 'ready'} />
<button type="submit" disabled={status !== 'ready'}>送信</button>
{(status === 'submitted' || status === 'streaming') && <button type="button" onClick={() => stop()}>停止</button>}
</form>
</main>
);
}
useChat は入力欄の状態を持たないので、useState で自分で持ちます。メッセージの中身は content ではなく parts(部品の配列) で、文は type: 'text'、ツールは type: 'tool-ツール名' で届きます。
画面で送ったときに起きたこと
Chromium で「東京の天気は?」と送ると、status は ready → streaming → ready と変わり、ツールの部品の state は output-available になりました。「こんにちは」と送ったときに useChat が /api/chat に送った本文です。
{"id":"zXxcy6jo2q0Yi66y","messages":[{"parts":[{"type":"text","text":"こんにちは"}],"id":"96bYZZDeCbeobsNy","role":"user"}],"trigger":"submit-message"}
Route Handler が返した SSE には、ツールの部品が次の順で入っていました。
data: {"type":"tool-input-start","toolCallId":"call_009","toolName":"get_weather"}
data: {"type":"tool-input-delta","toolCallId":"call_009","inputTextDelta":"{\"city\""}
data: {"type":"tool-input-available","toolCallId":"call_009","toolName":"get_weather","input":{"city":"東京"},...}
data: {"type":"tool-output-available","toolCallId":"call_009","output":{"city":"東京","weather":"晴れ","maxTemp":22},...}
data: {"type":"finish-step"}
data: {"type":"start-step"}
data: {"type":"text-start",...}
status |
意味 |
|---|---|
ready |
送信できる |
submitted |
送信して、最初の返答を待っている |
streaming |
返答を受け取っている |
error |
エラーで終わった(error に内容) |
useChat の返り値 |
役割 |
|---|---|
messages |
会話(UIMessage の配列) |
sendMessage({ text }) |
送る |
status・error |
状態・エラー |
stop() |
受け取りを止める |
regenerate() |
最後の返答を作り直す |
setMessages() |
会話を書き換える(保存した履歴の読み込みなど) |
Route Handler 自体の作り方は関連記事「Next.js Route HandlersでAPIの作り方|CRUD・CORS実例」で、App Router は関連記事「Next.js App Router 入門|v16対応・実際のビルド結果つき」で扱っています。API キーはサーバー側の環境変数(.env.local など)に置き、NEXT_PUBLIC_ を付けません。
エラーと再試行
generateText のエラーと再試行の回数
| 状況 | 投げられる例外(手元の結果) | リクエスト数 |
|---|---|---|
| 429(回数の上限) | RetryError(Failed after 3 attempts. Last error: ... Rate limit reached)・reason: maxRetriesExceeded |
3(約6秒) |
429 で maxRetries: 0 |
APICallError(statusCode: 429・isRetryable: true) |
1 |
| 401(キーの誤り) | APICallError(statusCode: 401・isRetryable: false) |
1(再試行しない) |
| 接続できない | RetryError(Cannot connect to API: connect ECONNREFUSED ...) |
maxRetries + 1 |
| API キーが無い | LoadAPIKeyError(OpenAI API key is missing. Pass it using the 'apiKey' parameter or the OPENAI_API_KEY environment variable.) |
0 |
maxRetries の既定は 2 で、最初の1回と合わせて3回送られました。 429 の3回は約6秒かかりました。RetryError の errors には3回分のエラーが入り、lastError に最後のエラーが入ります。
import { generateText, APICallError, RetryError, LoadAPIKeyError } from 'ai';
try {
const { text } = await generateText({ model: openai('gpt-6-luna'), prompt: '...', maxRetries: 2 });
} catch (e) {
if (RetryError.isInstance(e)) console.error('再試行しても失敗', e.lastError);
else if (APICallError.isInstance(e)) console.error(e.statusCode, e.responseBody);
else if (LoadAPIKeyError.isInstance(e)) console.error('キーの設定がない');
else throw e;
}
instanceof の代わりに ○○Error.isInstance(e) を使うと、パッケージが二重に入っていても判定できます。429 の考え方は、関連記事「LLM APIの429・529エラー対処|リトライ実装とエラー一覧」で扱っています。
streamText のエラーは投げられない
streamText は、エラーを例外として投げません。 401 を返したとき、手元では次のようになりました。
| 確かめたこと | 結果 |
|---|---|
textStream |
0件で終わる(例外にならない) |
onError |
APICallError(statusCode 401)で呼ばれた |
onError を付けないとき |
エラーの内容がコンソールに出力された |
await result.text |
NoOutputGeneratedError: No output generated. Check the stream for errors. |
result.stream |
start の次に type: 'error' の部品が届いた |
const result = streamText({
model: openai('gpt-6-luna'),
prompt: '...',
onError({ error }) {
console.error(error); // ログに残す
},
});
画面に返すエラーの文
Route Handler でエラーになると、HTTP のステータスは 200 のまま、SSE で次の部品が届きました。詳しい内容は既定で隠されます。
data: {"type":"start"}
data: {"type":"error","errorText":"An error occurred."}
data: [DONE]
画面に出す文を変えるときは、toUIMessageStream に onError を渡します。返した文字列が errorText になりました。
return createUIMessageStreamResponse({
stream: toUIMessageStream({
stream: result.stream,
onError: (error) =>
APICallError.isInstance(error) && error.statusCode === 401
? 'APIキーの設定を確認します'
: 'エラーが起きました',
}),
});
API のエラーの本文には内部の情報が入ることがあるので、そのまま画面に出さず、利用者向けの文に置き換えます。
テスト:MockLanguageModelV4 で API を呼ばずに動かす
ai/test には、決まった結果を返すモデル(モック)があります。 v7 のモデルの仕様(V4)に合わせた MockLanguageModelV4 を使います。
import { generateText, streamText, tool, isStepCount } from 'ai';
import { MockLanguageModelV4, convertArrayToReadableStream } from 'ai/test';
import { z } from 'zod';
import assert from 'node:assert/strict';
const usage = {
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 5, text: 5, reasoning: 0 },
};
// 1. まとめて返す
const model = new MockLanguageModelV4({
doGenerate: {
content: [{ type: 'text', text: 'こんにちは' }],
finishReason: { unified: 'stop', raw: 'stop' },
usage,
warnings: [],
},
});
const r = await generateText({ model, instructions: '短く', prompt: 'あいさつして' });
assert.equal(r.text, 'こんにちは');
console.log(model.doGenerateCalls[0].prompt); // モデルに渡ったプロンプト
// 2. ストリームで返す
const smodel = new MockLanguageModelV4({
doStream: {
stream: convertArrayToReadableStream([
{ type: 'text-start', id: 't1' },
{ type: 'text-delta', id: 't1', delta: 'こん' },
{ type: 'text-delta', id: 't1', delta: 'にちは' },
{ type: 'text-end', id: 't1' },
{ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage },
]),
},
});
for await (const t of streamText({ model: smodel, prompt: 'x' }).textStream) console.log(t);
// 3. ツールの往復(配列で渡すと順に返す)
let called = 0;
const getWeather = tool({
description: '天気',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => { called++; return `${city}は晴れ`; },
});
const tmodel = new MockLanguageModelV4({
doGenerate: [
{ content: [{ type: 'tool-call', toolCallId: 'c1', toolName: 'get_weather', input: JSON.stringify({ city: '東京' }) }],
finishReason: { unified: 'tool-calls', raw: 'tool_calls' }, usage, warnings: [] },
{ content: [{ type: 'text', text: '東京は晴れです。' }],
finishReason: { unified: 'stop', raw: 'stop' }, usage, warnings: [] },
],
});
const t = await generateText({ model: tmodel, tools: { get_weather: getWeather }, stopWhen: isStepCount(5), prompt: '東京の天気は?' });
assert.equal(called, 1);
assert.equal(t.text, '東京は晴れです。');
手元の結果です。
こんにちは 15 1
[{"role":"system","content":"短く"},{"role":"user","content":[{"type":"text","text":"あいさつして"}]}]
こん
にちは
2 東京は晴れです。 2
doGenerateCalls・doStreamCallsに、モデルに渡った引数(prompt・toolsなど)が残るdoGenerateに配列を渡すと、呼ばれるたびに順に返す。2回目のpromptの最後には、ツールの呼び出し(assistant)と結果(tool)が入っていたusageとfinishReasonは V4 の形(inputTokens.total・finishReason.unifiedなど)で書く
MockLanguageModelV3 も残っていました。テストケースの洗い出し方は、関連記事「生成AIでテストコードを作るプロンプト集|テストケースの洗い出し」で扱っています。
以前の版のコードを v7 に直す早見表
| v6 まで | v7 | 7.0.138 での扱い |
|---|---|---|
system: '...' |
instructions: '...' |
非推奨(動く) |
onFinish・onStepFinish |
onEnd・onStepEnd |
非推奨(動く) |
result.fullStream |
result.stream |
非推奨(動く) |
result.toUIMessageStreamResponse() |
createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) }) |
非推奨(動く) |
generateObject・streamObject |
generateText・streamText + output: Output.object(...) |
非推奨(警告が出る) |
stepCountIs |
isStepCount |
別名 |
messages に system |
instructions |
エラー(InvalidPromptError) |
usage(最後のステップ) |
usage(全ステップの合計)・finalStep.usage |
意味が変わった |
experimental_telemetry |
telemetry(OpenTelemetry は @ai-sdk/otel) |
移行ガイド |
| Node.js 18・20 | Node.js 22 以上 | engines |
よくある質問
Q. Next.js 以外でも使えますか。
A. generateText・streamText は Node.js(22 以上)のどこでも動きます。Express などでは pipeUIMessageStreamToResponse で Node.js のレスポンスに書けます。画面側は @ai-sdk/vue・@ai-sdk/svelte・@ai-sdk/angular もあります。
Q. ローカル LLM や社内の互換サーバーにつなぐには?
A. createOpenAICompatible({ name: 'local', baseURL: 'http://localhost:11434/v1' }) で作ったモデルを渡します。手元では POST /v1/chat/completions に {"model": "my-local-model", "messages": [...]} が送られました。Ollama 自体は関連記事「Ollamaの使い方|ローカルLLMのコマンド一覧とAPIの呼び方」で扱っています。
Q. LangChain との違いは?
A. AI SDK は TypeScript で、モデルの呼び出し・ストリーム・UI の部品(useChat)までをそろえています。LangChain は Python と JavaScript の両方があり、RAG やエージェントの部品が多くあります。Python 版は関連記事「LangChain入門|PythonでLLMアプリを作る基本とコピペ集」で扱っています。
Q. API キーはどこに置きますか。
A. サーバー側の環境変数(OPENAI_API_KEY・ANTHROPIC_API_KEY)に置きます。関連記事「.envの使い方と注意点|APIキーを安全に管理する方法」で扱っています。
まとめ
npm i ai @ai-sdk/openai zod。v7 は Node.js 22 以上・ESM だけ- まとめて受け取るのは
generateText、少しずつはstreamText(awaitを付けない) - システムの指示は
instructions。v7 ではmessagesに system を入れるとエラー - 構造化出力は
output: Output.object({ schema })。generateObjectは非推奨 - ツールは
tool({ description, inputSchema, execute })。回答まで進めるにはstopWhen: isStepCount(n)(既定は1ステップ) - Next.js は
createUIMessageStreamResponse+toUIMessageStreamとuseChat(中身はparts) maxRetriesの既定は 2(計3回)。streamTextのエラーは投げられないのでonErrorで拾う- テストは
MockLanguageModelV4(ai/test)で API を呼ばずに回せる
参考・出典
確認日はいずれも 2026年10月13日です。
- AI SDK ドキュメント「Generating Text」:https://ai-sdk.dev/docs/ai-sdk-core/generating-text
- AI SDK ドキュメント「Migration Guide 7.0」:https://ai-sdk.dev/docs/migration-guides/migration-guide-7-0
- AI SDK ドキュメント「Generating Structured Data」:https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data
- AI SDK ドキュメント「Tool Calling」:https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling
- AI SDK ドキュメント「Chatbot」(useChat):https://ai-sdk.dev/docs/ai-sdk-ui/chatbot
- AI SDK ドキュメント「Testing」:https://ai-sdk.dev/docs/ai-sdk-core/testing
- npm「ai」:https://www.npmjs.com/package/ai
- GitHub「vercel/ai」:https://github.com/vercel/ai
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vercel AI SDK入門|generateText・streamText」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。