Skip to main content

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:

  1. Define once with toolDefinition() - Creates a shared tool schema
  2. 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:

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 keeps null.

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 unknown for input/output types (it cannot derive types from a JSON Schema at compile time), so you must narrow or cast args before use. Zod schemas are recommended for full type safety.

Tip: Type safety from Zod schemas extends beyond tool execution. When you pass .client() tools to useChat, a check on part.name narrows part.input and part.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

  1. Model decides to call a tool - Based on user input and tool descriptions
  2. Tool is identified - Server or client implementation
  3. Tool executes - Automatically on server or client
  4. Result is returned - To the model as a tool result message
  5. 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

ValueWhat 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 modelOptions wins over toolChoice, for example tool_choice on 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, and claude-sonnet-5-5 never take a forced tool.
    • No Claude model takes a forced tool while thinking is on. See Reasoning.
  • Amazon Bedrock (Converse API): Converse has no none value, so 'none' sends no tools. The tool calls in the history go out as text.
  • Gemini: the value becomes functionCallingConfig (AUTO, NONE, or ANY). A named tool is ANY with allowedFunctionNames. 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 arguments
  • input-streaming - Partial arguments being streamed
  • input-complete - All arguments received
  • approval-requested - Tool requires user approval (if needsApproval: 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