A vercel ai sdk weather tool example is the fastest way to see function calling working end-to-end without wrestling with raw API schemas. This tutorial builds a small Node script that lets a model request current weather for a city, executes the tool, and returns a grounded answer.
Prerequisites
- Node.js 18 or newer (global
fetchavailable) - A package manager (
npmused below) - An OpenAI API key, or any OpenAI-compatible endpoint
- TypeScript basics and a terminal
Project setup
Create a directory and install the required packages. The Vercel AI SDK core lives in ai, the OpenAI provider in @ai-sdk/openai, and zod handles parameter validation.
mkdir weather-tool && cd weather-tool
npm init -y
npm install ai @ai-sdk/openai zod
npm install -D typescript @types/node tsx
Add a minimal tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true
},
"include": ["*.ts"]
}
Run scripts with tsx to avoid a build step: npx tsx index.ts.
Define the weather tool
The SDK’s tool helper takes a Zod schema and an execute function. We’ll use Open-Meteo’s free, key-less geocoding and forecast APIs so the example runs without extra signups.
import { tool } from 'ai';
import { z } from 'zod';
export const getWeather = tool({
parameters: z.object({
city: z.string().describe('City name, e.g. "San Francisco"'),
}),
execute: async ({ city }) => {
const geoRes = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geo = await geoRes.json();
if (!geo.results?.length) return { error: 'Location not found' };
const { latitude, longitude, name, country } = geo.results[0];
const weatherRes = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,weather_code`
);
const weather = await weatherRes.json();
return {
location: `${name}, ${country}`,
temperature: weather.current.temperature_2m,
units: weather.current_units.temperature_2m,
weatherCode: weather.current.weather_code,
};
},
});
The execute function returns a plain object. The SDK serializes it and feeds it back to the model as the tool result.
What the SDK sends to the model
Under the hood, the Zod schema becomes a JSON function definition. For the tool above, the provider receives roughly:
{
"name": "getWeather",
"description": "Get current temperature and weather code for a city",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name, e.g. \"San Francisco\"" }
},
"required": ["city"]
}
}
You never write this by hand. The SDK keeps your TypeScript types and the wire format in sync.
Wire up the model and run
Create index.ts. We call generateText with maxSteps: 2 so the model can emit a tool call, receive the result, and then produce a final natural-language answer.
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { getWeather } from './weather-tool';
const { text, toolCalls, toolResults } = await generateText({
model: openai('gpt-4o-mini'),
prompt: 'What is the weather in Tokyo right now?',
tools: { getWeather },
maxSteps: 2,
});
console.log('Final answer:', text);
console.log('Tool calls:', JSON.stringify(toolCalls, null, 2));
console.log('Tool results:', JSON.stringify(toolResults, null, 2));
Set your key:
export OPENAI_API_KEY=sk-...
npx tsx index.ts
If you prefer to route through a gateway that fronts 240+ models with automatic fallback when a provider is degraded, point the provider at n4n.ai’s OpenAI-compatible endpoint by passing baseURL: 'https://api.n4n.ai/v1' to openai(). The tool code stays identical.
Controlling tool selection
By default the model decides whether to call a tool. You can force or restrict it:
// Force the model to call getWeather exactly once
generateText({ /* ... */, toolChoice: { type: 'tool', toolName: 'getWeather' } });
// Disable tools entirely for a plain completion
generateText({ /* ... */, toolChoice: 'none' });
Use toolChoice: 'required' in tests to assert your execute logic without guessing model behavior.
Expected output
On a successful run you’ll see a final answer plus the intermediate tool trace:
Tool calls: [
{
"toolCallId": "call_abc",
"toolName": "getWeather",
"args": { "city": "Tokyo" }
}
]
Tool results: [
{
"toolCallId": "call_abc",
"result": {
"location": "Tokyo, Japan",
"temperature": 12.3,
"units": "°C",
"weatherCode": 3
}
}
]
Final answer: The current weather in Tokyo is 12.3°C with overcast conditions (weather code 3).
The exact temperature and wording vary. The key checkpoint is that toolCalls contains the parsed city argument and toolResults contains the fetched data.
Streaming for chat UIs
For a responsive interface, swap generateText for streamText. The tool still executes server-side; the SDK yields text deltas after the tool result returns.
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { getWeather } from './weather-tool';
const result = await streamText({
model: openai('gpt-4o-mini'),
prompt: 'Should I bring a jacket in Oslo today?',
tools: { getWeather },
maxSteps: 2,
});
for await (const delta of result.textStream) {
process.stdout.write(delta);
}
The stream emits nothing until the model has the weather data, then prints the recommendation.
Error handling and validation
Zod rejects malformed arguments before execute runs. If the geocoding API returns no results, we return an error object instead of throwing, so the model can recover:
if (!geo.results?.length) return { error: 'Location not found' };
Wrap external fetches in try/catch if you want to surface HTTP failures as tool errors:
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
return { error: (e as Error).message };
}
The model sees the error field and can ask for clarification or pick another city. This keeps the conversation alive instead of crashing the process.
Extending the vercel ai sdk weather tool example
Add a second tool for forecasts:
export const getForecast = tool({
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
// reuse geocoding, then request daily params
// ...
return { location, daily: [/* ... */] };
},
});
Register both in the tools map. The SDK dispatches based on the model’s choice; maxSteps may need a higher value for multi-tool chains.
Because the Vercel AI SDK normalizes tool calling across providers, the same vercel ai sdk weather tool example runs against Anthropic or Google models by swapping the provider import. Only the model line changes.
Production notes
- Cache geocoding results to avoid redundant calls for repeat cities.
- Set
temperature: 0for deterministic tool selection in integration tests. - Meter token usage if you bill customers; the gateway or provider returns
usageon the response object fromgenerateText. - Keep
executeside-effect free where possible. Tools that write data should confirm with the user or use a separatewriteflag.
That’s the full loop: define a tool, hand it to the model, let the model decide when to call it, and return grounded data. The pattern generalizes to any external API you want to expose to an LLM.