# Tools

> Create strongly-typed tools for LLMs with Zod schemas. Learn about regular tools, generator tools with progress, manual tools, and automatic multi-turn execution.

## The tool() Helper

The `tool()` function creates type-safe tools with Zod schema validation:

```typescript
import { OpenRouter, tool } from '@openrouter/sdk';
import { z } from 'zod';

const weatherTool = tool({
  name: 'get_weather',
  description: 'Get the current weather for a location',
  inputSchema: z.object({
    location: z.string().describe('City name, e.g., "San Francisco, CA"'),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    conditions: z.string(),
  }),
  execute: async (params) => {
    // params is typed as { location: string }
    const weather = await fetchWeather(params.location);
    return {
      temperature: weather.temp,
      conditions: weather.description,
    };
  },
});
```

## Tool Types

The SDK supports three types of tools, automatically detected from your configuration:

### Regular Tools

Standard tools with an execute function:

```typescript
const calculatorTool = tool({
  name: 'calculate',
  description: 'Perform a mathematical calculation',
  inputSchema: z.object({
    expression: z.string().describe('Math expression like "2 + 2"'),
  }),
  outputSchema: z.object({
    result: z.number(),
  }),
  execute: async (params) => {
    const result = eval(params.expression); // Use a safer eval in production
    return { result };
  },
});
```

### Generator Tools

Tools that yield progress updates during execution. Add `eventSchema` to enable generator mode:

```typescript
const searchTool = tool({
  name: 'search_database',
  description: 'Search documents with progress updates',
  inputSchema: z.object({
    query: z.string(),
    limit: z.number().default(10),
  }),
  // eventSchema triggers generator mode
  eventSchema: z.object({
    progress: z.number().min(0).max(100),
    message: z.string(),
  }),
  outputSchema: z.object({
    results: z.array(z.string()),
    totalFound: z.number(),
  }),
  // execute is now an async generator
  execute: async function* (params) {
    yield { progress: 0, message: 'Starting search...' };

    const results = [];
    for (let i = 0; i < 5; i++) {
      yield { progress: (i + 1) * 20, message: `Searching batch ${i + 1}...` };
      results.push(...await searchBatch(params.query, i));
    }

    // Final yield is the output
    yield { progress: 100, message: 'Complete!' };

    // Return the final result (or yield it as last value)
    return {
      results: results.slice(0, params.limit),
      totalFound: results.length,
    };
  },
});
```

Progress events are streamed to consumers via `getToolStream()` and `getFullResponsesStream()`.

### Manual Tools

Tools without automatic execution - you handle the tool calls yourself:

```typescript
const manualTool = tool({
  name: 'send_email',
  description: 'Send an email (requires user confirmation)',
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string(),
  }),
  execute: false, // Manual handling required
});
```

Use `getToolCalls()` to retrieve manual tool calls for processing.

## Schema Definition

### Input Schema

Define what parameters the tool accepts:

```typescript
const inputSchema = z.object({
  // Required parameters
  query: z.string().describe('Search query'),

  // Optional with default
  limit: z.number().default(10).describe('Max results'),

  // Optional without default
  filter: z.string().optional().describe('Filter expression'),

  // Enum values
  sortBy: z.enum(['relevance', 'date', 'popularity']).default('relevance'),

  // Nested objects
  options: z.object({
    caseSensitive: z.boolean().default(false),
    wholeWord: z.boolean().default(false),
  }).optional(),

  // Arrays
  tags: z.array(z.string()).optional(),
});
```

### Output Schema

Define the structure of results returned to the model:

```typescript
const outputSchema = z.object({
  results: z.array(z.object({
    id: z.string(),
    title: z.string(),
    score: z.number(),
  })),
  metadata: z.object({
    totalCount: z.number(),
    searchTimeMs: z.number(),
  }),
});
```

### Event Schema (Generator Tools)

Define progress/status events for generator tools:

```typescript
const eventSchema = z.object({
  stage: z.enum(['initializing', 'processing', 'finalizing']),
  progress: z.number(),
  currentItem: z.string().optional(),
});
```

## Type Inference

The SDK provides utilities to extract types from tools:

