# Openrouter Typescript Sdk

> Complete reference for integrating with 300+ AI models through the OpenRouter TypeScript SDK and Agent packages using the callModel pattern

- Skill: `openrouterteam/openrouter-typescript-sdk` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds add openrouterteam/openrouter-typescript-sdk`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openrouterteam/openrouter-typescript-sdk/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: openrouterteam (https://skillmd.com/u/openrouterteam)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/openrouterteam/openrouter-typescript-sdk

---


# OpenRouter TypeScript SDK

A comprehensive TypeScript SDK for interacting with OpenRouter's unified API, providing access to 300+ AI models through a single, type-safe interface. This skill enables AI agents to leverage the `callModel` pattern for text generation, tool usage, streaming, and multi-turn conversations.

The SDK is split into two packages:
- **`@openrouter/agent`** — Agent features: `callModel`, `tool()`, stop conditions, streaming, format converters
- **`@openrouter/sdk`** — Platform features: model listing, chat completions, credits, OAuth, API key management

---

## Installation

```bash
# For agent features (callModel, tools, stop conditions)
npm install @openrouter/agent

# For platform features (models, credits, OAuth, API keys)
npm install @openrouter/sdk
```

## Setup

Get your API key from [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys), then initialize:

```typescript
import { OpenRouter } from '@openrouter/agent';

const client = new OpenRouter({
  apiKey: process.env.OPENROUTER_API_KEY
});
```

---

## Authentication

The SDK supports two authentication methods: API keys for server-side applications and OAuth PKCE flow for user-facing applications.

### API Key Authentication

The primary authentication method uses API keys from your OpenRouter account.

#### Obtaining an API Key

1. Visit [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys)
2. Create a new API key
3. Store securely in an environment variable

#### Environment Setup

```bash
export OPENROUTER_API_KEY=sk-or-v1-your-key-here
```

#### Client Initialization

```typescript
import { OpenRouter } from '@openrouter/agent';

const client = new OpenRouter({
  apiKey: process.env.OPENROUTER_API_KEY
});
```

The client automatically uses this key for all subsequent requests:

```typescript
// API key is automatically included
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Hello!'
});
```

#### Get Current Key Metadata

Retrieve information about the currently configured API key (requires `@openrouter/sdk`):

```typescript
import OpenRouter from '@openrouter/sdk';
const sdkClient = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });

const keyInfo = await sdkClient.apiKeys.getCurrentKeyMetadata();
console.log('Key name:', keyInfo.name);
console.log('Created:', keyInfo.createdAt);
```

#### API Key Management

Programmatically manage API keys:

```typescript
// List all keys
const keys = await client.apiKeys.list();

// Create a new key
const newKey = await client.apiKeys.create({
  name: 'Production API Key'
});

// Get a specific key by hash
const key = await client.apiKeys.get({
  hash: 'sk-or-v1-...'
});

// Update a key
await client.apiKeys.update({
  hash: 'sk-or-v1-...',
  requestBody: {
    name: 'Updated Key Name'
  }
});

// Delete a key
await client.apiKeys.delete({
  hash: 'sk-or-v1-...'
});
```

### OAuth Authentication (PKCE Flow)

For user-facing applications where users should control their own API keys, OpenRouter supports OAuth with PKCE (Proof Key for Code Exchange). This flow allows users to generate API keys through a browser authorization flow without your application handling their credentials.

#### createAuthCode

Generate an authorization code and URL to start the OAuth flow:

```typescript
const authResponse = await client.oAuth.createAuthCode({
  callbackUrl: 'https://myapp.com/auth/callback'
});

// authResponse contains:
// - authorizationUrl: URL to redirect the user to
// - code: The authorization code for later exchange

console.log('Redirect user to:', authResponse.authorizationUrl);
```

**Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `callbackUrl` | `string` | Yes | Your application's callback URL after user authorization |

**Browser Redirect:**

```typescript
// In a browser environment
window.location.href = authResponse.authorizationUrl;

// Or in a server-rendered app, return a redirect response
res.redirect(authResponse.authorizationUrl);
```

#### exchangeAuthCodeForAPIKey

After the user authorizes your application, they are redirected back to your callback URL with an authorization code. Exchange this code for an API key:

```typescript
// In your callback handler
const code = req.query.code;  // From the redirect URL

