diff --git a/skills/openrouter-typescript-sdk/SKILL.md b/skills/openrouter-typescript-sdk/SKILL.md index 1389155..dee5dcd 100644 --- a/skills/openrouter-typescript-sdk/SKILL.md +++ b/skills/openrouter-typescript-sdk/SKILL.md @@ -6,7 +6,7 @@ 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. +Integrate with 300+ AI models through the `callModel` pattern for text generation, tool usage, streaming, and multi-turn conversations. --- @@ -32,224 +32,7 @@ const client = new OpenRouter({ ## 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 +The SDK supports API key authentication and OAuth PKCE for user-facing apps. For detailed key management, OAuth flow, and security best practices, see `references/authentication.md`. --- @@ -347,6 +130,8 @@ The result object provides multiple methods for consuming the response: | `getTextStream()` | Stream text deltas as they arrive | | `getReasoningStream()` | Stream reasoning tokens (for o1/reasoning models) | | `getToolCallsStream()` | Stream tool calls as they complete | +| `getItemsStream()` | Stream cumulative item snapshots with `isComplete` flag and healed `arguments` | +| `getNewMessagesStream()` | Stream cumulative message snapshots with `isComplete` flag | ### getText() @@ -474,7 +259,7 @@ const searchTool = tool({ ``` #### Manual Tools -Set `execute: false` to handle tool calls yourself: +Set `execute: false` for human-in-the-loop flows where a person produces the tool call result. When the model calls a manual tool during the `callModel` loop, the loop exits early and returns. The model's response (including the tool call) is available in the response output. Present the tool call to the user, collect their response, and pass it back as a `function_call_output` message on the next `callModel` call. See [Manual Tool Execution](#manual-tool-execution) for the full lifecycle. ```typescript const manualTool = tool({ @@ -531,6 +316,259 @@ const result = client.callModel({ --- +## Manual Tool Execution + +Manual tools (`execute: false`) are for **human-in-the-loop flows** where a person — not code — produces the tool call result. When the model calls a manual tool, the `callModel` loop exits early and returns. The model's response — including the tool call — is in the response output. Present the tool call to the user, collect their input, and pass it back as a `function_call_output` on the next `callModel` invocation. + +Manual tools do not support approval flows — they are a separate mechanism. + +### Manual Tool Lifecycle + +```typescript +import OpenRouter, { tool } from '@openrouter/sdk'; +import { z } from 'zod'; + +const client = new OpenRouter({ + apiKey: process.env.OPENROUTER_API_KEY +}); + +// 1. Define a manual tool — the human provides the result +const askUserTool = tool({ + name: 'ask_user', + description: 'Ask the user a clarifying question', + inputSchema: z.object({ + question: z.string().describe('The question to ask the user') + }), + execute: false // Human-in-the-loop — loop exits when called +}); + +// 2. Call the model — loop exits when the manual tool is called +const result = client.callModel({ + model: 'openai/gpt-5-nano', + input: 'Help me plan a trip to Japan', + tools: [askUserTool] +}); + +const response = await result.getResponse(); + +// 3. Extract the tool call from response output +const toolCall = response.output.find( + (item) => item.type === 'function_call' && item.name === 'ask_user' +); + +if (toolCall) { + const args = JSON.parse(toolCall.arguments); + + // 4. Present the question to the user and collect their response + const userAnswer = await promptUser(args.question); + + // 5. Pass the human's response back on the next callModel call + const followUp = client.callModel({ + model: 'openai/gpt-5-nano', + input: [ + ...previousMessages, + ...response.output, + { + type: 'function_call_output', + call_id: toolCall.call_id, + output: JSON.stringify({ answer: userAnswer }) + } + ], + tools: [askUserTool] + }); + + const text = await followUp.getText(); + console.log(text); +} +``` + +### Mixing Manual and Automatic Tools + +When both manual and automatic tools are provided, automatic tools execute normally through the multi-turn loop. The loop only exits when the model calls a manual tool: + +```typescript +const searchTool = tool({ + name: 'search', + description: 'Search for information', + inputSchema: z.object({ query: z.string() }), + execute: async ({ query }) => { + return { results: ['Result 1', 'Result 2'] }; + } +}); + +const getUserInputTool = tool({ + name: 'get_user_preference', + description: 'Ask the user for their preference', + inputSchema: z.object({ + question: z.string().describe('The question to ask'), + options: z.array(z.string()).describe('Available options') + }), + execute: false // Human provides the answer +}); + +const result = client.callModel({ + model: 'openai/gpt-5-nano', + input: 'Help me pick a restaurant for dinner tonight', + tools: [searchTool, getUserInputTool] +}); + +// The SDK automatically executes search calls in the loop. +// When the model calls get_user_preference, the loop exits so the +// human can answer, and their response is passed back on the next call. +const response = await result.getResponse(); +``` + +--- + +## Approval Flows + +The SDK provides a built-in approval system for tools that should execute automatically *once approved*, but pause the loop when approval has not yet been granted. Unlike manual tools (where a human produces the result), approval-gated tools have an `execute` function that runs code — the SDK just won't call it until the tool call is approved. + +Approval state is persisted across `callModel` invocations via a `state` accessor, so the developer can inspect pending tool calls, present them to the user, and provide approval or rejection decisions on the next call. + +### Defining Approval-Gated Tools + +Set `requireApproval` on the tool definition — either a static boolean or a dynamic check function: + +```typescript +import OpenRouter, { tool } from '@openrouter/sdk'; +import { z } from 'zod'; + +// Static: always requires approval +const deleteTool = tool({ + name: 'delete_record', + description: 'Delete a database record', + inputSchema: z.object({ + recordId: z.string().describe('ID of the record to delete') + }), + requireApproval: true, + execute: async ({ recordId }) => { + await db.delete(recordId); + return { deleted: recordId }; + } +}); + +// Dynamic: conditionally requires approval based on arguments +const transferTool = tool({ + name: 'transfer_funds', + description: 'Transfer funds between accounts', + inputSchema: z.object({ + from: z.string(), + to: z.string(), + amount: z.number() + }), + requireApproval: async (params, context) => { + // Only require approval for large transfers + return params.amount > 1000; + }, + execute: async ({ from, to, amount }) => { + return await ledger.transfer(from, to, amount); + } +}); +``` + +### State Management + +Approval flows require a `state` accessor to persist conversation state (including pending tool calls) across `callModel` invocations. The `state` parameter uses a `StateAccessor` interface: + +```typescript +// In-memory state storage (for simple cases) +let savedState = null; + +const stateAccessor = { + get: async () => savedState, + set: async (state) => { savedState = state; } +}; +``` + +The `ConversationState` object managed by the SDK contains: + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `string` | Unique conversation ID | +| `messages` | `Message[]` | Full message history | +| `status` | `string` | `'in_progress'` \| `'awaiting_approval'` \| `'complete'` \| `'interrupted'` | +| `pendingToolCalls` | `ParsedToolCall[]` | Tool calls awaiting approval | +| `unsentToolResults` | `ToolExecutionResult[]` | Results not yet sent to API | +| `previousResponseId` | `string` | Last response ID | +| `updatedAt` | `Date` | Auto-updated timestamp | + +### callModel-Level Approval Parameters + +| Parameter | Description | +|-----------|-------------| +| `state` | `StateAccessor` (`get`/`set`) — required for approval/rejection params | +| `requireApproval` | Callback `(toolCall, context) => boolean` checked for each tool call (overrides tool-level setting) | +| `approveToolCalls` | Array of tool call IDs to approve and execute | +| `rejectToolCalls` | Array of tool call IDs to reject | + +**Note:** `approveToolCalls` and `rejectToolCalls` require a `state` accessor — TypeScript will emit a compilation error if state is omitted. + +### Complete Approval Flow Example + +```typescript +const client = new OpenRouter({ + apiKey: process.env.OPENROUTER_API_KEY +}); + +let savedState = null; +const stateAccessor = { + get: async () => savedState, + set: async (state) => { savedState = state; } +}; + +const deleteTool = tool({ + name: 'delete_user', + description: 'Delete a user account', + inputSchema: z.object({ + userId: z.string().describe('User ID to delete') + }), + requireApproval: true, + execute: async ({ userId }) => { + await db.deleteUser(userId); + return { deleted: userId }; + } +}); + +// First call — the loop pauses when delete_user is called +const result = client.callModel({ + model: 'openai/gpt-5-nano', + input: 'Delete the account for user u_abc123', + tools: [deleteTool], + state: stateAccessor +}); + +const response = await result.getResponse(); +// response status is 'awaiting_approval' +// savedState.pendingToolCalls contains the delete_user call + +// Inspect pending calls and present to user +const pending = savedState.pendingToolCalls; +for (const call of pending) { + console.log(`Tool: ${call.name}, Args: ${JSON.stringify(call.arguments)}`); +} + +// User approves — pass approval decisions on next callModel call +const approved = await promptUser('Approve deletion? (y/n)'); + +const followUp = client.callModel({ + model: 'openai/gpt-5-nano', + input: [], // No new user input — just processing approvals + tools: [deleteTool], + state: stateAccessor, + ...(approved + ? { approveToolCalls: pending.map(c => c.id) } + : { rejectToolCalls: pending.map(c => c.id) } + ) +}); + +// If approved, the SDK executes delete_user and continues the loop +const text = await followUp.getText(); +console.log(text); +``` + +--- + ## Dynamic Parameters Compute parameters based on conversation context: @@ -686,354 +724,112 @@ 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. +## Types and Event Shapes -### Message Roles +For complete type interfaces (message shapes, response structures, TurnContext, StepResult) and streaming event shapes (delta events, response events, tool stream events), see `references/types-and-events.md`. Search for specific types with: `grep -n "interface\|type " references/types-and-events.md` -Messages contain a `role` property that determines the message type: +### Cumulative Streams: getNewMessagesStream() and getItemsStream() -| Role | Description | -|------|-------------| -| `user` | User-provided input | -| `assistant` | Model-generated responses | -| `system` | System instructions | -| `developer` | Developer-level directives | -| `tool` | Tool execution results | +Both `getNewMessagesStream()` and `getItemsStream()` yield **cumulative snapshots, not deltas**. Each emission contains the full accumulated state of that item up to that point. The same item is emitted multiple times as it grows — always **replace** your previous snapshot rather than appending. -### Text Message +This differs from delta-based streams like `getTextStream()` and `getFullResponsesStream()`, which yield incremental chunks. -Simple text content from user or assistant: +#### isComplete Flag -```typescript -interface TextMessage { - role: 'user' | 'assistant'; - content: string; -} -``` +Every item from both streams carries an `isComplete: boolean` field: -### Multimodal Message (Array Content) +- `false` — the item is still receiving data (more emissions will follow for this item) +- `true` — this is the final emission for this item -Messages with mixed content types: +Use `isComplete` to distinguish in-progress snapshots from the finished version without needing to track item IDs yourself. ```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 - } - } - >; -} +type WithCompletion = T & { isComplete: boolean }; ``` -### 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 - }; - }>; -} -``` +#### arguments and rawArguments (getItemsStream only) -### Tool Result Message +`function_call` items in `getItemsStream()` expose two argument fields: -Result returned after tool execution: +- **`arguments`** — a best-effort parsed object produced by the SDK's internal `healJson()` utility, which closes truncated strings, objects, and arrays. Always a valid object (or `undefined` if healing fails), safe to use at any point during the stream. +- **`rawArguments`** — the raw accumulated JSON string from the API, which may be incomplete/unparseable mid-stream. ```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; - usage?: { - inputTokens: number; - outputTokens: number; - cachedTokens?: number; - }; - finishReason?: string; - warnings?: Array<{ - type: string; - message: string - }>; - experimental_providerMetadata?: Record; -} -``` - -### Response Message Types - -Output messages in the response array: - -```typescript -// Text/content message -interface ResponseOutputMessage { - type: 'message'; - role: 'assistant'; - content: string | Array; - 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; -} -``` - -### 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; -} +// StreamingFunctionCallItem — the function_call type in getItemsStream() +type StreamingFunctionCallItem = Omit & { + arguments?: Record | undefined; // Healed, always valid + rawArguments: string; // Raw accumulated string +}; ``` -### Tool Stream Events +#### getNewMessagesStream() -The `getToolStream()` method yields: +Yields cumulative message-level snapshots. Each `message` event contains the full text generated so far, plus `isComplete` to indicate whether the message is still streaming. ```typescript -type ToolStreamEvent = - | { type: 'delta'; content: string } - | { type: 'preliminary_result'; toolCallId: string; result: unknown }; +type MessageStreamUpdate = + | WithCompletion // Text/content snapshots with isComplete + | OpenResponsesFunctionCallOutput // Tool results + | ResponsesOutputItemFunctionCall; // Function calls ``` -### Example: Processing Stream Events - ```typescript const result = client.callModel({ model: 'openai/gpt-5-nano', - input: 'Analyze this data', - tools: [analysisTool] + input: 'Research this topic', + tools: [searchTool] }); -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; +for await (const message of result.getNewMessagesStream()) { + if (message.type === 'message') { + // Replace display text — message.content is the full text so far, not a delta + clearLine(); + process.stdout.write(message.content); - case 'response.completed': - console.log('\n[Complete]', event.response.usage); - break; + if (message.isComplete) { + console.log('\n[Message complete]'); + } + } else if (message.type === 'function_call_output') { + console.log('Tool result:', message.output); } } ``` -### Message Stream Events +#### getItemsStream() -The `getNewMessagesStream()` yields OpenResponses format updates: - -```typescript -type MessageStreamUpdate = - | ResponsesOutputMessage // Text/content updates - | OpenResponsesFunctionCallOutput; // Tool results -``` - -### Example: Tracking New Messages +Yields cumulative item-level snapshots. Each `function_call` item is emitted multiple times with a progressively more complete `arguments` object (healed) and `rawArguments` string. Each `message` item grows as text content accumulates. All items carry `isComplete`. ```typescript const result = client.callModel({ model: 'openai/gpt-5-nano', - input: 'Research this topic', - tools: [searchTool] + input: 'Look up the weather in Paris and Tokyo', + tools: [weatherTool] }); -const allMessages: MessageStreamUpdate[] = []; - -for await (const message of result.getNewMessagesStream()) { - allMessages.push(message); +for await (const item of result.getItemsStream()) { + if (item.type === 'function_call') { + // Emitted multiple times as arguments grow — replace, don't append. + // item.arguments is the healed parsed object — safe to use mid-stream. + console.log(`[${item.name}] args so far:`, item.arguments); + + if (item.isComplete) { + // Final emission — arguments is the complete parsed object. + // rawArguments is the complete JSON string if needed. + console.log(`[${item.name}] final args:`, item.arguments); + console.log(`[${item.name}] raw JSON:`, item.rawArguments); + } + } - if (message.type === 'message') { - console.log('Assistant:', message.content); - } else if (message.type === 'function_call_output') { - console.log('Tool result:', message.output); + if (item.type === 'message') { + // message.content is the full text so far — replace, don't append. + clearLine(); + process.stdout.write(item.content); } } ``` +**Important:** Do not append content across events from either stream — each emission is the complete accumulated state. Appending will produce duplicated output. Track items by `call_id` (for tool calls) or by index and replace the previous snapshot on each emission. + --- ## API Reference diff --git a/skills/openrouter-typescript-sdk/references/authentication.md b/skills/openrouter-typescript-sdk/references/authentication.md new file mode 100644 index 0000000..3bd7d3b --- /dev/null +++ b/skills/openrouter-typescript-sdk/references/authentication.md @@ -0,0 +1,179 @@ +# Authentication Reference + +## API Key Authentication + +### 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 +``` + +### Get Current Key Metadata + +```typescript +const keyInfo = await client.apiKeys.getCurrentKeyMetadata(); +console.log('Key name:', keyInfo.name); +console.log('Created:', keyInfo.createdAt); +``` + +### API Key Management + +```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 control their own API keys, OpenRouter supports OAuth with PKCE (Proof Key for Code Exchange). + +### 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 | Application 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, exchange the code for an API key: + +```typescript +const code = req.query.code; // From the redirect URL + +const apiKeyResponse = await client.oAuth.exchangeAuthCodeForAPIKey({ + code: code +}); + +const userApiKey = apiKeyResponse.key; +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 // 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' + }); + + req.session.oauthState = { /* ... */ }; + 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 + }); + + 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); + + 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 +5. **Secure Storage**: Store user API keys encrypted in the database +6. **Minimal Scope**: Create keys with only the permissions needed diff --git a/skills/openrouter-typescript-sdk/references/types-and-events.md b/skills/openrouter-typescript-sdk/references/types-and-events.md new file mode 100644 index 0000000..9bbdc5b --- /dev/null +++ b/skills/openrouter-typescript-sdk/references/types-and-events.md @@ -0,0 +1,277 @@ +# Types and Event Shapes Reference + +## Responses API Message Shapes + +The SDK uses the **OpenResponses** format for messages. + +### Message Roles + +| Role | Description | +|------|-------------| +| `user` | User-provided input | +| `assistant` | Model-generated responses | +| `system` | System instructions | +| `developer` | Developer-level directives | +| `tool` | Tool execution results | + +### Text Message + +```typescript +interface TextMessage { + role: 'user' | 'assistant'; + content: string; +} +``` + +### Multimodal Message (Array Content) + +```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 + +```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 + +```typescript +interface ToolResultMessage { + role: 'tool'; + tool_call_id: string; + content: string; // JSON-encoded result +} +``` + +### Non-Streaming Response Structure + +```typescript +interface OpenResponsesNonStreamingResponse { + output: Array; + usage?: { + inputTokens: number; + outputTokens: number; + cachedTokens?: number; + }; + finishReason?: string; + warnings?: Array<{ + type: string; + message: string + }>; + experimental_providerMetadata?: Record; +} +``` + +### Response Message Types + +```typescript +// Text/content message +interface ResponseOutputMessage { + type: 'message'; + role: 'assistant'; + content: string | Array; + 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 + +```typescript +interface ParsedToolCall { + id: string; + name: string; + arguments: unknown; // Validated against inputSchema +} +``` + +### Tool Execution Result + +```typescript +interface ToolExecutionResult { + toolCallId: string; + toolName: string; + result: unknown; // Validated against outputSchema + preliminaryResults?: unknown[]; // From generator tools + error?: Error; +} +``` + +### Step Result (for Stop Conditions) + +```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; +} +``` + +### TurnContext + +```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 + +### 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 }` | + +### Event Interfaces + +```typescript +interface OutputTextDeltaEvent { + type: 'response.output_text.delta'; + delta: string; +} + +interface ReasoningDeltaEvent { + type: 'response.reasoning.delta'; + delta: string; +} + +interface FunctionCallArgumentsDeltaEvent { + type: 'response.function_call_arguments.delta'; + delta: string; +} + +interface ToolPreliminaryResultEvent { + type: 'tool.preliminary_result'; + toolCallId: string; + result: unknown; // Matches the tool's eventSchema +} + +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 }; +``` + +### Cumulative Stream Types + +```typescript +type WithCompletion = T & { isComplete: boolean }; + +// StreamingFunctionCallItem — the function_call type in getItemsStream() +type StreamingFunctionCallItem = Omit & { + arguments?: Record | undefined; // Healed, always valid + rawArguments: string; // Raw accumulated string +}; + +// getItemsStream() yields StreamableOutputItem +type StreamableOutputItem = + | WithCompletion + | WithCompletion + | WithCompletion + | WithCompletion + | WithCompletion + | WithCompletion + | WithCompletion; + +// getNewMessagesStream() yields MessageStreamUpdate +type MessageStreamUpdate = + | WithCompletion + | OpenResponsesFunctionCallOutput + | ResponsesOutputItemFunctionCall; +``` + +### Message Stream Events + +The `getNewMessagesStream()` yields OpenResponses format updates: + +```typescript +type MessageStreamUpdate = + | ResponsesOutputMessage // Text/content snapshots + | OpenResponsesFunctionCallOutput; // Tool results +```