n4nAI

Vercel AI SDK tool calling: a step-by-step tutorial

Hands-on vercel ai sdk tool calling tutorial: build a typed tool-calling agent with streaming, error handling, and provider fallback in Node.

n4n Team3 min read727 words

Audio narration

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

This vercel ai sdk tool calling tutorial shows how to wire typed tools into the Vercel AI SDK and run them against a real model. We start from an empty Node project and end with a streaming agent that calls functions, streams intermediate text, and survives tool errors.

Prerequisites

  • Node.js 18.18+ (fetch + stream support)
  • A package manager (npm or pnpm)
  • An OpenAI API key, or any OpenAI-compatible endpoint key
  • Basic TypeScript familiarity

If you plan to follow the provider-swap section, grab a key from an OpenAI-compatible gateway. n4n.ai provides one OpenAI-compatible endpoint that addresses 240+ models with automatic fallback when a provider is rate-limited or degraded, which is handy for production.

Project Setup

Create a directory and init a TypeScript project:

mkdir tool-calling-demo && cd tool-calling-demo
npm init -y
npm install ai @ai-sdk/openai zod
npm install -D typescript @types/node tsx

Add a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["*.ts"]
}

Create .env.local:

OPENAI_API_KEY=sk-...

We will write all code in index.ts and run with npx tsx index.ts.

Define Your First Tool

The AI SDK models tools as objects with a description, a Zod schema for parameters, and an async execute function. The model sees the description and schema; your code runs execute.

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

const getWeather = tool({
  parameters: z.object({
    city: z.string().describe('City name, e.g. "Paris"'),
  }),
  execute: async ({ city }) => {
    // Stand-in for a real API call
    const fakeDb: Record<string, { tempC: number; conditions: string }> = {
      paris: { tempC: 21, conditions: 'clear' },
      tokyo: { tempC: 28, conditions: 'humid' },
    };
    const data = fakeDb[city.toLowerCase()];
    if (!data) return { error: `No weather for ${city}` };
    return { city, ...data };
  },
});

The Zod schema is not optional. It drives the JSON schema sent to the model and gives you runtime validation for free.

Single Tool Call with generateText

generateText is the simplest entry point. It returns the final text, the tool calls the model requested, and the results of executing them.

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?',
  tools: { getWeather },
});

console.log('TEXT:', text);
console.log('CALLS:', toolCalls);
console.log('RESULTS:', toolResults);

Expected output (abridged):

TEXT: The weather in Paris is clear with a temperature of 21°C.
CALLS: [ { type: 'tool-call', toolName: 'getWeather', args: { city: 'Paris' } } ]
RESULTS: [ { toolName: 'getWeather', args: { city: 'Paris' }, result: { city: 'Paris', tempC: 21, conditions: 'clear' } } ]

The model emitted a tool call, the SDK executed getWeather, fed the result back, and the model produced a natural language answer. That round trip is the core of any vercel ai sdk tool calling tutorial.

Multi-step agents

If the model needs to chain tools (e.g., weather then calculator on the result), set maxSteps:

const { text } = await generateText({
  model: openai('gpt-4o-mini'),
  prompt: 'Add 10 to the temperature in Paris.',
  tools: { getWeather, calculator },
  maxSteps: 3,
});

Without maxSteps, the SDK returns after the first tool result. The default is a single step.

Streaming Tool Calls with streamText

Production chat UIs need tokens as they generate. streamText returns a textStream async iterable and promises for the final tool calls.

import { streamText } from 'ai';

const result = streamText({
  model: openai('gpt-4o-mini'),
  prompt: 'What is the weather in Tokyo and London?',
  tools: { getWeather },
});

for await (const delta of result.textStream) {
  process.stdout.write(delta);
}
console.log('\n---');

const calls = await result.toolCalls;
const results = await result.toolResults;
console.log('TOOL CALLS:', calls);
console.log('TOOL RESULTS:', results);

You will see text stream in, then after the stream closes, the tool call metadata prints. The SDK automatically runs execute for each call before finalizing.

Inspecting Partial Tool Calls

If you need to show a “calling tool…” spinner, consume result.fullStream which yields tool-call and tool-result events interleaved with text deltas.

for await (const part of result.fullStream) {
  if (part.type === 'tool-call') console.log('CALLING', part.toolName, part.args);
  if (part.type === 'tool-result') console.log('GOT', part.result);
}

This avoids waiting for the entire generation to finish before knowing which tools fired.

Multiple Tools and Error Handling

Add a second tool and make execute fail gracefully. The model can recover if you return a structured error instead of throwing.

const calculator = tool({
  parameters: z.object({ expression: z.string() }),
  execute: async ({ expression }) => {
    try {
      // Never use eval in prod; this is a demo
      const sanitized = expression.replace(/[^0-9+\-*/(). ]/g, '');
      const value = Function(`"use strict"; return (${sanitized})`)();
      if (typeof value !== 'number' || !Number.isFinite(value)) {
        return { error: 'Invalid expression' };
      }
      return { value };
    } catch {
      return { error: 'Could not compute' };
    }
  },
});

const { text } = await generateText({
  model: openai('gpt-4o-mini'),
  prompt: 'What is 12 * (4 + 3) and the weather in Tokyo?',
  tools: { getWeather, calculator },
});
console.log(text);

Expected output:

12 * (4 + 3) is 84. The weather in Tokyo is humid with a temperature of 28°C.

If the calculator returns { error: '...' }, the model sees that and can apologize or retry. Throwing inside execute also works; the SDK captures the error as a tool result with an error field, but returning a shaped object gives you cleaner logs.

Swap in an OpenAI-Compatible Gateway

The AI SDK speaks the OpenAI chat protocol, so any compliant base URL works. To avoid vendor lock or to access more models, instantiate a custom provider:

import { createOpenAI } from '@ai-sdk/openai';

const gateway = createOpenAI({
  baseURL: 'https://api.n4n.ai/v1',
  apiKey: process.env.N4N_KEY,
});

const { text } = await generateText({
  model: gateway('anthropic/claude-3.5-sonnet'),
  prompt: 'Weather in Paris via tool?',
  tools: { getWeather },
});

n4n.ai honors client routing directives and forwards provider cache-control hints, so the same tool definitions work across 240+ models without code changes. This is the part of the vercel ai sdk tool calling tutorial where you stop worrying about which provider is up.

Checkpoint: Expected Outputs

At this point, running the full script should produce:

  1. A non-streamed answer with one tool call for Paris.
  2. A streamed answer for Tokyo/London with tool calls printed after text.
  3. A combined math + weather answer.
  4. (If gateway configured) A response from a non-OpenAI model using the same tools.

If you see toolCalls: [], the model likely ignored the tool. Tighten the description or use temperature: 0.

Production Considerations

  • Validation: Zod schemas protect you, but always sanity-check args inside execute.
  • Timeouts: Wrap execute with Promise.race against a timeout; a hung tool blocks the whole generation.
  • Billing: Tool calls still consume input tokens for the schema and output tokens for the call. With a gateway that does per-token usage metering, log usage from the result to track cost.
  • Streaming UI: Feed result.textStream to your frontend and expose toolCalls via a separate websocket or poll fullStream.
  • Error isolation: Never let an unhandled rejection escape execute. Return an error shape.

That’s the full loop. You defined tools, ran them synchronously and streamed, handled errors, and pointed the SDK at a different endpoint. The patterns here are the same ones you’ll extend for RAG, agentic loops, or multi-step workflows.

Tagsvercel-ai-sdktool-callingtutorialllm-api

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 deep dive posts →