home / skills / openrouterteam / agent-skills / typescript-sdk
This skill helps AI agents integrate with OpenRouter TypeScript SDK to callModel with type safety and multi-turn support.
npx playbooks add skill openrouterteam/agent-skills --skill typescript-sdkReview the files below or copy the command above to add this skill to your agents.
---
name: openrouter-typescript-sdk
description: Complete reference for integrating with 300+ AI models through the OpenRouter TypeScript SDK using the callModel pattern
version: 1.0.0
---
# 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.
---
## Installation
```bash
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/sdk';
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/sdk';
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:
```typescript
const keyInfo = await client.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 express from 'express';
const app = express();
const client = new OpenRouter({
apiKey: process.env.OPENROUTER_API_KEY // Your app's key for OAuth operations
});
// 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 a client with the user's key
const userClient = new OpenRouter({
apiKey: userApiKey
});
const result = userClient.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/sdk';
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/sdk';
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/sdk';
// 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/sdk';
// 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 client provides access to other API endpoints:
```typescript
const client = new OpenRouter({
apiKey: process.env.OPENROUTER_API_KEY
});
// List available models
const models = await client.models.list();
// Chat completions (alternative to callModel)
const completion = await client.chat.send({
model: 'openai/gpt-5-nano',
messages: [{ role: 'user', content: 'Hello!' }]
});
// Legacy completions format
const legacyCompletion = await client.completions.generate({
model: 'openai/gpt-5-nano',
prompt: 'Once upon a time'
});
// Usage analytics
const activity = await client.analytics.getUserActivity();
// Credit balance
const credits = await client.credits.getCredits();
// API key management
const keys = await client.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, { tool, stepCountIs } from '@openrouter/sdk';
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)
- **GitHub Issues**: [github.com/OpenRouterTeam/typescript-sdk/issues](https://github.com/OpenRouterTeam/typescript-sdk/issues)
---
*SDK Status: Beta - Report issues on GitHub*
This skill provides a complete reference for integrating with 300+ AI models using the OpenRouter TypeScript SDK and the callModel pattern. It documents authentication (API key and OAuth PKCE), input formats, streaming and synchronous response methods, tool creation, multi-turn flows, and security suggestions. The content is focused on practical code examples and patterns for production usage.
The SDK exposes a single, type-safe callModel function that accepts flexible inputs (strings, message arrays, multimodal content) and returns a result object with multiple consumption methods (getText, getResponse, getTextStream, etc.). It handles API key and OAuth PKCE authentication, automatic tool execution, streaming, and multi-turn conversations, while allowing dynamic parameters and stop conditions. Tools are defined with strong typing (Zod) and can be regular, generator, or manual, including hooks to modify next-turn parameters.
Which authentication method should I pick for a server app?
Use API keys stored in environment variables. They are simpler for server-to-server integration and can be managed programmatically via the key management API.
How do I stream partial output to clients?
Call result.getTextStream() and iterate async for deltas to forward to your client (WebSocket, SSE, or HTTP chunking).
Can tools change the model or temperature for the next turn?
Yes. Tools can provide nextTurnParams that compute instructions, model selection, or temperature based on tool output and the current context.