
Research
/Security News
npm Package Uses Prompt Injection and Token Flooding to Disrupt AI Malware Scanners
A new npm package tests AI malware scanners with prompt injection, safety-triggering comments, context flooding, and obfuscated JavaScript.
@openrouter/agent
Advanced tools
Agent toolkit for building AI applications with OpenRouter — tool orchestration, streaming, multi-turn conversations, and format compatibility.
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.
# 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').
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);
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();
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,
});
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.
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);
},
});
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' },
},
});
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: [] },
},
});
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,
});
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 }) => { /* ... */ },
});
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);
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';
# 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 lint
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)
Full callModel documentation is available at openrouter.ai/docs/sdks/typescript/call-model.
Apache-2.0
FAQs
Agent toolkit for building AI applications with OpenRouter — tool orchestration, streaming, multi-turn conversations, and format compatibility.
The npm package @openrouter/agent receives a total of 6,327 weekly downloads. As such, @openrouter/agent popularity was classified as popular.
We found that @openrouter/agent demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A new npm package tests AI malware scanners with prompt injection, safety-triggering comments, context flooding, and obfuscated JavaScript.

Product
Socket now detects supply chain risks in project manifests, starting with missing lockfiles that can make dependency installs non-reproducible.

Research
/Security News
The trojanized extensions use TinyGo-compiled WebAssembly and Solana transaction memos to resolve command-and-control infrastructure.