Prerequisites
npm install @openrouter/agent zod
pnpm add @openrouter/agent zod
yarn add @openrouter/agent zod
bun add @openrouter/agent zod
deno add npm:@openrouter/agent npm:zod
export OPENROUTER_API_KEY=your_openrouter_key
Basic Implementation
import { OpenRouter, tool } from '@openrouter/agent';
import { z } from 'zod';
const openrouter = new OpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
// WMO weather codes, as Open-Meteo returns them
const WEATHER_CODES: Record<number, string> = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Fog',
48: 'Rime fog',
51: 'Light drizzle',
53: 'Drizzle',
55: 'Dense drizzle',
56: 'Light freezing drizzle',
57: 'Dense freezing drizzle',
61: 'Light rain',
63: 'Rain',
65: 'Heavy rain',
66: 'Light freezing rain',
67: 'Heavy freezing rain',
71: 'Light snow',
73: 'Snow',
75: 'Heavy snow',
77: 'Snow grains',
80: 'Light rain showers',
81: 'Rain showers',
82: 'Violent rain showers',
85: 'Light snow showers',
86: 'Heavy snow showers',
95: 'Thunderstorm',
96: 'Thunderstorm with hail',
99: 'Thunderstorm with heavy hail',
};
// Open-Meteo forecasts by coordinates, so look the city up first
async function findCity(city: string) {
const response = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
if (!response.ok) {
throw new Error(`Geocoding API error: ${response.status}`);
}
const data = await response.json();
const place = data.results?.[0];
if (!place) {
throw new Error(`City not found: ${city}`);
}
return place;
}
const weatherTool = tool({
name: 'get_weather',
description: 'Get current weather conditions for any city worldwide',
inputSchema: z.object({
city: z.string().describe('City name, e.g., "San Francisco" or "London"'),
units: z
.enum(['celsius', 'fahrenheit'])
.default('celsius')
.describe('Temperature units'),
}),
outputSchema: z.object({
temperature: z.number(),
feelsLike: z.number(),
conditions: z.string(),
humidity: z.number(),
windSpeed: z.number(),
windDirection: z.number().describe('Degrees clockwise from north'),
location: z.object({
name: z.string(),
region: z.string(),
country: z.string(),
}),
}),
execute: async ({ city, units }) => {
const place = await findCity(city);
const params = new URLSearchParams({
latitude: String(place.latitude),
longitude: String(place.longitude),
current:
'temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m',
temperature_unit: units,
});
const response = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
const { current } = await response.json();
return {
temperature: current.temperature_2m,
feelsLike: current.apparent_temperature,
conditions: WEATHER_CODES[current.weather_code] ?? 'Unknown',
humidity: current.relative_humidity_2m,
windSpeed: current.wind_speed_10m,
windDirection: current.wind_direction_10m,
location: {
name: place.name,
region: place.admin1 ?? '',
country: place.country,
},
};
},
});
Usage
const result = openrouter.callModel({
model: 'openai/gpt-5-nano',
input: 'What is the weather like in Tokyo?',
tools: [weatherTool],
});
const text = await result.getText();
console.log(text);
// "The current weather in Tokyo, Japan is partly cloudy with a temperature
// of 22°C (feels like 24°C). Humidity is at 65% with winds from the
// southwest at 15 km/h."
With Multiple Cities
const result = openrouter.callModel({
model: 'openai/gpt-5-nano',
input: 'Compare the weather in New York and Los Angeles',
tools: [weatherTool],
});
// The model will call the tool twice, once for each city
const text = await result.getText();
Extended Version with Forecast
const forecastTool = tool({
name: 'get_forecast',
description: 'Get weather forecast for the next few days',
inputSchema: z.object({
city: z.string().describe('City name'),
days: z.number().min(1).max(7).default(3).describe('Number of forecast days'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
outputSchema: z.object({
location: z.string(),
forecast: z.array(
z.object({
date: z.string(),
maxTemp: z.number(),
minTemp: z.number(),
conditions: z.string(),
chanceOfRain: z.number(),
})
),
}),
execute: async ({ city, days, units }) => {
const place = await findCity(city);
const params = new URLSearchParams({
latitude: String(place.latitude),
longitude: String(place.longitude),
daily: 'temperature_2m_max,temperature_2m_min,weather_code,precipitation_probability_max',
forecast_days: String(days),
temperature_unit: units,
// Group days by the city's own calendar instead of GMT
timezone: 'auto',
});
const response = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
const { daily } = await response.json();
return {
location: `${place.name}, ${place.country}`,
forecast: daily.time.map((date: string, index: number) => ({
date,
maxTemp: daily.temperature_2m_max[index],
minTemp: daily.temperature_2m_min[index],
conditions: WEATHER_CODES[daily.weather_code[index]] ?? 'Unknown',
chanceOfRain: daily.precipitation_probability_max[index],
})),
};
},
});
// Use both tools together
const result = openrouter.callModel({
model: 'openai/gpt-5-nano',
input: 'What is the weather in Paris today and for the next 3 days?',
tools: [weatherTool, forecastTool],
});
Error Handling
The tool includes proper error handling:const weatherToolWithRetry = tool({
name: 'get_weather',
description: 'Get current weather with retry logic',
inputSchema: z.object({
city: z.string(),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
error: z.string().optional(),
}),
execute: async ({ city, units }) => {
const maxRetries = 3;
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
if (attempt > 1) {
// Back off before each retry, whichever call failed
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt - 1)));
}
try {
const place = await findCity(city);
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}¤t=temperature_2m,weather_code&temperature_unit=${units}`
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const { current } = await response.json();
return {
temperature: current.temperature_2m,
conditions: WEATHER_CODES[current.weather_code] ?? 'Unknown',
};
} catch (error) {
lastError = error as Error;
}
}
// Return error in output rather than throwing
return {
temperature: 0,
conditions: 'Unknown',
error: `Failed after ${maxRetries} attempts: ${lastError?.message}`,
};
},
});
Testing
import { describe, it, expect, mock } from 'bun:test';
describe('weatherTool', () => {
it('returns weather data for valid city', async () => {
// Mock both Open-Meteo calls: the city lookup, then the forecast
global.fetch = mock((url: string) =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
url.includes('geocoding-api')
? {
results: [
{
name: 'London',
admin1: 'England',
country: 'United Kingdom',
latitude: 51.5,
longitude: -0.12,
},
],
}
: {
current: {
temperature_2m: 22,
apparent_temperature: 24,
relative_humidity_2m: 45,
weather_code: 0,
wind_speed_10m: 10,
wind_direction_10m: 315,
},
}
),
})
);
const result = await weatherTool.function.execute(
{ city: 'London', units: 'celsius' },
{ numberOfTurns: 1 }
);
expect(result.temperature).toBe(22);
expect(result.conditions).toBe('Clear sky');
expect(result.location.name).toBe('London');
});
it('handles city not found', async () => {
// The city lookup answers 200 with no results
global.fetch = mock(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
})
);
await expect(
weatherTool.function.execute(
{ city: 'InvalidCity123', units: 'celsius' },
{ numberOfTurns: 1 }
)
).rejects.toThrow('City not found');
});
});
See Also
- Tools Guide - Tool creation fundamentals
- API Reference - Complete type definitions