> ## Documentation Index
> Fetch the complete documentation index at: https://openrouter.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Weather Tool

> A complete weather tool demonstrating external API integration, proper validation, and error handling.

## Prerequisites

```bash title="npm" lines theme={null}
npm install @openrouter/agent zod
```

```bash title="pnpm" lines theme={null}
pnpm add @openrouter/agent zod
```

```bash title="yarn" lines theme={null}
yarn add @openrouter/agent zod
```

```bash title="bun" lines theme={null}
bun add @openrouter/agent zod
```

```bash title="deno" lines theme={null}
deno add npm:@openrouter/agent npm:zod
```

This example uses [Open-Meteo](https://open-meteo.com/), which needs no API key and is free for non-commercial use.

```bash lines theme={null}
export OPENROUTER_API_KEY=your_openrouter_key
```

## Basic Implementation

```typescript expandable lines theme={null}
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

```typescript lines theme={null}
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

```typescript lines theme={null}
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

```typescript expandable lines theme={null}
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:

```typescript expandable lines theme={null}
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}&current=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

```typescript expandable lines theme={null}
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](https://openrouter.ai/docs/agent-sdk/call-model/tools.md)** - Tool creation fundamentals
* **[API Reference](https://openrouter.ai/docs/agent-sdk/call-model/api-reference.md)** - Complete type definitions
