本页还没有译文
本页 69 段还没译成Português (BR),下面显示的是英文原文。Tools
还没翻译译自英文(tanstack.com)本页 69 段都还没译
Tools (also called "function calling") allow AI models to interact with external systems, APIs, or perform computations. TanStack AI provides an isomorphic tool system that enables type-safe, framework-agnostic tool definitions that work on both server and client.
Tools enable your AI application to:
- Fetch data from APIs or databases
- Perform calculations or data transformations
- Interact with services like email, calendars, or payment systems
- Execute client-side operations like updating UI or local storage
- Create hybrid tools that execute in both server and client contexts
Looking for provider-native tools like Anthropic web search, OpenAI code interpreter, or Gemini URL context? See Provider Tools.
Framework Support
TanStack AI works with any JavaScript framework:
- TanStack Start, Next.js, Express, Remix, Fastify, etc.
- React, Vue, Solid, Svelte, vanilla JS, etc.
TanStack AI works with any JavaScript framework.
Isomorphic Tool Architecture
TanStack AI uses a two-step tool definition process:
- Define once with
toolDefinition()- Creates a shared tool schema - Implement with
.server()or.client()- Add execution logic for each environment
This approach provides:
- Type Safety: Full TypeScript inference from Zod schemas
- Code Reuse: Define schemas once, use everywhere
- Flexibility: Tools can execute on server, client, or both
- Schema Options: Use Zod schemas or raw JSON Schema objects
Schema Options
TanStack AI supports two ways to define tool schemas:
Option 1: Zod Schemas (Recommended)
Zod schemas provide full TypeScript type inference and runtime validation:
import { z } from "zod";
const inputSchema = z.object({
location: z.string().meta({ description: "City name" }),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
});Note: For OpenAI-compatible providers, an omitted
.optional()tool field is absent when your tool runs. A.nullable()field keepsnull.
Option 2: JSON Schema Objects
For cases where you already have JSON Schema definitions or prefer not to use Zod, you can pass raw JSON Schema objects directly:
import type { JSONSchema } from "@tanstack/ai";
const inputSchema: JSONSchema = {
type: "object",
properties: {
location: {
type: "string",
description: "City name",
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
},
},
required: ["location"],
};Note: When using JSON Schema, TypeScript infers
unknownfor input/output types (it cannot derive types from a JSON Schema at compile time), so you must narrow or castargsbefore use. Zod schemas are recommended for full type safety.
Tip: Type safety from Zod schemas extends beyond tool execution. When you pass
.client()tools touseChat, a check onpart.namenarrowspart.inputandpart.output. See Type-safe tool call events.
Tool Definition
Tools are defined using toolDefinition() from @tanstack/ai:
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
// Step 1: Define the tool schema
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema: z.object({
location: z.string().meta({ description: "The city and state, e.g. San Francisco, CA" }),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
location: z.string(),
}),
});
// Step 2: Create a server implementation
const getWeatherServer = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&unit=${
unit || "fahrenheit"
}`
);
const data = await response.json();
return {
temperature: data.temperature,
conditions: data.conditions,
location: data.location,
};
});Using JSON Schema
If you prefer JSON Schema or have existing schema definitions:
import { toolDefinition } from "@tanstack/ai";
import type { JSONSchema } from "@tanstack/ai";
// Define schemas using JSON Schema
const inputSchema: JSONSchema = {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA",
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
},
},
required: ["location"],
};
const outputSchema: JSONSchema = {
type: "object",
properties: {
temperature: { type: "number" },
conditions: { type: "string" },
location: { type: "string" },
},
required: ["temperature", "conditions", "location"],
};
// Create the tool definition
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema,
outputSchema,
});
// With a raw JSON Schema, `args` is `unknown` — narrow it before use
// (prefer a Zod schema for automatic typing).
const getWeatherServer = getWeatherDef.server(async (args) => {
if (typeof args !== "object" || args === null || !("location" in args)) {
throw new Error("Invalid input: expected a location");
}
const location = String(args.location);
const unit = "unit" in args ? String(args.unit) : "fahrenheit";
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&unit=${unit}`
);
return await response.json();
});Using Tools in Chat
Server-Side
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema: z.object({
location: z.string().meta({ description: "The city and state, e.g. San Francisco, CA" }),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
location: z.string(),
}),
});
export async function POST(request: Request) {
const { messages } = await request.json();
// Create server implementation
const getWeather = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(`https://api.weather.com/v1/current?...`);
return await response.json();
});
const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [getWeather], // Pass server tools
});
return toServerSentEventsResponse(stream);
}Client-Side with Type Safety
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const updateUIDef = toolDefinition({
name: "updateUI",
description: "Update the UI with a notification message",
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ success: z.boolean() }),
});
const saveToStorageDef = toolDefinition({
name: "saveToStorage",
description: "Save data to storage",
inputSchema: z.object({ key: z.string(), value: z.string() }),
outputSchema: z.object({ saved: z.boolean() }),
});
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// Update UI state
console.log(input.message);
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = [updateUI, saveToStorage];
const textOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Infer message types for full type safety
type ChatMessages = InferChatMessages<typeof textOptions>;
function ChatComponent() {
const { messages } = useChat(textOptions);
// messages is now fully typed with tool names and outputs!
return (
<div>
{messages.map((m) => (
<div key={m.id}>{m.role}</div>
))}
</div>
);
}Hybrid Tools
Tools can be implemented for both server and client, enabling flexible execution patterns:
import { toolDefinition, chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
import { db } from "./db";
// Define once
const addToCartDef = toolDefinition({
name: "add_to_cart",
description: "Add item to shopping cart",
inputSchema: z.object({
itemId: z.string(),
quantity: z.number(),
}),
outputSchema: z.object({
success: z.boolean(),
cartId: z.string(),
}),
needsApproval: true,
});
// Server implementation - Store in database
const addToCartServer = addToCartDef.server(async (input) => {
const cart = await db.carts.create({
data: { itemId: input.itemId, quantity: input.quantity },
});
return { success: true, cartId: cart.id };
});
// Client implementation - Update local wishlist
const addToCartClient = addToCartDef.client((input) => {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
wishlist.push(input.itemId);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
return { success: true, cartId: "local" };
});On the server, pass either the definition (for client execution) or the server implementation — in separate chat() calls:
const messages = [{ role: 'user' as const, content: 'Add item abc to my cart' }]
// Pass the definition: the client will execute the tool
chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [addToCartDef],
});
// Or pass the server implementation: the server will execute the tool
chat({
adapter: openaiText("gpt-5.5"),
messages,
tools: [addToCartServer],
});Type Safety Benefits
The isomorphic architecture provides complete type safety:
import { useChat } from "@tanstack/ai-react";
import { fetchServerSentEvents } from "@tanstack/ai-client";
function CartChat() {
const { messages: uiMessages } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
// In your React component
uiMessages.forEach((message) => {
message.parts.forEach((part) => {
if (part.type === 'tool-call' && part.name === 'add_to_cart') {
// ✅ TypeScript knows part.name is literally 'add_to_cart'
// ✅ part.input is typed as { itemId: string, quantity: number }
// ✅ part.output is typed as { success: boolean, cartId: string } | undefined
if (part.output) {
console.log(part.output.cartId); // ✅ Fully typed!
}
}
});
});
return null;
}Tool Execution Flow
- Model decides to call a tool - Based on user input and tool descriptions
- Tool is identified - Server or client implementation
- Tool executes - Automatically on server or client
- Result is returned - To the model as a tool result message
- Model continues - Uses the result to generate a response
Run tools one at a time
When the model calls several server tools in one turn, they start at the same time, and the model gets the results in the order of its calls. Some tools must not overlap, for example a tool that writes a file and a tool that runs the tests.
Set toolExecution: 'sequential' on chat() to run the tools of each turn one at a time, in call order:
import { chat, toolDefinition } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const writeFile = toolDefinition({
name: 'write_file',
description: 'Write a file',
inputSchema: z.object({ path: z.string(), content: z.string() }),
}).server(async ({ path }) => ({ written: path }))
const runTests = toolDefinition({
name: 'run_tests',
description: 'Run the test suite',
inputSchema: z.object({}),
}).server(async () => ({ passed: true }))
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Fix the bug, then run the tests.' }],
tools: [writeFile, runTests],
toolExecution: 'sequential',
})Client tools do not change: the client runs them as their calls arrive.
Choose when the model calls a tool
Your weather assistant gives advice before it looks up the weather. Or you want a plain text answer, with no tool calls at all. Set toolChoice, and the model does what you ask.
Make the model call get_weather first:
import {
chat,
toServerSentEventsResponse,
toolDefinition,
type ChatMiddleware,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const getWeather = toolDefinition({
name: 'get_weather',
description: 'Get the current weather for a city',
inputSchema: z.object({ city: z.string() }),
}).server(async ({ city }) => ({ city, temperature: 21 }))
// Force the tool on the first model call only.
const weatherFirst: ChatMiddleware = {
name: 'weather-first',
onConfig: (ctx) => {
if (ctx.phase === 'beforeModel' && ctx.iteration === 0) {
return { toolChoice: { type: 'tool', name: 'get_weather' } }
}
},
}
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
tools: [getWeather],
middleware: [weatherFirst],
})
return toServerSentEventsResponse(stream)
}The first call must call get_weather. The next call goes back to the default, so the model answers with the forecast.
Your client code does not change. toolChoice is a server option.
The values
| Value | What the model does |
|---|---|
'auto' | It decides. |
'none' | It calls no tool. |
'required' | It must call a tool. |
{ type: 'tool', name } | It must call the tool with this name. |
Set it for the whole run
chat({ toolChoice }) applies to every model call of the run. With 'required' or a named tool, the model never writes a text answer. The run stops at the loop limit, after a tool result.
For one lookup and nothing more, stop after one call:
import { maxIterations } from '@tanstack/ai'
const oneLookup = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Weather in Oslo?' }],
tools: [getWeather],
toolChoice: 'required',
agentLoopStrategy: maxIterations(1),
})In chat({ toolChoice: { type: 'tool', name } }), your editor suggests the names of the tools that you pass. Any other string also works, for example the name of a provider tool.
Need a text answer on the last call? Return { toolChoice: 'none' } from a middleware. See Change the tool choice of a call.
Rules for every adapter
- If the request has no tools, the adapter sends no tool choice.
- A tool choice in
modelOptionswins overtoolChoice, for exampletool_choiceon OpenAI. - The adapter sends the name as you give it. It does not make sure that a tool has this name.
Provider notes
OpenAI, Grok, Groq, Mistral, OpenRouter, and the other OpenAI-compatible adapters send the value as it is. Some providers change it:
- Claude (Anthropic and Amazon Bedrock): some calls cannot force a tool. Then
'required'and a named tool become'auto'.claude-fable-5-1,claude-mythos-5-1,claude-opus-5-5, andclaude-sonnet-5-5never take a forced tool.- No Claude model takes a forced tool while thinking is on. See Reasoning.
- Amazon Bedrock (Converse API): Converse has no
nonevalue, so'none'sends no tools. The tool calls in the history go out as text. - Gemini: the value becomes
functionCallingConfig(AUTO,NONE, orANY). A named tool isANYwithallowedFunctionNames. With only provider tools, such as Google Search, Gemini gets no tool config. - Ollama: Ollama has no tool choice. See Ollama.
- CLI-style adapters: each page says which values work. See Claude Code, Codex, OpenCode, Grok Build, and ACP-compatible.
Progress Events and Runtime Context
A server tool's .server() implementation receives a second argument, the ToolExecutionContext — { context, toolCallId, emitCustomEvent }. Use emitCustomEvent to stream typed progress to the client while the tool runs, and context to read request-scoped dependencies (auth, DB clients, etc.):
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
type ImportContext = {
db: {
read(source: string): Promise<unknown[]>;
write(rows: unknown[]): Promise<void>;
};
};
const importDataDef = toolDefinition({
name: "import_data",
description: "Import data from a source",
inputSchema: z.object({ source: z.string() }),
outputSchema: z.object({ imported: z.number() }),
});
const importData = importDataDef.server<ImportContext>(async (input, { context, emitCustomEvent }) => {
emitCustomEvent("progress", { step: 1, total: 3 });
const rows = await context.db.read(input.source);
emitCustomEvent("progress", { step: 2, total: 3 });
await context.db.write(rows);
emitCustomEvent("progress", { step: 3, total: 3 });
return { imported: rows.length };
});
Each emitCustomEvent call flushes through durability immediately, so the
client can show progress while the tool still runs. Pass { batch: true }
only for a high-volume stream. See Custom Events.
See Server Tools for the full runtime-context pattern.
Tool States
Tools go through different states during execution:
awaiting-input- Tool call received, waiting for argumentsinput-streaming- Partial arguments being streamedinput-complete- All arguments receivedapproval-requested- Tool requires user approval (ifneedsApproval: true)approval-responded- User has approved/denied
Once arguments (and approval, if required) are in, the result appears as part.output on the tool-call part and as a separate sibling tool-result part whose state is complete or error. See Tool Architecture for the full state model.
Tip: If your use case involves calling multiple tools with complex logic (filtering, aggregation, parallel calls), consider Code Mode — it lets the LLM write a TypeScript program that orchestrates tools in a single execution instead of one tool call at a time.
Next Steps
- Server Tools - Learn about server-side tool execution
- Client Tools - Learn about client-side tool execution
- Tool Approval Flow - Implement approval workflows
- How Tools Work - Deep dive into the tool architecture
- MCP Server Tools - Connect to external MCP servers for additional tools