```typescript
import type { InferToolInput, InferToolOutput, InferToolEvent } from '@openrouter/sdk';

// Get the input type
type WeatherInput = InferToolInput<typeof weatherTool>;
// { location: string }

// Get the output type
type WeatherOutput = InferToolOutput<typeof weatherTool>;
// { temperature: number; conditions: string }

// Get event type (generator tools only)
type SearchEvent = InferToolEvent<typeof searchTool>;
// { progress: number; message: string }
```

## Using Tools with callModel

### Single Tool

```typescript
const openrouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });

const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'What is the weather in Tokyo?',
  tools: [weatherTool],
});

// Tools are automatically executed
const text = await result.getText();
// "The weather in Tokyo is 22°C and sunny."
```

### Multiple Tools

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Search for TypeScript tutorials and calculate 2+2',
  tools: [searchTool, calculatorTool],
});
```

### Type-Safe Tool Calls with `as const`

Use `as const` for full type inference on tool calls:

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'What is the weather?',
  tools: [weatherTool, searchTool] as const,
  maxToolRounds: 0, // Get tool calls without executing
});

// Tool calls are typed as union of tool inputs
for await (const toolCall of result.getToolCallsStream()) {
  if (toolCall.name === 'get_weather') {
    // toolCall.arguments is typed as { location: string }
    console.log('Weather for:', toolCall.arguments.location);
  }
}
```

## TurnContext

Tool execute functions receive a `TurnContext` with conversation state:

```typescript
const contextAwareTool = tool({
  name: 'context_tool',
  inputSchema: z.object({ data: z.string() }),
  outputSchema: z.object({ result: z.string() }),
  execute: async (params, context) => {
    console.log('Turn number:', context?.numberOfTurns);
    console.log('Message history:', context?.turnRequest?.input);
    console.log('Model:', context?.turnRequest?.model);

    return { result: `Processed on turn ${context?.numberOfTurns}` };
  },
});
```

### TurnContext Properties

| Property        | Type                                         | Description                                                   |
| --------------- | -------------------------------------------- | ------------------------------------------------------------- |
| `numberOfTurns` | `number`                                     | Current turn number (1-indexed)                               |
| `turnRequest`   | `OpenResponsesRequest \| undefined`          | Current request object containing messages and model settings |
| `toolCall`      | `OpenResponsesFunctionToolCall \| undefined` | The specific tool call being executed                         |

## Tool Execution

callModel automatically executes tools and handles multi-turn conversations. When the model calls a tool, the SDK executes it, sends the result back, and continues until the model provides a final response.

### Automatic Execution Flow

When you provide tools with execute functions:

```typescript
import { OpenRouter, tool } from '@openrouter/sdk';
import { z } from 'zod';

const weatherTool = tool({
  name: 'get_weather',
  inputSchema: z.object({ location: z.string() }),
  outputSchema: z.object({ temperature: z.number() }),
  execute: async ({ location }) => {
    return { temperature: await fetchTemperature(location) };
  },
});

const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'What is the weather in Paris?',
  tools: [weatherTool],
});

// getText() waits for all tool execution to complete
const text = await result.getText();
// "The weather in Paris is 18°C."
```

### Execution Sequence

1. Model receives prompt and generates tool call
2. SDK extracts tool call and validates arguments
3. Tool's execute function runs
4. Result is formatted and sent back to model
5. Model generates final response (or more tool calls)
6. Process repeats until model is done

### Controlling Execution Rounds

#### maxToolRounds (Number)

Limit the maximum number of tool execution rounds:

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Research this topic thoroughly',
  tools: [searchTool, analyzeTool],
  maxToolRounds: 3, // Stop after 3 rounds of tool execution
});
```

Setting `maxToolRounds: 0` disables automatic execution - you get raw tool calls.

#### maxToolRounds (Function)

Use a function for dynamic control:

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Research and analyze',
  tools: [searchTool],
  maxToolRounds: (context) => {
    // Continue if under 5 turns
    return context.numberOfTurns < 5;
  },
});
```

The function receives `TurnContext` and returns `true` to continue or `false` to stop.

