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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?