const apiKeyResponse = await client.oAuth.exchangeAuthCodeForAPIKey({
  code: code
});

// apiKeyResponse contains:
// - key: The user's API key
// - Additional metadata about the key

const userApiKey = apiKeyResponse.key;

// Store securely for this user's future requests
await saveUserApiKey(userId, userApiKey);
```

**Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `code` | `string` | Yes | The authorization code from the OAuth redirect |

#### Complete OAuth Flow Example

```typescript
import OpenRouter from '@openrouter/sdk';
import { OpenRouter as Agent } from '@openrouter/agent';
import express from 'express';

const app = express();
// SDK client for OAuth and API key management
const client = new OpenRouter({
  apiKey: process.env.OPENROUTER_API_KEY
});

// Step 1: Initiate OAuth flow
app.get('/auth/start', async (req, res) => {
  const authResponse = await client.oAuth.createAuthCode({
    callbackUrl: 'https://myapp.com/auth/callback'
  });

  // Store any state needed for the callback
  req.session.oauthState = { /* ... */ };

  // Redirect user to OpenRouter authorization page
  res.redirect(authResponse.authorizationUrl);
});

// Step 2: Handle callback and exchange code
app.get('/auth/callback', async (req, res) => {
  const { code } = req.query;

  if (!code) {
    return res.status(400).send('Authorization code missing');
  }

  try {
    const apiKeyResponse = await client.oAuth.exchangeAuthCodeForAPIKey({
      code: code as string
    });

    // Store the user's API key securely
    await saveUserApiKey(req.session.userId, apiKeyResponse.key);

    res.redirect('/dashboard?auth=success');
  } catch (error) {
    console.error('OAuth exchange failed:', error);
    res.redirect('/auth/error');
  }
});

// Step 3: Use the user's API key for their requests
app.post('/api/chat', async (req, res) => {
  const userApiKey = await getUserApiKey(req.session.userId);

  // Create an agent client with the user's key
  const userAgent = new Agent({
    apiKey: userApiKey
  });

  const result = userAgent.callModel({
    model: 'openai/gpt-5-nano',
    input: req.body.message
  });

  const text = await result.getText();
  res.json({ response: text });
});
```

### Security Best Practices

1. **Environment Variables**: Store API keys in environment variables, never in code
2. **Key Rotation**: Rotate keys periodically using the key management API
3. **Environment Separation**: Use different keys for development, staging, and production
4. **OAuth for Users**: Use the OAuth PKCE flow for user-facing apps to avoid handling user credentials
5. **Secure Storage**: Store user API keys encrypted in your database
6. **Minimal Scope**: Create keys with only the permissions needed

---

## Core Concepts: callModel

The `callModel` function is the primary interface for text generation. It provides a unified, type-safe way to interact with any supported model.

### Basic Usage

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Explain quantum computing in one sentence.',
});

const text = await result.getText();
```

### Key Benefits

- **Type-safe parameters** with full IDE autocomplete
- **Auto-generated from OpenAPI specs** - automatically updates with new models
- **Multiple consumption patterns** - text, streaming, structured data
- **Automatic tool execution** with multi-turn support

---

## Input Formats

The SDK accepts flexible input types for the `input` parameter:

### String Input
A simple string becomes a user message:

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Hello, how are you?'
});
```

### Message Arrays
For multi-turn conversations:

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: [
    { role: 'user', content: 'What is the capital of France?' },
    { role: 'assistant', content: 'The capital of France is Paris.' },
    { role: 'user', content: 'What is its population?' }
  ]
});
```

### Multimodal Content
Including images and text:

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: [
    {
      role: 'user',
      content: [
        { type: 'text', text: 'What is in this image?' },
        { type: 'image_url', image_url: { url: 'https://example.com/image.png' } }
      ]
    }
  ]
});
```

### System Instructions
Use the `instructions` parameter for system-level guidance:

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  instructions: 'You are a helpful coding assistant. Be concise.',
  input: 'How do I reverse a string in Python?'
});
```

---

## Response Methods

