Skip to main content
Jodei
Insights

Tool Use with the Vercel AI SDK: LLM Function Calling in Next.js

Tool use turns a text model into a reasoning engine that can invoke real code. Here's how to define tools with Zod, wire them into streamText, and surface results in a Next.js 16 Server Component — without a waterfall.

6 min read

A plain text completion is useful. A model that can call your code is transformative. Tool use — also called function calling — lets the LLM decide at runtime which operations to invoke, with what arguments, and then incorporate the results into its next response. The Vercel AI SDK makes this straightforward to wire up in Next.js, but there are a few sharp edges worth knowing before you ship.

Defining tools with Zod

Every tool has three parts: a description the model reads to decide when to call it, a Zod schema for the arguments, and an execute function that runs on the server. The SDK validates arguments against the schema before calling execute, so a hallucinated field or wrong type never reaches your business logic.

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

const getWeather = tool({
  description: "Return current weather for a city.",
  parameters: z.object({
    city: z.string().describe("City name, e.g. 'San Francisco'"),
    unit: z.enum(["celsius", "fahrenheit"]).default("fahrenheit"),
  }),
  execute: async ({ city, unit }) => {
    // call your weather API here
    return { city, temperature: 72, unit, condition: "sunny" };
  },
});

Keep descriptions precise. The model uses them as its only signal for when to call a tool — vague descriptions produce spurious calls and missed opportunities. If two tools overlap, say explicitly which one takes priority.

Streaming with tools in a Server Component

Pass your tools to streamText alongside the model and prompt. The stream delivers text tokens interspersed with tool calls; the SDK handles the sequencing.

import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

const { textStream } = await streamText({
  model: openai("gpt-4o"),
  tools: { getWeather },
  prompt: "What's the weather in San Francisco today? Use Fahrenheit.",
});

for await (const chunk of textStream) {
  process.stdout.write(chunk);
}

In a Route Handler or Server Action, pipe toDataStreamResponse()to the client so the AI SDK's React hooks (useChat, useCompletion) can reconstruct the stream — including tool-call deltas — on the browser side.

Multi-step tool calls with maxSteps

By default the model makes one round of tool calls and stops. Real tasks often require chaining: fetch the weather, then look up a packing list for that weather, then summarise. Set maxSteps to allow the model to loop until it decides no more tool calls are needed or the step limit is hit.

const { text } = await generateText({
  model: openai("gpt-4o"),
  tools: { getWeather, getPackingList },
  maxSteps: 5,
  prompt: "What should I pack for a trip to San Francisco tomorrow?",
});

Each step increments your token usage, and the full conversation history is sent back on every turn. For tight latency budgets, cap maxSteps at two or three and prefer composing tools server-side rather than relying on the model to sequence them.

Server Components and tool results

When you call generateText (non-streaming) in a Server Component, you can render tool results directly into JSX without shipping any AI logic to the browser. The component fetches, runs tools, and returns final markup — one round trip, no client state.

// app/weather/page.tsx  (Server Component)
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

export default async function WeatherPage() {
  const { text } = await generateText({
    model: openai("gpt-4o"),
    tools: { getWeather },
    maxSteps: 2,
    prompt: "Give me a one-sentence summary of the weather in San Francisco.",
  });

  return <p>{text}</p>;
}

Wrap this component in Suspense if it sits alongside fast-rendering content — the weather summary can stream in without blocking the rest of the page.

What to watch out for

  • Tool execute runs on the server. Secrets, database credentials, and internal APIs are safe inside execute — none of that reaches the client. But validate tool arguments defensively anyway; the schema is your contract, not a security boundary by itself.
  • Models can refuse to call tools. If the prompt is ambiguous or the model decides plain text is sufficient, it may skip tool calls entirely. Handle the no-tool-call path explicitly — your UI should not assume a tool was always invoked.
  • Streaming tool calls arrive as deltas. Argument JSON streams token by token, so you cannot parse it until the tool call closes. The SDK buffers this automatically; if you are processing the raw stream yourself, wait for the tool-call event before reading arguments.

Putting it together

Tool use is the bridge between language models and your application's real capabilities. The pattern is consistent: define tools with tight Zod schemas and precise descriptions, pass them to streamText or generateText, and let the model decide when to reach for them. For multi-step workflows, set maxSteps and budget your token cost accordingly. For server-side rendering, skip streaming entirely and call generateText in a Server Component behind a Suspense boundary — you get composable, typesafe AI results with zero client JavaScript.