| name | tanstack-ai |
| description | Build AI-powered chat applications with TanStack AI and React. Use when working with @tanstack/ai, @tanstack/ai-react, @tanstack/ai-client, or any TanStack AI packages. Covers useChat hook, streaming, tools (server/client/hybrid), tool approval, structured outputs, multimodal content, adapters (OpenAI, Anthropic, Gemini, Ollama, Grok), agentic cycles, devtools, and type safety patterns. Triggers on AI chat UI, function calling, LLM integration, or streaming response tasks using TanStack AI. |
TanStack AI (React)
AI chat framework with isomorphic tools, streaming, and full type safety.
Packages
@tanstack/ai — core: chat(), toolDefinition(), toServerSentEventsResponse(), maxIterations()
@tanstack/ai-react — React: useChat() hook, re-exports connection adapters
@tanstack/ai-client — headless: ChatClient, clientTools(), createChatClientOptions(), InferChatMessages
@tanstack/ai-{openai,anthropic,gemini,ollama,grok,openrouter,fal} — adapter packages
Quick Start
Install
npm install @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
Server (Next.js API Route)
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,
});
return toServerSentEventsResponse(stream);
}
Client (React)
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts.map((part, idx) => {
if (part.type === "text") return <span key={idx}>{part.content}</span>;
if (part.type === "thinking") return <em key={idx}>{part.content}</em>;
return null;
})}
</div>
))}
<form onSubmit={(e) => { e.preventDefault(); sendMessage(input); setInput(""); }}>
<input value={input} onChange={(e) => setInput(e.target.value)} disabled={isLoading} />
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}
useChat Hook
const {
messages,
sendMessage,
append,
isLoading,
error,
stop,
reload,
clear,
setMessages,
addToolResult,
addToolApprovalResponse,
} = useChat({
connection: fetchServerSentEvents("/api/chat"),
tools?,
initialMessages?,
id?,
body?,
onResponse?,
onChunk?,
onFinish?,
onError?,
});
Message Structure
Messages use UIMessage with a parts array:
interface UIMessage {
id: string;
role: "user" | "assistant";
parts: (TextPart | ThinkingPart | ToolCallPart | ToolResultPart)[];
}
Render parts by type:
part.type === "text" — part.content (string)
part.type === "thinking" — part.content (model reasoning, UI-only, not sent back)
part.type === "tool-call" — part.name, part.input, part.output, part.state
part.type === "tool-result" — part.output, part.state
Connection Adapters
import { fetchServerSentEvents, fetchHttpStream, stream } from "@tanstack/ai-react";
fetchServerSentEvents("/api/chat", { headers: { Authorization: "Bearer token" } })
fetchHttpStream("/api/chat")
stream(async (messages, data, signal) => { })
Adapters
Model passed to adapter factory — one function per activity for tree-shaking:
import { openaiText } from "@tanstack/ai-openai";
import { anthropicText } from "@tanstack/ai-anthropic";
import { geminiText } from "@tanstack/ai-gemini";
import { ollamaText } from "@tanstack/ai-ollama";
import { grokText } from "@tanstack/ai-grok";
import { openRouterText } from "@tanstack/ai-openrouter";
Tools Overview
Two-step process: define schema with toolDefinition(), then implement with .server() or .client().
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get current weather for a location",
inputSchema: z.object({ location: z.string() }),
outputSchema: z.object({ temperature: z.number(), conditions: z.string() }),
needsApproval: false,
});
const getWeather = getWeatherDef.server(async ({ location }) => {
const data = await fetchWeather(location);
return { temperature: data.temp, conditions: data.conditions };
});
const getWeatherClient = getWeatherDef.client((input) => {
return { temperature: 72, conditions: "cached" };
});
For detailed tool patterns (server, client, hybrid, approval, agentic cycle), see references/tools.md.
Type Safety
Use clientTools() + createChatClientOptions() + InferChatMessages for full type inference:
import { clientTools, createChatClientOptions, type InferChatMessages } from "@tanstack/ai-client";
const tools = clientTools(updateUI, saveToStorage);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type ChatMessages = InferChatMessages<typeof chatOptions>;
const { messages } = useChat(chatOptions);
Devtools
npm install -D @tanstack/react-ai-devtools @tanstack/react-devtools
import { TanStackDevtools } from "@tanstack/react-devtools";
import { aiDevtoolsPlugin } from "@tanstack/react-ai-devtools";
<TanStackDevtools
plugins={[aiDevtoolsPlugin()]}
eventBusConfig={{ connectToServerBus: true }}
/>
Additional Guides