Class References
Function References
- text
- textOptions
- combineStrategies
- convertMessagesToModelMessages
- convertZodToJsonSchema
- createReplayStream
- embedding
- generateMessageId
- maxIterations
- messages
- modelMessageToUIMessage
- modelMessagesToUIMessages
- normalizeToUIMessage
- parsePartialJSON
- summarize
- toServerSentEventsStream
- toServerSentEventsResponse
- toolDefinition
- uiMessageToModelMessages
- realtimeToken
- untilFinishReason
Interface References
- AIAdapter
- AIAdapterConfig
- AgentLoopState
- AudioPart
- AudioVisualization
- BaseStreamChunk
- TextCompletionChunk
- TextOptions
- ChunkRecording
- ChunkStrategy
- ClientTool
- ContentPartSource
- ContentStreamChunk
- DefaultMessageMetadataByModality
- DocumentPart
- DoneStreamChunk
- EmbeddingOptions
- EmbeddingResult
- ErrorStreamChunk
- ImagePart
- InternalToolCallState
- JSONParser
- ModelMessage
- ProcessorResult
- ProcessorState
- RealtimeAdapter
- RealtimeClientOptions
- RealtimeConnection
- RealtimeMessage
- RealtimeSessionConfig
- RealtimeToken
- RealtimeTokenAdapter
- RealtimeTokenOptions
- ResponseFormat
- ServerTool
- StreamProcessorEvents
- StreamProcessorHandlers
- StreamProcessorOptions
- SummarizationOptions
- SummarizationResult
- TextPart
- ThinkingPart
- ThinkingStreamChunk
- Tool
- ToolCall
- ToolCallPart
- ToolCallStreamChunk
- ToolConfig
- ToolDefinition
- ToolDefinitionConfig
- ToolDefinitionInstance
- ToolInputAvailableStreamChunk
- ToolResultPart
- ToolResultStreamChunk
- UIMessage
- VideoPart
Type Alias References
- AgentLoopStrategy
- AnyClientTool
- TextStreamOptionsForModel
- TextStreamOptionsUnion
- ConstrainedContent
- ConstrainedModelMessage
- ContentPart
- ContentPartForModalities
- ExtractModalitiesForModel
- ExtractModelsFromAdapter
- InferToolInput
- InferToolName
- InferToolOutput
- MessagePart
- RealtimeEvent
- RealtimeMessagePart
- RealtimeMode
- RealtimeStatus
- ModalitiesArrayToUnion
- Modality
- StreamChunk
- ToolCallState
- ToolResultState
Variable References
@tanstack/ai-remix
Remix 3 helpers for TanStack AI. To build your first chat, start with the Quick Start. For a typed chat layout, see Remix Chat UI.
Installation#
npm i @tanstack/ai-remix remixpnpm add @tanstack/ai-remix remixyarn add @tanstack/ai-remix remixbun add @tanstack/ai-remix remixremix is a required peer. The package publishes uncompiled source. Remix compiles the JSX through the remix/ui jsxImportSource.
Every helper takes the component Handle as its first argument. Call it in the setup function, not in the render function.
The server side is a normal route. A Remix controller action returns the same SSE Response as any other host:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { createController } from "remix/router";
import { post, route } from "remix/routes";
const routes = route({
chat: {
stream: post("/chat"),
},
});
export default createController(routes.chat, {
actions: {
async stream({ request }) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
},
},
});createChat(handle, options)#
Holds the messages and the run state of one chat, and talks to your server route.
import { createChat, fetchServerSentEvents } from "@tanstack/ai-remix";
import { clientEntry, on, type Handle } from "remix/ui";
export const Chat = clientEntry(
import.meta.url,
function Chat(handle: Handle) {
const chat = createChat(handle, {
connection: fetchServerSentEvents("/chat"),
threadId: "support-chat",
});
return () => (
<div>
{chat.messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts
.filter((part) => part.type === "text")
.map((part) => part.content)
.join("")}
</div>
))}
<form
mix={on("submit", (event) => {
event.preventDefault();
const form = event.currentTarget;
const text = String(new FormData(form).get("message") ?? "").trim();
if (text === "") {
return;
}
form.reset();
void chat.sendMessage(text);
})}
>
<input name="message" disabled={chat.isLoading} />
<button type="submit" disabled={chat.isLoading}>
Send
</button>
</form>
</div>
);
},
);Put connection and tools in setup. They are not serializable clientEntry props.
Options#
You must pass threadId. You must also pass connection or fetcher, not both.
| Option | What it does |
|---|---|
| connection | Connection adapter, for example fetchServerSentEvents("/chat"). See Transports. |
| fetcher | A function that returns a Response or a stream. Use it with server functions. |
| tools | Client tools from .client(). They run when the model calls them. A get tools() getter is read each time a run starts. See Client Tools. |
| threadId | Required. The id of this chat. Use a stable id for each conversation, for example the id from your route. |
| initialMessages | The messages to show first. |
| forwardedProps | JSON sent to the server in the AG-UI forwardedProps field, for example { model: "gpt-6.1-sol" }. |
| persistence | true to load the thread from the server, or a storage adapter. See Chat Persistence. |
| outputSchema | A Standard Schema. Adds typed partial and final to the return value. See Streaming structured output. |
| context | Client-only runtime context for client tools. It is not sent to the server. |
| live | Subscribe in setup and unsubscribe when the handle aborts. |
| queue | What sendMessage does while a run is busy. Default: queue the message. See Queueing. |
| interrupts | Definitions from defineInterrupt(). See Interrupts. |
| byok, byokProvider | A keyring from defineByok(). See Bring Your Own Key. |
| devtools | Display options for TanStack AI Devtools. The helper always tags framework: 'remix'. |
The helper also takes the ChatClient callbacks (onResponse, onChunk, onFinish, onError, onInterruptStateChange, onCustomEvent) and the other client options. See ChatClient options.
Returns#
State fields are getters. Read them in the render function, so each paint sees the latest values. Cleanup runs when handle.signal aborts.
| Field | What it is |
|---|---|
| messages | The UIMessage array of the chat. |
| sendMessage(content, options?) | Sends a user message. content is a string or { content, id?, metadata? }. options takes whenBusy and body. |
| isLoading | true while a response streams. |
| error | The last error, or undefined. |
| status | "ready", "submitted", "streaming", or "error". |
| stop() | Stops the current response. |
| reload() | Runs the last user message again. |
| append(message) | Adds a UIMessage or ModelMessage and starts a run. |
| addToolResult(result) | Gives the result of a client tool by hand: { toolCallId, tool, output, state?, errorText? }. |
| setMessages(messages), clear() | Replace or clear the messages. |
| queue, cancelQueued(id) | Messages that wait while a run is busy. See Queueing. |
| runId | The id of the run in flight, or null. |
| interrupts | Pending interrupts. Each item has resolveInterrupt(...). See Interrupts. |
| resolveInterrupts, cancelInterrupts, retryInterrupts, resumeInterruptsUnsafe | Act on all pending interrupts at once. |
| interruptErrors, resuming | Errors from the last resume, and true while a resume runs. |
| isHydrating | true while the chat loads from persistence. |
| hasOlderMessages, loadOlderMessages() | Page in older messages. See Chat Persistence. |
| isSubscribed, connectionStatus, sessionGenerating | State of the live subscription. |
| partial, final | Structured output. Only when you pass outputSchema. |
Example: tool approval#
A tool with needsApproval: true pauses the run. The pending call shows up in interrupts.
import { toolDefinition } from "@tanstack/ai";
import { createChat, fetchServerSentEvents } from "@tanstack/ai-remix";
import { clientEntry, on, type Handle } from "remix/ui";
import { z } from "zod";
const deleteFileTool = toolDefinition({
name: "delete_file",
description: "Delete a file",
needsApproval: true,
inputSchema: z.object({ path: z.string() }),
});
export const ChatWithApproval = clientEntry(
import.meta.url,
function ChatWithApproval(handle: Handle) {
const chat = createChat(handle, {
connection: fetchServerSentEvents("/chat"),
threadId: "support-chat",
tools: [deleteFileTool],
});
return () => (
<div>
{chat.interrupts.map((interrupt) =>
interrupt.kind === "tool-approval" ? (
<div key={interrupt.id}>
<p>Approve: {interrupt.toolName}</p>
<button
type="button"
mix={on("click", () => interrupt.resolveInterrupt(true))}
>
Approve
</button>
<button
type="button"
mix={on("click", () => interrupt.resolveInterrupt(false))}
>
Deny
</button>
</div>
) : null,
)}
</div>
);
},
);See Tool Approval for the server side.
createChatHook(options)#
This package does not export createChatHook from its main entry. @tanstack/ai-remix/ui has a createChatHook that binds chat options and typed UI components. Call it once at module scope. Then your app calls the returned factory with the handle and renders <ui.Chat chat={chat} />. See Remix Chat UI.
Generation hooks#
Each helper runs one generation request and keeps its result. See Generation Hooks.
| Helper | generate() input | result |
|---|---|---|
| createGenerateImage | ImageGenerateInput | ImageGenerationResult |
| createGenerateAudio | AudioGenerateInput | AudioGenerationResult |
| createGenerateSpeech | SpeechGenerateInput | TTSResult |
| createTranscription | TranscriptionGenerateInput | TranscriptionResult |
| createSummarize | SummarizeGenerateInput | SummarizationResult |
| createGenerateVideo | VideoGenerateInput | VideoGenerateResult |
| createGeneration | Your own input type | Your own result type |
import { createGenerateImage, fetchServerSentEvents } from "@tanstack/ai-remix";
import { on, type Handle } from "remix/ui";
function ImageGenerator(handle: Handle) {
const image = createGenerateImage(handle, {
connection: fetchServerSentEvents("/api/generate/image"),
threadId: "image-studio",
});
return () => (
<button
type="button"
disabled={image.isLoading}
mix={on("click", () => {
void image.generate({ prompt: "A sunset over mountains" });
})}
>
Generate
</button>
);
}Options: threadId (required), connection or fetcher, body, persistence, byok, byokProvider, devtools, onResult, onError, onProgress, onChunk.
Returns: generate(input), result, isLoading, error, status, stop(), reset(), runId. The state fields are getters.
createGenerateVideo also returns jobId and videoStatus, and takes onJobCreated and onStatusUpdate.
createRealtimeChat(handle, options)#
Runs a voice conversation with a realtime model. Pass getToken and adapter. See Realtime Chat.
import { createRealtimeChat } from "@tanstack/ai-remix";
import { openaiRealtime } from "@tanstack/ai-openai";
import { on, type Handle } from "remix/ui";
function VoiceChat(handle: Handle) {
const chat = createRealtimeChat(handle, {
getToken: () => fetch("/api/realtime-token").then((response) => response.json()),
adapter: openaiRealtime(),
});
return () => (
<button
type="button"
mix={on("click", () => {
if (chat.status === "idle") {
void chat.connect();
return;
}
void chat.disconnect();
})}
>
{chat.status === "idle" ? "Start" : "Stop"}
</button>
);
}Returns status, error, mode, messages, connect(), disconnect(), startListening(), stopListening(), interrupt(), sendText(text), sendImage(data, mimeType), updateSession(config), plus audio levels and frequency data for visualizers.
createAudioRecorder(handle, options?)#
Records audio from the microphone. See Audio Recording.
Returns recording, isRecording, isSupported (getters), start(), stop(), and cancel(). Pass onComplete to transform the recording. Then recording and stop() use the transformed type.
createByok(handle, client)#
Subscribes to a BYOK keyring. The return value is a function that gives the latest snapshot. See Bring Your Own Key.
import { createByok } from "@tanstack/ai-remix";
import type { Handle } from "remix/ui";
import { byok } from "./byok";
function KeyStatus(handle: Handle) {
const getSnapshot = createByok(handle, byok);
return () => {
const openai = getSnapshot().status.openai;
return <p>{openai && "masked" in openai ? openai.masked : "No key"}</p>;
};
}The snapshot has status, locked, prompt, and storageError. Save a key from your own UI with byok.update(provider, value).
createMcpAppBridge(handle, options)#
Creates an MCP App bridge for one thread. Options: threadId, callEndpoint, chat, fetchImpl, onLink. See MCP Apps.
createRegisterWebMCPTools(handle, tools, options?)#
Exposes client tools to the browser WebMCP API. Remix removes them when handle.signal aborts. See WebMCP Tools.
import { createRegisterWebMCPTools } from "@tanstack/ai-remix";
import { clientEntry, type Handle } from "remix/ui";
import { searchProducts } from "./tools";
export const ProductsPage = clientEntry(
import.meta.url,
function ProductsPage(handle: Handle) {
createRegisterWebMCPTools(handle, [searchProducts], {
onError: (error) => console.error(error),
});
return () => null;
},
);Options: toolOptions, context, and onError. context is required when a tool declares a required runtime context.
createPageWebMCPTools(handle, options?)#
Reads the WebMCP tools that the page registered, as client tools. The tools field starts empty. When the page adds or removes a tool, the helper updates tools and calls handle.update(). Read it in a get tools() getter on createChat. See Page WebMCP Tools in Chat.
import { createPageWebMCPTools } from "@tanstack/ai-remix";
import type { Handle } from "remix/ui";
export function readPageTools(handle: Handle) {
return createPageWebMCPTools(handle, {
filter: (tool) => tool.origin === location.origin,
});
}Options: filter (return false to skip a tool) and onError.
Re-exports from @tanstack/ai-client#
You can import these from @tanstack/ai-remix directly. See @tanstack/ai-client for details.
| Group | Exports |
|---|---|
| Connection adapters | fetchServerSentEvents, fetchHttpStream, fetchJson, xhrServerSentEvents, xhrHttpStream, webSocket, stream, rpcStream |
| Typed options | createChatClientOptions, clientTools |
| Clients | ChatClient, GenerationClient, VideoGenerationClient, RealtimeClient, AudioRecorder, InterruptManager |
| WebMCP | registerWebMCPTools, getWebMCPTools, subscribeWebMCPTools |
| Errors | UnsupportedResponseStreamError, StreamTruncatedError, DurableStreamIncompleteError, StreamReconnectLimitError |
| Stream helpers | StreamProcessor, chunk strategies, parsePartialJSON, message converters |
The browser persistence adapters (localStoragePersistence and the others) are not re-exported here. Import them from @tanstack/ai-client. BYOK helpers (defineByok, defaultByokStorage, and more) are on the @tanstack/ai-remix/byok subpath.
Types#
| Type | What it is |
|---|---|
| CreateChatOptions, CreateChatReturn | The options and return value of createChat. |
| UIMessage<TTools> | A chat message with typed parts. |
| InferChatMessages<T> | The message type for a createChatClientOptions result. |
| SendMessageOptions, WhenBusy, QueueOption, QueuedMessage | Send and queue types. |
| DeepPartial<T> | The type of partial while structured output streams. |
| Create<Helper>Options, Create<Helper>Return | Options for each helper. The generation and realtime helpers also export a return type. |
Pass a plain tools array to createChat or createChatClientOptions. Tool names, inputs, and outputs stay typed without as const. Import toolDefinition from @tanstack/ai.