Imported from Mushfiq2002/stack-cost (
.agents/skills/sp-openrouter/SKILL.md). Install upstream withnpx skills add Mushfiq2002/stack-cost --skill sp-openrouter. Copyright stays with the author.
OpenRouter provider guidance
Generated by stripe projects llm-context for OpenRouter.
Source: https://openrouter.ai/.well-known/skills/openrouter-typescript-sdk/SKILL.md
name: openrouter-typescript-sdk description: Complete reference for integrating with 300+ AI models through the OpenRouter TypeScript SDK and Agent packages using the callModel pattern version: 2.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.
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
# 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, then initialize:
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
- Visit openrouter.ai/settings/keys
- Create a new API key
- Store securely in an environment variable
Environment Setup
export OPENROUTER_API_KEY=sk-or-v1-your-key-here
Client Initialization
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:
// 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):
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:
// 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:
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:
// 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:
// 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
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
- Environment Variables: Store API keys in environment variables, never in code
- Key Rotation: Rotate keys periodically using the key management API
- Environment Separation: Use different keys for development, staging, and production
- OAuth for Users: Use the OAuth PKCE flow for user-facing apps to avoid handling user credentials
- Secure Storage: Store user API keys encrypted in your database
- 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
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:
const result = client.callModel({
model: 'openai/gpt-5-nano',
input: 'Hello, how are you?'
});
Message Arrays
For multi-turn conversations:
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:
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:
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()
const result = client.callModel({
model: 'openai/gpt-5-nano',
input: 'Write a haiku about coding'
});
const text = await result.getText();
console.log(text);
getResponse()
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()
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
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
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:
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:
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:
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:
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
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:
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:
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:
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:
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
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
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
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:
interface TextMessage {
role: 'user' | 'assistant';
content: string;
}
Multimodal Message (Array Content)
Messages with mixed content types:
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:
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:
interface ToolResultMessage {
role: 'tool';
tool_call_id: string;
content: string; // JSON-encoded result
}
Non-Streaming Response Structure
The complete response object from getResponse():
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:
// Text/content message
interface ResponseOutputMessage {
type: 'message';
role: 'assistant';
content: string | Array<ContentPart>;
reasoning?: string; // Fo
...[truncated]