はじめに
AIチャット機能を自社サービスに組み込むとき、最初はOpenAIで実装したはいいものの、 「Anthropicにも対応してほしい」「コストが安いモデルに切り替えたい」という要望は 珍しくありません。ところが多くのSDKはプロバイダーごとにAPIの形が違うため、 乗り換えのたびにストリーミング処理やツールコールの実装をほぼ書き直すことになります。
TanStack AIは、この「プロバイダー間の書き直し」を解消するために作られたSDKです。 TanStack Query や TanStack Router と同じ思想で、フレームワークやLLMプロバイダーに 依存しない共通レイヤーを提供し、アダプターを差し替えるだけでOpenAI・Anthropic・ Google Geminiなどを行き来できるようにします。
TanStack AIとは
TanStack AIは、ストリーミングチャット・型安全なツールコール・AIエージェントの 実装をひとつのAPIでまとめて扱える、軽量なTypeScript製SDKです。TanStack Query や TanStack Table などでおなじみのTanStackが手がけており、React・Solidの公式アダプターと、 Next.js・Express・TanStack Start・React Native/Expoなど幅広い実行環境に対応しています。
主な特徴
- プロバイダー非依存 - OpenAI、Anthropic、Google Gemini、AWS Bedrock、Groq、
xAI Grok、OpenRouterなど15種類以上のアダプターが用意されており、
chat()に渡す アダプターを差し替えるだけでモデルを乗り換えられます - 型安全なツールコール -
toolDefinition()にZodスキーマを渡すだけで、 入出力の検証とTypeScriptの型推論が同時に手に入ります - 同型(isomorphic)なツール定義 - 同じツール定義をサーバー側実行・ クライアント側実行のどちらにも使い回せます
- ストリーミングとエージェントループが標準搭載 - Server-Sent Eventsでの
ストリーミングに加え、
maxIterations()のようなエージェントループ戦略や、needsApprovalによるツール実行の承認フローを内蔵しています - React向けの
useChatフック - メッセージ状態管理・ローディング状態・ ストリーミング表示を@tanstack/ai-reactがまとめて面倒を見てくれます
インストール
利用するプロバイダーに応じてアダプターパッケージを組み合わせてインストールします。 Reactでは以下のようになります。
npm install @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
# yarn
yarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
# pnpm
pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
Anthropicを使う場合は@tanstack/ai-openaiの代わりに@tanstack/ai-anthropic、
複数プロバイダーをまとめて扱いたい場合はOpenRouter用アダプターを追加する形です。
本記事執筆時点での@tanstack/aiの最新バージョンは0.49.1でした。
なお、TanStack AIの実運用にはOpenAIやAnthropicなどのAPIキーが必須です。 このためこの記事では、実際にモデルを呼び出すコード自体を埋め込み実行可能な サンプルとして掲載していません(外部APIキーを要するコードを不特定多数が 開くページに埋め込むのは安全上好ましくないためです)。代わりに、コピーして 自分のプロジェクトにそのまま貼り付けられる完成形のコードを示します。
基本的な使い方
TanStack AIは「サーバー側でストリームを作る」「クライアント側でuseChatが
それを受け取る」という2ステップで構成されます。まずはNext.jsのAPI Routeで
chat()を呼び出し、toServerSentEventsResponse()でSSEレスポンスに変換します。
// app/api/chat/route.ts
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
systemPrompts: ["You are a helpful assistant."],
});
return toServerSentEventsResponse(stream);
}
クライアント側は@tanstack/ai-reactのuseChatフックで受け取ります。
メッセージ配列の管理や送信中のローディング状態はフックが自動で処理してくれるため、
自前でreducerを書く必要がありません。
// app/components/Chat.tsx
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
return (
<div>
{messages.map((msg) => (
<p key={msg.id}>
<strong>{msg.role}:</strong> {msg.parts}
</p>
))}
<button
disabled={isLoading}
onClick={() => sendMessage({ role: "user", content: "こんにちは" })}
>
送信
</button>
</div>
);
}
adapter: openaiText("gpt-5.2")の部分をanthropicText("claude-sonnet-4-5")に
差し替えるだけで、クライアント側のコードには一切手を入れずにAnthropicへ
乗り換えられるのが、TanStack AIがプロバイダー非依存を掲げる所以です。
実践的なユースケース
プロバイダーを実行時に切り替える
複数のLLMプロバイダーを併用したい場合、chat()に渡すadapterを切り替えるだけで
対応できます。ユーザーごとに使うモデルを変えたり、障害時に別プロバイダーへ
フォールバックしたりする実装も、この差し替えだけで完結します。
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { anthropicText } from "@tanstack/ai-anthropic";
function getAdapter(provider: "openai" | "anthropic") {
return provider === "openai"
? openaiText("gpt-5.2")
: anthropicText("claude-sonnet-4-5");
}
export async function POST(request: Request) {
const { messages, provider } = await request.json();
const stream = chat({
adapter: getAdapter(provider),
messages,
});
return toServerSentEventsResponse(stream);
}
多数のモデルをまとめて扱いたい場合は、単一のAPIキーで300以上のモデルに アクセスできるOpenRouter向けアダプターを使う方法もあります。
型安全なツールコールを実装する
チャットにDB検索や外部処理を行わせたいときは、toolDefinition()で入力を
Zodスキーマとして定義します。モデルが渡してくる引数はスキーマに沿って
検証され、TypeScriptの型もそのまま推論されるため、anyを挟まずに実装できます。
import { toolDefinition, chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const searchProducts = toolDefinition({
name: "searchProducts",
description: "商品名から在庫を検索する",
inputSchema: z.object({
query: z.string().min(1),
}),
execute: async ({ query }) => {
const results = await db.products.search(query);
return { results };
},
});
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [searchProducts],
});
このツール定義は同型(isomorphic)なので、executeをサーバー側の関数から
クライアント側の関数に差し替えれば、そのままブラウザ内実行のツールとしても
再利用できます。
承認が必要なツールに承認フローを挟む
ファイルの削除や決済処理のように、モデルの判断だけで実行させたくない操作には
needsApproval: trueを付けます。TanStack AIはツール実行の直前で処理を止め、
ユーザーの承認を待つ状態をチャットの状態として表現してくれます。
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const deleteRecord = toolDefinition({
name: "deleteRecord",
description: "指定したIDのレコードを削除する(要承認)",
needsApproval: true,
inputSchema: z.object({ id: z.string() }),
execute: async ({ id }) => {
await db.records.delete(id);
return { deleted: id };
},
});
クライアント側では、承認待ちになったツール呼び出しをmessagesの一部として
受け取れるので、「実行してよいですか?」という確認UIをuseChatの状態から
そのまま組み立てられます。
エージェントループの上限を制御する
ツールコールを繰り返しながら自律的にタスクを進めるエージェント的な使い方では、
モデルが延々とツールを呼び続けてしまうリスクがあります。agentLoopStrategyに
maxIterations()を渡すことで、モデルのターン数に上限を設けられます。
import { chat, maxIterations } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [searchProducts, deleteRecord],
agentLoopStrategy: maxIterations(20),
});
maxIterations(20)をmaxIterations(5)のように小さくすれば、コストや
レイテンシを抑えた軽量なエージェントとして動かすこともできます。
まとめ
TanStack AIは、AIチャット機能を実装するときに避けて通れない「プロバイダーごとの
API差異」「ストリーミング処理」「ツールコールの型安全性」を、ひとつの一貫した
SDKにまとめてくれるライブラリです。adapterを差し替えるだけでOpenAIから
Anthropicへ乗り換えられる設計は、特定のベンダーにロックインされたくない
チームにとって大きな安心材料になります。
まずはopenaiTextアダプター1つでチャットを動かしてみて、慣れてきたら
toolDefinition()でツールコールを追加し、needsApprovalやmaxIterations()で
安全なエージェント運用に広げていくのがおすすめです。