@openrouter/agent
v0.7.0
Published
Agent toolkit for building AI applications with OpenRouter — tool orchestration, streaming, multi-turn conversations, and format compatibility.
Readme
OpenRouter Agent (Beta)
Agent toolkit for building AI applications with OpenRouter — tool orchestration, streaming, multi-turn conversations, and format compatibility.
[!IMPORTANT] This SDK is currently in beta. There may be breaking changes between versions. We recommend pinning to a specific version in your
package.json.
Installation
# npm
npm install @openrouter/agent
# pnpm
pnpm add @openrouter/agent
# bun
bun add @openrouter/agent
# yarn
yarn add @openrouter/agent[!NOTE] This package is ESM-only. If you are using CommonJS, you can use
await import('@openrouter/agent').
Quick Start
import OpenRouter from '@openrouter/sdk';
import { callModel, tool } from '@openrouter/agent';
import { z } from 'zod';
const client = new OpenRouter({ apiKey: 'YOUR_API_KEY' });
const weatherTool = tool({
name: 'get_weather',
description: 'Get the current weather for a location',
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => ({
temperature: 72,
condition: 'sunny',
location,
}),
});
const result = callModel(client, {
model: 'openai/gpt-4o',
input: 'What is the weather in San Francisco?',
tools: [weatherTool] as const,
});
// Get the final text response (tools are auto-executed)
const text = await result.getText();
console.log(text);Features
Multiple Response Consumption Patterns
callModel returns a ModelResult that supports many ways to consume the response — all usable concurrently on the same result:
const result = callModel(client, { model, input, tools });
// Await the final text
const text = await result.getText();
// Await the full response with usage data
const response = await result.getResponse();
console.log(response.usage); // { inputTokens, outputTokens, cost, ... }
// Stream text deltas
for await (const delta of result.getTextStream()) {
process.stdout.write(delta);
}
// Stream reasoning deltas
for await (const delta of result.getReasoningStream()) {
process.stdout.write(delta);
}
// Stream tool execution events
for await (const event of result.getToolStream()) {
console.log(event);
}
// Stream structured tool calls
for await (const toolCall of result.getToolCallsStream()) {
console.log(toolCall.name, toolCall.input);
}
// Get all tool calls after completion
const toolCalls = await result.getToolCalls();Tool Types
The tool() factory creates type-safe tools with full Zod schema inference. Three tool types are supported:
Regular tools — automatically executed by the agent loop:
const searchTool = tool({
name: 'search',
description: 'Search the web',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.object({ results: z.array(z.string()) }),
execute: async ({ query }) => {
const results = await performSearch(query);
return { results };
},
});Generator tools — stream intermediate events during execution:
const analysisTool = tool({
name: 'analyze',
inputSchema: z.object({ data: z.string() }),
eventSchema: z.object({ progress: z.number() }),
outputSchema: z.object({ summary: z.string() }),
execute: async function* ({ data }) {
yield { progress: 0.5 };
// ... processing ...
return { summary: 'Analysis complete' };
},
});Manual tools — reported to the model but not auto-executed (for human-in-the-loop flows):
const confirmTool = tool({
name: 'confirm_action',
inputSchema: z.object({ action: z.string() }),
execute: false,
});Stop Conditions
Control when the agent loop stops executing tools:
import { callModel, stepCountIs, hasToolCall, maxTokensUsed, maxCost } from '@openrouter/agent';
const result = callModel(client, {
model: 'openai/gpt-4o',
input: 'Research this topic thoroughly',
tools: [searchTool, summarizeTool] as const,
// Single condition
stopWhen: stepCountIs(10),
// Or combine multiple (stops when ANY condition is met)
stopWhen: [stepCountIs(10), maxCost(0.50), hasToolCall('summarize')],
});Built-in stop conditions:
| Condition | Description |
|---|---|
| stepCountIs(n) | Stop after n tool execution steps (default: 5) |
| hasToolCall(name) | Stop when a specific tool is called |
| maxTokensUsed(n) | Stop when total tokens exceed a threshold |
| maxCost(dollars) | Stop when total cost exceeds a dollar amount |
| finishReasonIs(reason) | Stop on a specific finish reason |
Final response after stop
When stopWhen fires while the model is still emitting tool calls, pass
allowFinalResponse to force one more model turn with no tools:
callModel(client, {
model: 'openai/gpt-4o',
input: 'Research this topic',
tools: [searchTool] as const,
stopWhen: stepCountIs(5),
allowFinalResponse: 'Please summarize what you found.',
// or just: allowFinalResponse: true
});The pending tool calls from the halted turn are executed first so they
have real outputs in the input, then the full conversation and the
original instructions are sent to the model with no tools defined. A
non-empty string value is appended as a final user message. Any
non-executable (manual) tool calls in the halted turn are paired with
synthesized stub function_call_output items so the input is well-formed.
Tool Approval
Gate tool execution with approval checks for sensitive operations:
const deleteTool = tool({
name: 'delete_record',
inputSchema: z.object({ id: z.string() }),
requireApproval: true, // Always require approval
execute: async ({ id }) => { /* ... */ },
});
// Or use a function for conditional approval
const writeTool = tool({
name: 'write_file',
inputSchema: z.object({ path: z.string(), content: z.string() }),
requireApproval: ({ path }) => path.startsWith('/etc'),
execute: async ({ path, content }) => { /* ... */ },
});
// Handle approvals at the callModel level
const result = callModel(client, {
model: 'openai/gpt-4o',
input: 'Delete record abc-123',
tools: [deleteTool] as const,
approveToolCalls: async (toolCalls) => {
// Return IDs of approved tool calls
return toolCalls.map(tc => tc.id);
},
});Tool Context
Provide typed context data to tools without passing it through the model:
const dbTool = tool({
name: 'query_db',
inputSchema: z.object({ sql: z.string() }),
contextSchema: z.object({ connectionString: z.string() }),
execute: async ({ sql }, ctx) => {
const db = connect(ctx?.context.connectionString);
return db.query(sql);
},
});
const result = callModel(client, {
model: 'openai/gpt-4o',
input: 'List all users',
tools: [dbTool] as const,
context: {
query_db: { connectionString: 'postgres://localhost/mydb' },
},
});Shared Context
Share mutable state across all tools in a conversation:
const result = callModel(client, {
model: 'openai/gpt-4o',
input: 'Process these items',
tools: [toolA, toolB] as const,
sharedContextSchema: z.object({ processedIds: z.array(z.string()) }),
context: {
shared: { processedIds: [] },
},
});Conversation State Management
Persist multi-turn conversations with full state tracking:
import { createInitialState, callModel } from '@openrouter/agent';
// Start a conversation
let state = createInitialState();
// First turn
const result1 = callModel(client, {
model: 'openai/gpt-4o',
input: 'Search for TypeScript best practices',
tools: [searchTool] as const,
state,
});
// State is updated with messages, tool calls, and metadata
state = (await result1.getResponse()).state;
// Continue the conversation
const result2 = callModel(client, {
model: 'openai/gpt-4o',
input: 'Now summarize what you found',
tools: [searchTool] as const,
state,
});Dynamic Parameters Between Turns
Adjust model parameters dynamically based on tool execution:
const searchTool = tool({
name: 'search',
inputSchema: z.object({ query: z.string() }),
nextTurnParams: {
temperature: (input) => input.query.includes('creative') ? 0.9 : 0.1,
maxOutputTokens: () => 2000,
},
execute: async ({ query }) => { /* ... */ },
});Format Compatibility
Convert between OpenRouter and other message formats:
import { toClaudeMessage, fromClaudeMessages } from '@openrouter/agent';
import { toChatMessage, fromChatMessages } from '@openrouter/agent';
// Anthropic Claude format
const claudeMsg = toClaudeMessage(openRouterMessage);
const orMessages = fromClaudeMessages(claudeMessages);
// Standard Chat format
const chatMsg = toChatMessage(openRouterMessage);
const orMessages2 = fromChatMessages(chatMessages);Subpath Exports
For tree-shaking or targeted imports, the package provides granular subpath exports:
import { callModel } from '@openrouter/agent/call-model';
import { tool } from '@openrouter/agent/tool';
import { ModelResult } from '@openrouter/agent/model-result';
import { stepCountIs, maxCost } from '@openrouter/agent/stop-conditions';
import { toClaudeMessage } from '@openrouter/agent/anthropic-compat';
import { toChatMessage } from '@openrouter/agent/chat-compat';
import { ToolContextStore } from '@openrouter/agent/tool-context';
import { ToolEventBroadcaster } from '@openrouter/agent/tool-event-broadcaster';
import { createInitialState } from '@openrouter/agent/conversation-state';Development
# Install dependencies
pnpm install
# Build
pnpm build
# Run unit tests
pnpm test
# Run end-to-end tests (requires OPENROUTER_API_KEY in .env)
pnpm test:e2e
# Type check
pnpm typecheck
# Lint
pnpm lintRunning Tests
Create a .env file with your OpenRouter API key:
OPENROUTER_API_KEY=sk-or-...Then run:
pnpm test # Unit tests
pnpm test:e2e # Integration tests (requires API key)Documentation
Full callModel documentation is available at openrouter.ai/docs/sdks/typescript/call-model.
License
Apache-2.0