The result object provides multiple methods for consuming the response:

| Method | Purpose |
|--------|---------|
| `getText()` | Get complete text after all tools complete |
| `getResponse()` | Full response object with token usage |
| `getTextStream()` | Stream text deltas as they arrive |
| `getReasoningStream()` | Stream reasoning tokens (for o1/reasoning models) |
| `getToolCallsStream()` | Stream tool calls as they complete |

### getText()

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Write a haiku about coding'
});

const text = await result.getText();
console.log(text);
```

### getResponse()

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Hello!'
});

const response = await result.getResponse();
console.log('Text:', response.text);
console.log('Token usage:', response.usage);
```

### getTextStream()

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Write a short story'
});

for await (const delta of result.getTextStream()) {
  process.stdout.write(delta);
}
```

---

## Tool System

Create strongly-typed tools using Zod schemas for automatic validation and type inference.

### Defining Tools

```typescript
import { tool } from '@openrouter/agent/tool';
import { z } from 'zod';

const weatherTool = tool({
  name: 'get_weather',
  description: 'Get current weather for a location',
  inputSchema: z.object({
    location: z.string().describe('City name'),
    units: z.enum(['celsius', 'fahrenheit']).optional().default('celsius')
  }),
  outputSchema: z.object({
    temperature: z.number(),
    conditions: z.string(),
    humidity: z.number()
  }),
  execute: async (params) => {
    // Implement weather fetching logic
    return {
      temperature: 22,
      conditions: 'Sunny',
      humidity: 45
    };
  }
});
```

### Using Tools with callModel

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'What is the weather in Paris?',
  tools: [weatherTool]
});

const text = await result.getText();
// The SDK automatically executes the tool and continues the conversation
```

### Tool Types

#### Regular Tools
Standard execute functions that return a result:

```typescript
const calculatorTool = tool({
  name: 'calculate',
  description: 'Perform mathematical calculations',
  inputSchema: z.object({
    expression: z.string()
  }),
  execute: async ({ expression }) => {
    return { result: eval(expression) };
  }
});
```

#### Generator Tools
Yield progress events using `eventSchema`:

```typescript
const searchTool = tool({
  name: 'web_search',
  description: 'Search the web',
  inputSchema: z.object({ query: z.string() }),
  eventSchema: z.object({
    type: z.literal('progress'),
    message: z.string()
  }),
  outputSchema: z.object({ results: z.array(z.string()) }),
  execute: async function* ({ query }) {
    yield { type: 'progress', message: 'Searching...' };
    yield { type: 'progress', message: 'Processing results...' };
    return { results: ['Result 1', 'Result 2'] };
  }
});
```

#### Manual Tools
Set `execute: false` to handle tool calls yourself:

```typescript
const manualTool = tool({
  name: 'user_confirmation',
  description: 'Request user confirmation',
  inputSchema: z.object({ message: z.string() }),
  execute: false
});
```

---

## Multi-Turn Conversations with Stop Conditions

Control automatic tool execution with stop conditions:

```typescript
import { stepCountIs, maxCost, hasToolCall } from '@openrouter/agent/stop-conditions';

const result = client.callModel({
  model: 'openai/gpt-5.2',
  input: 'Research this topic thoroughly',
  tools: [searchTool, analyzeTool],
  stopWhen: [
    stepCountIs(10),      // Stop after 10 turns
    maxCost(1.00),        // Stop if cost exceeds $1.00
    hasToolCall('finish') // Stop when 'finish' tool is called
  ]
});
```

### Available Stop Conditions

| Condition | Description |
|-----------|-------------|
| `stepCountIs(n)` | Stop after n turns |
| `maxCost(amount)` | Stop when cost exceeds amount |
| `hasToolCall(name)` | Stop when specific tool is called |

### Custom Stop Conditions

```typescript
const customStop = (context) => {
  return context.messages.length > 20;
};

const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Complex task',
  tools: [myTool],
  stopWhen: customStop
});
```

---

## Dynamic Parameters

Compute parameters based on conversation context:

```typescript
const result = client.callModel({
  model: (ctx) => ctx.numberOfTurns > 3 ? 'openai/gpt-4' : 'openai/gpt-4o-mini',
  temperature: (ctx) => ctx.numberOfTurns > 1 ? 0.3 : 0.7,
  input: 'Hello!'
});
```

