This vercel ai sdk tool calling tutorial walks through building a typed agent that invokes a backend function, streams intermediate state, and degrades gracefully when a model hiccups. We’ll use the ai core package, the OpenAI provider adapter, and Zod for parameter validation—no mock hand-waving.
Prerequisites
- Node.js 18.18+ (fetch + stream native)
- A package manager (
npmorpnpm) - TypeScript 5.x
- An OpenAI API key, or any OpenAI-compatible endpoint key
- Familiarity with
async/awaitand basic Zod
If you plan to follow the provider-swap section, set OPENAI_API_KEY or a gateway key in your env.
Project setup
Initialize a minimal TS project:
mkdir vc-tool-call && cd vc-tool-call
npm init -y
npm install ai @ai-sdk/openai zod
npm install -D typescript @types/node
npx tsc --init --module esnext --target es2022 --moduleResolution bundler
Create src/index.ts. We’ll build the example there. The ai package is provider-agnostic; @ai-sdk/openai supplies the model binding.
Define a tool with Zod
The SDK’s tool helper binds a description, a parameter schema, and an execute function. The model sees the schema; your code gets typed args.
import { tool } from 'ai';
import { z } from 'zod';
const getWeather = tool({
parameters: z.object({
location: z.string().describe('City name, e.g. "Paris"'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ location, unit }) => {
// Stand-in for a real API call
const temp = unit === 'celsius' ? 21 : 70;
return {
location,
temperature: temp,
unit,
condition: 'partly cloudy',
};
},
});
The execute return value is serialized and sent back to the model as the tool result. Keep it JSON-friendly. The Zod schema doubles as documentation and runtime guard.
Single-shot generation with generateText
For batch use, generateText runs the prompt, lets the model emit a tool call, executes it, and returns the final text.
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const { text, toolCalls, toolResults } = await generateText({
model: openai('gpt-4o-mini'),
prompt: 'What is the weather in Paris? Reply with the temperature.',
tools: { getWeather },
maxSteps: 2, // allow one tool round-trip
});
console.log(text);
console.log('toolCalls:', toolCalls);
Expected output (abridged):
The temperature in Paris is 21°C and it is partly cloudy.
toolCalls: [ { toolName: 'getWeather', args: { location: 'Paris', unit: 'celsius' } } ]
If the model skips the tool, toolCalls is empty and text holds the direct answer. maxSteps is mandatory here; without it the call stops at finishReason: 'tool-calls'.
Streaming with streamText
As with the rest of this vercel ai sdk tool calling tutorial, the streaming variant matters for real UIs. streamText exposes a textStream and automatically executes tools between steps when maxSteps > 1.
import { streamText } from 'ai';
const result = await streamText({
model: openai('gpt-4o-mini'),
prompt: 'Weather in Tokyo, then tell me if I need a jacket.',
tools: { getWeather },
maxSteps: 3,
});
for await (const delta of result.textStream) {
process.stdout.write(delta);
}
You can also await result.toolCalls after the stream closes to log what fired. The SDK handles the intermediate request/response cycle internally.
Defining multiple tools and letting the model route
Real agents expose several capabilities. Add a second tool and let the model pick:
const getStockPrice = tool({
parameters: z.object({ symbol: z.string().describe('e.g. "AAPL"') }),
execute: async ({ symbol }) => ({
symbol,
price: 187.34,
currency: 'USD',
}),
});
const { text, toolCalls } = await generateText({
model: openai('gpt-4o-mini'),
prompt: 'What is the weather in London and what is AAPL trading at?',
tools: { getWeather, getStockPrice },
maxSteps: 3,
});
The model will emit two tool calls in one step. The SDK executes both and feeds results back together. Inspect toolCalls.map(t => t.toolName) to confirm routing.
Inspecting and overriding tool execution
Sometimes you want to gate a tool behind auth or mutate args. Use onStepFinish or handle toolCalls manually:
const { text, toolCalls, toolResults, finishReason } = await generateText({
model: openai('gpt-4o-mini'),
prompt: 'Weather in Berlin?',
tools: { getWeather },
maxSteps: 2,
onStepFinish: ({ toolCalls }) => {
if (toolCalls?.some(t => t.toolName === 'getWeather')) {
// audit log, rate limit, etc.
}
},
});
If execute throws, the SDK surfaces the error as a tool result with an error flag. The model can then recover or apologize. Never let an unhandled rejection escape execute—it will abort the whole generation.
Swapping the provider without rewriting tools
The Vercel AI SDK speaks OpenAI’s wire format, so any compliant base URL works. n4n.ai exposes one OpenAI-compatible endpoint that addresses 240+ models and applies automatic fallback when a provider is rate-limited, which is useful when you don’t want to hard-code a single vendor.
import { createOpenAI } from '@ai-sdk/openai';
const gateway = createOpenAI({
baseURL: 'https://api.n4n.ai/v1',
apiKey: process.env.N4N_API_KEY,
});
const { text } = await generateText({
model: gateway('openai/gpt-4o-mini'),
prompt: 'Weather in Rome?',
tools: { getWeather },
maxSteps: 2,
});
The tool definition stays identical. You only changed the model argument. Client routing directives and provider cache-control hints are forwarded unchanged.
Validating untrusted args
Zod runs before execute. If the model sends malformed args, the SDK catches it and returns a validation error to the model instead of crashing your process.
const safeDivide = tool({
parameters: z.object({
a: z.number(),
b: z.number().refine(v => v !== 0, 'b must not be zero'),
}),
execute: async ({ a, b }) => a / b,
});
A bad b: 0 yields a tool result containing the refinement message; the model can retry with a corrected call. This closes the loop without custom error plumbing.
Testing tools in isolation
Because execute is a plain async function, unit-test it without the model:
import assert from 'node:assert';
const res = await getWeather.execute!({ location: 'Oslo', unit: 'celsius' });
assert.equal(res.location, 'Oslo');
assert.equal(res.temperature, 21);
TypeScript infers the args type from the Zod schema, so mistakes in tests fail at compile time.
Common failure modes
- Missing
maxSteps: WithoutmaxSteps > 1, the model emits a tool call but the SDK stops. You’ll seefinishReason: 'tool-calls'and no follow-up text. - Non-serializable returns: Returning a
DateorMapbreaks the second LLM round-trip. Return plain objects. - Over-broad tool descriptions: Vague descriptions cause the model to call the wrong tool. Be explicit about when to use each.
- Forgetting
describe(): The model relies on field descriptions; barez.string()yields poor arg quality.
Wrapping up the agent loop
For a REPL-style agent, wrap generateText in a loop and feed text back as the next user message only if finishReason is stop. That gives you a controllable multi-turn tool user without a heavier framework.
The vercel ai sdk tool calling tutorial above covers the parts that actually break in production: streaming, validation, multi-tool routing, and provider independence. Clone the snippet, point it at your own tools, and ship.