You want a streaming chat in your app. TanStack AI streams from a server route. The hook for your framework renders the tokens.
Tip
If you do not want a key per provider, OpenRouter gives you 300+ models with one API key.
React Native or Expo needs an absolute server URL and an XHR transport. See Quick Start: React Native.
No UI: see Quick Start: Server Only.
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openaipnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiyarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaibun add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiCall chat(). Then wrap the result with toServerSentEventsResponse.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-5.6"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}This works with TanStack Start, Next.js, SvelteKit, Hono, and any host that returns a Web Response.
If your server is Node streams (Express), see Quick Start: Server Only.
Put the API key on the server:
OPENAI_API_KEY=your-openai-api-keyThe adapter reads OPENAI_API_KEY at runtime. Do not send this key to the browser.
If you do not want a server key, see Bring Your Own Key.
Call useChat from @tanstack/ai-react. Hold the composer text in useState. Pass it to sendMessage.
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading, stop } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
return (
<>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault();
if (input.trim() === "") {
return;
}
sendMessage(input);
setInput("");
}}
>
<input
value={input}
onChange={(event) => setInput(event.target.value)}
/>
{isLoading ? (
<button type="button" onClick={stop}>
Stop
</button>
) : (
<button type="submit">Send</button>
)}
</form>
</>
);
}messages updates as chunks arrive. isLoading is true while the run is in flight.
See the React API.
Send a message. Tokens show up in the UI.