### Context Object Properties

| Property | Type | Description |
|----------|------|-------------|
| `numberOfTurns` | number | Current turn count |
| `messages` | array | All messages so far |
| `instructions` | string | Current system instructions |
| `totalCost` | number | Accumulated cost |

---

## nextTurnParams: Context Injection

Tools can modify parameters for subsequent turns, enabling skills and context-aware behavior:

```typescript
const skillTool = tool({
  name: 'load_skill',
  description: 'Load a specialized skill',
  inputSchema: z.object({
    skill: z.string().describe('Name of the skill to load')
  }),
  nextTurnParams: {
    instructions: (params, context) => {
      const skillInstructions = loadSkillInstructions(params.skill);
      return `${context.instructions}\n\n${skillInstructions}`;
    }
  },
  execute: async ({ skill }) => {
    return { loaded: skill };
  }
});
```

### Use Cases for nextTurnParams

- **Skill Systems**: Dynamically load specialized capabilities
- **Context Accumulation**: Build up context over multiple turns
- **Mode Switching**: Change model behavior mid-conversation
- **Memory Injection**: Add retrieved context to instructions

---

## Generation Parameters

Control model behavior with these parameters:

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Write a creative story',
  temperature: 0.7,        // Creativity (0-2, default varies by model)
  maxOutputTokens: 1000,   // Maximum tokens to generate
  topP: 0.9,               // Nucleus sampling parameter
  frequencyPenalty: 0.5,   // Reduce repetition
  presencePenalty: 0.5,    // Encourage new topics
  stop: ['\n\n']           // Stop sequences
});
```

---

## Streaming

All streaming methods support concurrent consumers from a single result object:

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Write a detailed explanation'
});

// Consumer 1: Stream text to console
const textPromise = (async () => {
  for await (const delta of result.getTextStream()) {
    process.stdout.write(delta);
  }
})();

// Consumer 2: Get full response simultaneously
const responsePromise = result.getResponse();

// Both run concurrently
const [, response] = await Promise.all([textPromise, responsePromise]);
console.log('\n\nTotal tokens:', response.usage.totalTokens);
```

### Streaming Tool Calls

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Search for information about TypeScript',
  tools: [searchTool]
});

for await (const toolCall of result.getToolCallsStream()) {
  console.log(`Tool called: ${toolCall.name}`);
  console.log(`Arguments: ${JSON.stringify(toolCall.arguments)}`);
  console.log(`Result: ${JSON.stringify(toolCall.result)}`);
}
```

---

## Format Conversion

Convert between ecosystem formats for interoperability:

### OpenAI Format

```typescript
import { fromChatMessages, toChatMessage } from '@openrouter/agent';

// OpenAI messages → OpenRouter format
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: fromChatMessages(openaiMessages)
});

// Response → OpenAI chat message format
const response = await result.getResponse();
const chatMsg = toChatMessage(response);
```

### Claude Format

```typescript
import { fromClaudeMessages, toClaudeMessage } from '@openrouter/agent';

// Claude messages → OpenRouter format
const result = client.callModel({
  model: 'anthropic/claude-3-opus',
  input: fromClaudeMessages(claudeMessages)
});