### Accessing Tool Calls

#### getToolCalls()

Get all tool calls from the initial response (before auto-execution):

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'What is the weather in Tokyo and Paris?',
  tools: [weatherTool],
  maxToolRounds: 0, // Don't auto-execute
});

const toolCalls = await result.getToolCalls();

for (const call of toolCalls) {
  console.log(`Tool: ${call.name}`);
  console.log(`ID: ${call.id}`);
  console.log(`Arguments:`, call.arguments);
}
```

#### getToolCallsStream()

Stream tool calls as they complete:

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Check weather in multiple cities',
  tools: [weatherTool],
  maxToolRounds: 0,
});

for await (const toolCall of result.getToolCallsStream()) {
  console.log(`Received tool call: ${toolCall.name}`);

  // Process each tool call as it arrives
  const weatherResult = await processWeatherRequest(toolCall.arguments);
  console.log('Result:', weatherResult);
}
```

### Tool Stream Events

#### getToolStream()

Stream both argument deltas and preliminary results:

```typescript
const searchTool = tool({
  name: 'search',
  inputSchema: z.object({ query: z.string() }),
  eventSchema: z.object({ progress: z.number(), status: z.string() }),
  outputSchema: z.object({ results: z.array(z.string()) }),
  execute: async function* ({ query }) {
    yield { progress: 25, status: 'Searching...' };
    yield { progress: 50, status: 'Processing...' };
    yield { progress: 75, status: 'Ranking...' };
    yield { progress: 100, status: 'Complete' };
    return { results: ['result1', 'result2'] };
  },
});

const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Search for TypeScript tutorials',
  tools: [searchTool],
});

for await (const event of result.getToolStream()) {
  switch (event.type) {
    case 'delta':
      // Raw argument delta from the model
      process.stdout.write(event.content);
      break;
    case 'preliminary_result':
      // Progress from generator tool
      console.log(`Progress: ${event.result.progress}% - ${event.result.status}`);
      break;
  }
}
```

#### Event Types

| Type                 | Description                                                |
| -------------------- | ---------------------------------------------------------- |
| `delta`              | Raw tool call argument chunks from model                   |
| `preliminary_result` | Progress events from generator tools (intermediate yields) |

### Tool Result Events

When using `getFullResponsesStream()`, you can also receive `tool.result` events that fire when a tool execution completes:

```typescript
for await (const event of result.getFullResponsesStream()) {
  switch (event.type) {
    case 'tool.preliminary_result':
      // Intermediate progress from generator tools
      console.log(`Progress (${event.toolCallId}):`, event.result);
      break;
    case 'tool.result':
      // Final result when tool execution completes
      console.log(`Tool ${event.toolCallId} completed`);
      console.log('Result:', event.result);
      // Access any preliminary results that were emitted during execution
      if (event.preliminaryResults) {
        console.log('All progress events:', event.preliminaryResults);
      }
      break;
  }
}
```

#### ToolResultEvent Type

```typescript
type ToolResultEvent<TResult = unknown, TPreliminaryResults = unknown> = {
  type: 'tool.result';
  toolCallId: string;
  result: TResult;
  timestamp: number;
  preliminaryResults?: TPreliminaryResults[];
};
```

The `tool.result` event provides the final output from tool execution along with all intermediate `preliminaryResults` that were yielded during execution (for generator tools). This is useful when you need both real-time progress updates and a summary of all progress at completion.

### Parallel Tool Execution

When the model calls multiple tools, they execute in parallel:

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Get weather in Paris, Tokyo, and New York simultaneously',
  tools: [weatherTool],
});

// All three weather calls execute in parallel
const text = await result.getText();
```

### Manual Tool Handling

For tools without execute functions:

```typescript
const confirmTool = tool({
  name: 'send_email',
  description: 'Send an email (requires confirmation)',
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string(),
  }),
  execute: false, // Manual handling
});

const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Send an email to alice@example.com',
  tools: [confirmTool],
  maxToolRounds: 0,
});

const toolCalls = await result.getToolCalls();

