Tools

Models write sentences well; they are weak at exact math and “what is the weather right now”. Tools are functions the model can call: a description, a Zod inputSchema, and an optional execute. The AI SDK runs execute on the server, feeds the result back, and lets the model continue.

Use the tool() helper so execute parameters infer correctly.


Defining a tool

import { tool } from 'ai';
import { z } from 'zod';

const weather = tool({
  description: 'Get the weather in a location (fahrenheit)',
  inputSchema: z.object({
    location: z.string().describe('The location to get the weather for'),
  }),
  execute: async ({ location }) => {
    const temperature = Math.round(Math.random() * (90 - 32) + 32);
    return { location, temperature };
  },
});
FieldRole
descriptionWhen the model should call it
inputSchemaZod or JSON Schema; validates model arguments
executeOptional: omit it to forward the call to the client or a queue
strictStrict JSON on providers that support it

tools is an object: keys are tool names.


Wire into streamText (chat route)

Default stopWhen is isStepCount(1): after one tool call the model stops, so the UI often shows JSON and no natural-language summary. Allow a loop:

import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  isStepCount,
  streamText,
  toUIMessageStream,
  tool,
  type UIMessage,
} from 'ai';
import { z } from 'zod';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'openai/gpt-4.1-mini',
    messages: await convertToModelMessages(messages),
    stopWhen: isStepCount(5),
    tools: {
      weather: tool({
        description: 'Get the weather in a location (fahrenheit)',
        inputSchema: z.object({
          location: z.string().describe('The location to get the weather for'),
        }),
        execute: async ({ location }) => ({
          location,
          temperature: Math.round(Math.random() * 58 + 32),
        }),
      }),
      convertFahrenheitToCelsius: tool({
        description: 'Convert fahrenheit to celsius',
        inputSchema: z.object({
          temperature: z.number().describe('Temperature in fahrenheit'),
        }),
        execute: async ({ temperature }) => ({
          celsius: Math.round((temperature - 32) * (5 / 9)),
        }),
      }),
    },
  });

  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  });
}

Ask “What is the temperature in New York in celsius?” and the model should call weather, then convertFahrenheitToCelsius, then answer in text. Log toolResults in onStepEnd if you want.

The same tools + stopWhen work with generateText when there is no UI.


Render tool parts on the client

Tool parts are always typed tool-{toolName}:

{message.parts.map((part, i) => {
  switch (part.type) {
    case 'text':
      return <div key={i}>{part.text}</div>;
    case 'tool-weather':
    case 'tool-convertFahrenheitToCelsius':
      return <pre key={i}>{JSON.stringify(part, null, 2)}</pre>;
    default:
      return null;
  }
})}

State walks input-streaminginput-availableoutput-available. Show “Looking up weather…” before output-available.


Approval, safety, anti-patterns

Tools with execute run automatically. Dangerous actions (delete, pay) should use toolApproval (user-approval, etc.). Do not eval untrusted input in production.

Tools run on the server: they can hit databases and internal APIs. Never return secrets to the model or the browser. Write clear description and .describe() text to cut down on random calls.

stopWhen: isStepCount(n) is a cap, not a quota. Structured output also counts as a step—leave enough room when combining tools and output.


Next

评论