// Response → Claude message format
const response = await result.getResponse();
const claudeMsg = toClaudeMessage(response);
```

---

## Responses API Message Shapes

The SDK uses the **OpenResponses** format for messages. Understanding these shapes is essential for building robust agents.

### Message Roles

Messages contain a `role` property that determines the message type:

| Role | Description |
|------|-------------|
| `user` | User-provided input |
| `assistant` | Model-generated responses |
| `system` | System instructions |
| `developer` | Developer-level directives |
| `tool` | Tool execution results |

### Text Message

Simple text content from user or assistant:

```typescript
interface TextMessage {
  role: 'user' | 'assistant';
  content: string;
}
```

### Multimodal Message (Array Content)

Messages with mixed content types:

```typescript
interface MultimodalMessage {
  role: 'user';
  content: Array<
    | { type: 'input_text'; text: string }
    | { type: 'input_image'; imageUrl: string; detail?: 'auto' | 'low' | 'high' }
    | {
        type: 'image';
        source: {
          type: 'url' | 'base64';
          url?: string;
          media_type?: string;
          data?: string
        }
      }
  >;
}
```

### Tool Function Call Message

When the model requests a tool execution:

```typescript
interface ToolCallMessage {
  role: 'assistant';
  content?: null;
  tool_calls?: Array<{
    id: string;
    type: 'function';
    function: {
      name: string;
      arguments: string;  // JSON-encoded arguments
    };
  }>;
}
```

### Tool Result Message

Result returned after tool execution:

```typescript
interface ToolResultMessage {
  role: 'tool';
  tool_call_id: string;
  content: string;  // JSON-encoded result
}
```

### Non-Streaming Response Structure

The complete response object from `getResponse()`:

```typescript
interface OpenResponsesNonStreamingResponse {
  output: Array<ResponseMessage>;
  usage?: {
    inputTokens: number;
    outputTokens: number;
    cachedTokens?: number;
  };
  finishReason?: string;
  warnings?: Array<{
    type: string;
    message: string
  }>;
  experimental_providerMetadata?: Record<string, unknown>;
}
```

### Response Message Types

Output messages in the response array:

```typescript
// Text/content message
interface ResponseOutputMessage {
  type: 'message';
  role: 'assistant';
  content: string | Array<ContentPart>;
  reasoning?: string;  // For reasoning models (o1, etc.)
}

// Tool result in output
interface FunctionCallOutputMessage {
  type: 'function_call_output';
  call_id: string;
  output: string;
}
```

### Parsed Tool Call

When tool calls are parsed from the response:

```typescript
interface ParsedToolCall {
  id: string;
  name: string;
  arguments: unknown;  // Validated against inputSchema
}
```

### Tool Execution Result

After a tool completes execution:

```typescript
interface ToolExecutionResult {
  toolCallId: string;
  toolName: string;
  result: unknown;                  // Validated against outputSchema
  preliminaryResults?: unknown[];   // From generator tools
  error?: Error;
}
```

### Step Result (for Stop Conditions)

Available in custom stop condition callbacks:

```typescript
interface StepResult {
  stepType: 'initial' | 'continue';
  text: string;
  toolCalls: ParsedToolCall[];
  toolResults: ToolExecutionResult[];
  response: OpenResponsesNonStreamingResponse;
  usage?: {
    inputTokens: number;
    outputTokens: number;
    cachedTokens?: number;
  };
  finishReason?: string;
  warnings?: Array<{ type: string; message: string }>;
  experimental_providerMetadata?: Record<string, unknown>;
}
```

### TurnContext

Available to tools and dynamic parameter functions:

```typescript
interface TurnContext {
  numberOfTurns: number;                     // Turn count (1-indexed)
  turnRequest?: OpenResponsesRequest;        // Current request being made
  toolCall?: OpenResponsesFunctionToolCall;  // Current tool call (in tool context)
}
```

---

## Event Shapes

The SDK provides multiple streaming methods that yield different event types.

### Response Stream Events

The `getFullResponsesStream()` method yields these event types:

```typescript
type EnhancedResponseStreamEvent =
  | ResponseCreatedEvent
  | ResponseInProgressEvent
  | OutputTextDeltaEvent
  | OutputTextDoneEvent
  | ReasoningDeltaEvent
  | ReasoningDoneEvent
  | FunctionCallArgumentsDeltaEvent
  | FunctionCallArgumentsDoneEvent
  | ResponseCompletedEvent
  | ToolPreliminaryResultEvent;