for (const call of toolCalls) {
  if (call.name === 'send_email') {
    // Show confirmation UI
    const confirmed = await showConfirmDialog(call.arguments);

    if (confirmed) {
      await sendEmail(call.arguments);
    }
  }
}
```

### Execution Results

Access execution metadata through getResponse():

```typescript
const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'What is 2+2 and the weather in Paris?',
  tools: [calculatorTool, weatherTool],
});

const response = await result.getResponse();

// Response includes all execution rounds
console.log('Final output:', response.output);
console.log('Usage:', response.usage);
```

## Error Handling

### Tool Execution Errors

Errors in execute functions are caught and sent back to the model:

```typescript
const riskyTool = tool({
  name: 'risky_operation',
  inputSchema: z.object({ input: z.string() }),
  outputSchema: z.object({ result: z.string() }),
  execute: async (params) => {
    if (params.input === 'fail') {
      throw new Error('Operation failed: invalid input');
    }
    return { result: 'success' };
  },
});

const result = openrouter.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Try the risky operation with "fail"',
  tools: [riskyTool],
});

// Model receives error message and can respond appropriately
const text = await result.getText();
// "I tried the operation but it failed with: Operation failed: invalid input"
```

### Validation Errors

Invalid tool arguments are caught before execution:

```typescript
const strictTool = tool({
  name: 'strict',
  inputSchema: z.object({
    email: z.string().email(),
    age: z.number().min(0).max(150),
  }),
  execute: async (params) => {
    // Only runs with valid input
    return { valid: true };
  },
});
```

### Graceful Error Handling

Handle errors gracefully in execute functions:

```typescript
const robustTool = tool({
  name: 'fetch_data',
  inputSchema: z.object({ url: z.string().url() }),
  outputSchema: z.object({
    data: z.unknown().optional(),
    error: z.string().optional(),
  }),
  execute: async (params) => {
    try {
      const response = await fetch(params.url);
      if (!response.ok) {
        return { error: `HTTP ${response.status}: ${response.statusText}` };
      }
      return { data: await response.json() };
    } catch (error) {
      return { error: `Failed to fetch: ${error.message}` };
    }
  },
});
```

## Best Practices

### Descriptive Names and Descriptions

```typescript
// Good: Clear name and description
const tool1 = tool({
  name: 'search_knowledge_base',
  description: 'Search the company knowledge base for documents, FAQs, and policies. Returns relevant articles with snippets.',
  // ...
});

// Avoid: Vague or generic
const tool2 = tool({
  name: 'search',
  description: 'Searches stuff',
  // ...
});
```

### Schema Descriptions

Add `.describe()` to help the model understand parameters:

```typescript
const inputSchema = z.object({
  query: z.string().describe('Natural language search query'),
  maxResults: z.number()
    .min(1)
    .max(100)
    .default(10)
    .describe('Maximum number of results to return (1-100)'),
  dateRange: z.enum(['day', 'week', 'month', 'year', 'all'])
    .default('all')
    .describe('Filter results by time period'),
});
```

### Idempotent Tools

Design tools to be safely re-executable:

```typescript
const createUserTool = tool({
  name: 'create_user',
  inputSchema: z.object({
    email: z.string().email(),
    name: z.string(),
  }),
  execute: async (params) => {
    // Check if user exists first
    const existing = await findUserByEmail(params.email);
    if (existing) {
      return { userId: existing.id, created: false };
    }

    const user = await createUser(params);
    return { userId: user.id, created: true };
  },
});
```

### Timeout Handling

Wrap long-running operations:

```typescript
const longRunningTool = tool({
  name: 'process_data',
  inputSchema: z.object({ dataId: z.string() }),
  execute: async (params) => {
    const timeoutMs = 30000;

    const result = await Promise.race([
      processData(params.dataId),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
      ),
    ]);

    return result;
  },
});
```

## Next Steps

* **[nextTurnParams](/docs/sdks/typescript/call-model/next-turn-params)** - Tool-driven context injection
* **[Stop Conditions](/docs/sdks/typescript/call-model/stop-conditions)** - Advanced execution control
* **[Examples](/docs/sdks/typescript/call-model/examples/weather-tool)** - Complete tool implementations