n4nAI

Vercel AI SDK tool calling: a complete walkthrough

A practical vercel ai sdk tool calling tutorial: step by step, build a typed tool-calling agent with streaming, error handling, and provider fallback in Node.

n4n Team3 min read674 words

Audio narration

Coming soon — every post will get a voice note here.

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 (npm or pnpm)
  • TypeScript 5.x
  • An OpenAI API key, or any OpenAI-compatible endpoint key
  • Familiarity with async/await and 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: Without maxSteps > 1, the model emits a tool call but the SDK stops. You’ll see finishReason: 'tool-calls' and no follow-up text.
  • Non-serializable returns: Returning a Date or Map breaks 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; bare z.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.

Tagsvercel-ai-sdktool-callingfunction-callingwalkthrough

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk tool & function calling posts →