```

### Event Type Reference

| Event Type | Description | Payload |
|------------|-------------|---------|
| `response.created` | Response object initialized | `{ response: ResponseObject }` |
| `response.in_progress` | Generation has started | `{}` |
| `response.output_text.delta` | Text chunk received | `{ delta: string }` |
| `response.output_text.done` | Text generation complete | `{ text: string }` |
| `response.reasoning.delta` | Reasoning chunk (o1 models) | `{ delta: string }` |
| `response.reasoning.done` | Reasoning complete | `{ reasoning: string }` |
| `response.function_call_arguments.delta` | Tool argument chunk | `{ delta: string }` |
| `response.function_call_arguments.done` | Tool arguments complete | `{ arguments: string }` |
| `response.completed` | Full response complete | `{ response: ResponseObject }` |
| `tool.preliminary_result` | Generator tool progress | `{ toolCallId: string; result: unknown }` |

### Text Delta Event

```typescript
interface OutputTextDeltaEvent {
  type: 'response.output_text.delta';
  delta: string;
}
```

### Reasoning Delta Event

For reasoning models (o1, etc.):

```typescript
interface ReasoningDeltaEvent {
  type: 'response.reasoning.delta';
  delta: string;
}
```

### Function Call Arguments Delta Event

```typescript
interface FunctionCallArgumentsDeltaEvent {
  type: 'response.function_call_arguments.delta';
  delta: string;
}
```

### Tool Preliminary Result Event

From generator tools that yield progress:

```typescript
interface ToolPreliminaryResultEvent {
  type: 'tool.preliminary_result';
  toolCallId: string;
  result: unknown;  // Matches the tool's eventSchema
}
```

### Response Completed Event

```typescript
interface ResponseCompletedEvent {
  type: 'response.completed';
  response: OpenResponsesNonStreamingResponse;
}
```

### Tool Stream Events

The `getToolStream()` method yields:

```typescript
type ToolStreamEvent =
  | { type: 'delta'; content: string }
  | { type: 'preliminary_result'; toolCallId: string; result: unknown };
```

### Example: Processing Stream Events

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Analyze this data',
  tools: [analysisTool]
});

for await (const event of result.getFullResponsesStream()) {
  switch (event.type) {
    case 'response.output_text.delta':
      process.stdout.write(event.delta);
      break;

    case 'response.reasoning.delta':
      console.log('[Reasoning]', event.delta);
      break;

    case 'response.function_call_arguments.delta':
      console.log('[Tool Args]', event.delta);
      break;

    case 'tool.preliminary_result':
      console.log(`[Progress: ${event.toolCallId}]`, event.result);
      break;

    case 'response.completed':
      console.log('\n[Complete]', event.response.usage);
      break;
  }
}
```

### Message Stream Events

The `getNewMessagesStream()` yields OpenResponses format updates:

```typescript
type MessageStreamUpdate =
  | ResponsesOutputMessage        // Text/content updates
  | OpenResponsesFunctionCallOutput;  // Tool results
```

### Example: Tracking New Messages

```typescript
const result = client.callModel({
  model: 'openai/gpt-5-nano',
  input: 'Research this topic',
  tools: [searchTool]
});

const allMessages: MessageStreamUpdate[] = [];

for await (const message of result.getNewMessagesStream()) {
  allMessages.push(message);

  if (message.type === 'message') {
    console.log('Assistant:', message.content);
  } else if (message.type === 'function_call_output') {
    console.log('Tool result:', message.output);
  }
}
```

---

## API Reference

### Client Methods

Beyond `callModel`, the SDK client (`@openrouter/sdk`) provides access to platform API endpoints:

```typescript
import OpenRouter from '@openrouter/sdk';

const sdkClient = new OpenRouter({
  apiKey: process.env.OPENROUTER_API_KEY
});

// List available models
const models = await sdkClient.models.list();

// Chat completions (alternative to callModel)
const completion = await sdkClient.chat.send({
  model: 'openai/gpt-5-nano',
  messages: [{ role: 'user', content: 'Hello!' }]
});

// Legacy completions format
const legacyCompletion = await sdkClient.completions.generate({
  model: 'openai/gpt-5-nano',
  prompt: 'Once upon a time'
});

// Usage analytics
const activity = await sdkClient.analytics.getUserActivity();

// Credit balance
const credits = await sdkClient.credits.getCredits();

// API key management
const keys = await sdkClient.apiKeys.list();
```

---

## Error Handling

The SDK provides specific error types with actionable messages:

```typescript
try {
  const result = await client.callModel({
    model: 'openai/gpt-5-nano',
    input: 'Hello!'
  });
  const text = await result.getText();
} catch (error) {
  if (error.statusCode === 401) {
    console.error('Invalid API key - check your OPENROUTER_API_KEY');
  } else if (error.statusCode === 402) {
    console.error('Insufficient credits - add credits at openrouter.ai');
  } else if (error.statusCode === 429) {
    console.error('Rate limited - implement backoff retry');
  } else if (error.statusCode === 503) {
    console.error('Model temporarily unavailable - try again or use fallback');
  } else {
    console.error('Unexpected error:', error.message);
  }
}
```

### Error Status Codes

| Code | Meaning | Action |
|------|---------|--------|
| 400 | Bad request | Check request parameters |
| 401 | Unauthorized | Verify API key |
| 402 | Payment required | Add credits |
| 429 | Rate limited | Implement exponential backoff |
| 500 | Server error | Retry with backoff |
| 503 | Service unavailable | Try alternative model |

---

## Complete Example: Agent with Tools

```typescript
import { OpenRouter } from '@openrouter/agent';
import { tool } from '@openrouter/agent/tool';
import { stepCountIs, hasToolCall } from '@openrouter/agent/stop-conditions';
import { z } from 'zod';

const client = new OpenRouter({
  apiKey: process.env.OPENROUTER_API_KEY
});

// Define tools
const searchTool = tool({
  name: 'web_search',
  description: 'Search the web for information',
  inputSchema: z.object({
    query: z.string().describe('Search query')
  }),
  outputSchema: z.object({
    results: z.array(z.object({
      title: z.string(),
      snippet: z.string(),
      url: z.string()
    }))
  }),
  execute: async ({ query }) => {
    // Implement actual search
    return {
      results: [
        { title: 'Example', snippet: 'Example result', url: 'https://example.com' }
      ]
    };
  }
});

const finishTool = tool({
  name: 'finish',
  description: 'Complete the task with final answer',
  inputSchema: z.object({
    answer: z.string().describe('The final answer')
  }),
  execute: async ({ answer }) => ({ answer })
});

// Run agent
async function runAgent(task: string) {
  const result = client.callModel({
    model: 'openai/gpt-5-nano',
    instructions: 'You are a helpful research assistant. Use web_search to find information, then use finish to provide your final answer.',
    input: task,
    tools: [searchTool, finishTool],
    stopWhen: [
      stepCountIs(10),
      hasToolCall('finish')
    ]
  });

  // Stream progress
  for await (const toolCall of result.getToolCallsStream()) {
    console.log(`[${toolCall.name}] ${JSON.stringify(toolCall.arguments)}`);
  }

  return await result.getText();
}

// Usage
const answer = await runAgent('What are the latest developments in quantum computing?');
console.log('Final answer:', answer);
```

---

## Best Practices

### 1. Prefer callModel Over Direct API Calls
The `callModel` pattern provides automatic tool execution, type safety, and multi-turn handling.

### 2. Use Zod for Tool Schemas
Zod provides runtime validation and excellent TypeScript inference:

```typescript
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  age: z.number().int().positive()
});
```

### 3. Implement Stop Conditions
Always set reasonable limits to prevent runaway costs:

```typescript
stopWhen: [stepCountIs(20), maxCost(5.00)]
```

### 4. Handle Errors Gracefully
Implement retry logic for transient failures:

```typescript
async function callWithRetry(params, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await client.callModel(params).getText();
    } catch (error) {
      if (error.statusCode === 429 || error.statusCode >= 500) {
        await sleep(Math.pow(2, i) * 1000);
        continue;
      }
      throw error;
    }
  }
}
```

### 5. Use Streaming for Long Responses
Streaming provides better UX and allows early termination:

```typescript
for await (const delta of result.getTextStream()) {
  // Process incrementally
}
```

---

## Additional Resources

- **API Keys**: [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys)
- **Model List**: [openrouter.ai/models](https://openrouter.ai/models)
- **Agent SDK**: [github.com/OpenRouterTeam/typescript-agent](https://github.com/OpenRouterTeam/typescript-agent)
- **SDK Issues**: [github.com/OpenRouterTeam/typescript-sdk/issues](https://github.com/OpenRouterTeam/typescript-sdk/issues)

---

*SDK Status: Beta - Report issues on GitHub*

