
Create Agent
- 2.7k installs
- 29 repo stars
- Updated January 31, 2026
- openrouterteam/agent-skills
create-agent is an OpenRouter skill that bootstraps a modular TypeScript agent with SDK model calls, tool hooks, and optional Ink terminal UI.
About
create-agent is an OpenRouter skill for bootstrapping a modular AI agent where a standalone Agent core runs headless while optional Ink TUI, HTTP API, or Discord interfaces attach on top. Architecture separates Agent Core with EventEmitter hooks from the OpenRouter SDK client that calls models with tools and step limits. Setup runs npm init, installs @openrouter/sdk, zod, eventemitter3, optional ink and react, configures TypeScript NodeNext module resolution, and adds start, start:headless, and dev scripts. src/agent.ts defines Message types, AgentEvents for stream, tool, reasoning, and error hooks, plus send() using getItemsStream with item:update emissions keyed by item id. src/tools.ts holds tool definitions; src/cli.tsx provides optional Ink UI; src/headless.ts demonstrates programmatic usage. The skill documents API key security via environment variables, default model openrouter/auto, configurable maxSteps with stepCountIs, runtime addTool registration, and reasoning item handling. Prerequisites point to openrouter.ai/settings/keys for credentials. File structure keeps UI optional so the same core serves CLI, server, or chat integrations without rewriting agent logic.
- Standalone Agent core with EventEmitter hooks decoupled from optional Ink TUI.
- OpenRouter SDK callModel with tools, maxSteps, and items-based streaming.
- Project scaffold covers agent.ts, tools.ts, cli.tsx, and headless.ts entrypoints.
- getItemsStream item:update pattern replaces chunks by stable item id.
- Supports headless, Ink terminal, or custom HTTP or Discord interface layers.
Create Agent by the numbers
- 2,687 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #304 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
create-agent capabilities & compatibility
- Capabilities
- openrouter sdk client setup with model, instruct · eventemitter lifecycle hooks for stream, tool, a · items based streaming via getitemsstream with id · optional ink react terminal ui separated from ag · runtime tool registration via addtool on agent i
- Works with
- openai · anthropic
- Use cases
- orchestration · api development · planning
What create-agent says it does
Never commit API keys. Use environment variables.
Use getItemsStream() for items-based streaming (recommended)
npx skills add https://github.com/openrouterteam/agent-skills --skill create-agentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.7k |
|---|---|
| repo stars | ★ 29 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 31, 2026 |
| Repository | openrouterteam/agent-skills ↗ |
How do I scaffold a modular AI agent with OpenRouter model access, streaming hooks, and optional terminal UI without coupling logic to one interface?
Bootstrap a modular TypeScript AI agent with OpenRouter SDK, hookable Agent core, tool definitions, and optional Ink terminal UI.
Who is it for?
Developers starting OpenRouter-backed agents who want hookable cores separable from Ink or HTTP frontends.
Skip if: Skip when you need a hosted no-code chatbot or a non-TypeScript stack without porting the scaffold.
When should I use this skill?
User wants to create an OpenRouter agent, bootstrap agent core with hooks, or add Ink TUI over OpenRouter SDK.
What you get
TypeScript project with Agent class, tool definitions, headless runner, and optional Ink CLI wired to OpenRouter via environment API key.
- src/agent.ts core module
- src/tools.ts definitions
- optional src/cli.tsx Ink UI
By the numbers
- OpenRouter SDK guide version 1.1.0 (January 2026)
- Access to 300+ models via OpenRouter Models API
Files
Build a Modular AI Agent with OpenRouter
This skill helps you create a modular AI agent with:
- Standalone Agent Core - Runs independently, extensible via hooks
- OpenRouter SDK - Unified access to 300+ language models
- Optional Ink TUI - Beautiful terminal UI (separate from agent logic)
Architecture
┌─────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Ink TUI │ │ HTTP API │ │ Discord │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Agent Core │ │
│ │ (hooks & lifecycle) │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ OpenRouter SDK │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────┘Prerequisites
Get an OpenRouter API key at: https://openrouter.ai/settings/keys
⚠️ Security: Never commit API keys. Use environment variables.
Project Setup
Step 1: Initialize Project
mkdir my-agent && cd my-agent
npm init -y
npm pkg set type="module"Step 2: Install Dependencies
npm install @openrouter/sdk zod eventemitter3
npm install ink react # Optional: only for TUI
npm install -D typescript @types/react tsxStep 3: Create tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}Step 4: Add Scripts to package.json
{
"scripts": {
"start": "tsx src/cli.tsx",
"start:headless": "tsx src/headless.ts",
"dev": "tsx watch src/cli.tsx"
}
}File Structure
src/
├── agent.ts # Standalone agent core with hooks
├── tools.ts # Tool definitions
├── cli.tsx # Ink TUI (optional interface)
└── headless.ts # Headless usage exampleStep 1: Agent Core with Hooks
Create src/agent.ts - the standalone agent that can run anywhere:
import { OpenRouter, tool, stepCountIs } from '@openrouter/sdk';
import type { Tool, StopCondition, StreamableOutputItem } from '@openrouter/sdk';
import { EventEmitter } from 'eventemitter3';
import { z } from 'zod';
// Message types
export interface Message {
role: 'user' | 'assistant' | 'system';
content: string;
}
// Agent events for hooks (items-based streaming model)
export interface AgentEvents {
'message:user': (message: Message) => void;
'message:assistant': (message: Message) => void;
'item:update': (item: StreamableOutputItem) => void; // Items emitted with same ID, replace by ID
'stream:start': () => void;
'stream:delta': (delta: string, accumulated: string) => void;
'stream:end': (fullText: string) => void;
'tool:call': (name: string, args: unknown) => void;
'tool:result': (name: string, result: unknown) => void;
'reasoning:update': (text: string) => void; // Extended thinking content
'error': (error: Error) => void;
'thinking:start': () => void;
'thinking:end': () => void;
}
// Agent configuration
export interface AgentConfig {
apiKey: string;
model?: string;
instructions?: string;
tools?: Tool<z.ZodTypeAny, z.ZodTypeAny>[];
maxSteps?: number;
}
// The Agent class - runs independently of any UI
export class Agent extends EventEmitter<AgentEvents> {
private client: OpenRouter;
private messages: Message[] = [];
private config: Required<Omit<AgentConfig, 'apiKey'>> & { apiKey: string };
constructor(config: AgentConfig) {
super();
this.client = new OpenRouter({ apiKey: config.apiKey });
this.config = {
apiKey: config.apiKey,
model: config.model ?? 'openrouter/auto',
instructions: config.instructions ?? 'You are a helpful assistant.',
tools: config.tools ?? [],
maxSteps: config.maxSteps ?? 5,
};
}
// Get conversation history
getMessages(): Message[] {
return [...this.messages];
}
// Clear conversation
clearHistory(): void {
this.messages = [];
}
// Add a system message
setInstructions(instructions: string): void {
this.config.instructions = instructions;
}
// Register additional tools at runtime
addTool(newTool: Tool<z.ZodTypeAny, z.ZodTypeAny>): void {
this.config.tools.push(newTool);
}
// Send a message and get streaming response using items-based model
// Items are emitted multiple times with the same ID but progressively updated content
// Replace items by their ID rather than accumulating chunks
async send(content: string): Promise<string> {
const userMessage: Message = { role: 'user', content };
this.messages.push(userMessage);
this.emit('message:user', userMessage);
this.emit('thinking:start');
try {
const result = this.client.callModel({
model: this.config.model,
instructions: this.config.instructions,
input: this.messages.map((m) => ({ role: m.role, content: m.content })),
tools: this.config.tools.length > 0 ? this.config.tools : undefined,
stopWhen: [stepCountIs(this.config.maxSteps)],
});
this.emit('stream:start');
let fullText = '';
// Use getItemsStream() for items-based streaming (recommended)
// Each item emission is complete - replace by ID, don't accumulate
for await (const item of result.getItemsStream()) {
// Emit the item for UI state management (use Map keyed by item.id)
this.emit('item:update', item);
switch (item.type) {
case 'message':
// Message items contain progressively updated content
const textContent = item.content?.find((c: { type: string }) => c.type === 'output_text');
if (textContent && 'text' in textContent) {
const newText = textContent.text;
if (newText !== fullText) {
const delta = newText.slice(fullText.length);
fullText = newText;
this.emit('stream:delta', delta, fullText);
}
}
break;
case 'function_call':
// Function call arguments stream progressively
if (item.status === 'completed') {
this.emit('tool:call', item.name, JSON.parse(item.arguments || '{}'));
}
break;
case 'function_call_output':
this.emit('tool:result', item.callId, item.output);
break;
case 'reasoning':
// Extended thinking/reasoning content
const reasoningText = item.content?.find((c: { type: string }) => c.type === 'reasoning_text');
if (reasoningText && 'text' in reasoningText) {
this.emit('reasoning:update', reasoningText.text);
}
break;
// Additional item types: web_search_call, file_search_call, image_generation_call
}
}
// Get final text if streaming didn't capture it
if (!fullText) {
fullText = await result.getText();
}
this.emit('stream:end', fullText);
const assistantMessage: Message = { role: 'assistant', content: fullText };
this.messages.push(assistantMessage);
this.emit('message:assistant', assistantMessage);
return fullText;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.emit('error', error);
throw error;
} finally {
this.emit('thinking:end');
}
}
// Send without streaming (simpler for programmatic use)
async sendSync(content: string): Promise<string> {
const userMessage: Message = { role: 'user', content };
this.messages.push(userMessage);
this.emit('message:user', userMessage);
try {
const result = this.client.callModel({
model: this.config.model,
instructions: this.config.instructions,
input: this.messages.map((m) => ({ role: m.role, content: m.content })),
tools: this.config.tools.length > 0 ? this.config.tools : undefined,
stopWhen: [stepCountIs(this.config.maxSteps)],
});
const fullText = await result.getText();
const assistantMessage: Message = { role: 'assistant', content: fullText };
this.messages.push(assistantMessage);
this.emit('message:assistant', assistantMessage);
return fullText;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.emit('error', error);
throw error;
}
}
}
// Factory function for easy creation
export function createAgent(config: AgentConfig): Agent {
return new Agent(config);
}Step 2: Define Tools
Create src/tools.ts:
import { tool } from '@openrouter/sdk';
import { z } from 'zod';
export const timeTool = tool({
name: 'get_current_time',
description: 'Get the current date and time',
inputSchema: z.object({
timezone: z.string().optional().describe('Timezone (e.g., "UTC", "America/New_York")'),
}),
execute: async ({ timezone }) => {
return {
time: new Date().toLocaleString('en-US', { timeZone: timezone || 'UTC' }),
timezone: timezone || 'UTC',
};
},
});
export const calculatorTool = tool({
name: 'calculate',
description: 'Perform mathematical calculations',
inputSchema: z.object({
expression: z.string().describe('Math expression (e.g., "2 + 2", "sqrt(16)")'),
}),
execute: async ({ expression }) => {
// Simple safe eval for basic math
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, '');
const result = Function(`"use strict"; return (${sanitized})`)();
return { expression, result };
},
});
export const defaultTools = [timeTool, calculatorTool];Step 3: Headless Usage (No UI)
Create src/headless.ts - use the agent programmatically:
import { createAgent } from './agent.js';
import { defaultTools } from './tools.js';
async function main() {
const agent = createAgent({
apiKey: process.env.OPENROUTER_API_KEY!,
model: 'openrouter/auto',
instructions: 'You are a helpful assistant with access to tools.',
tools: defaultTools,
});
// Hook into events
agent.on('thinking:start', () => console.log('\n🤔 Thinking...'));
agent.on('tool:call', (name, args) => console.log(`🔧 Using ${name}:`, args));
agent.on('stream:delta', (delta) => process.stdout.write(delta));
agent.on('stream:end', () => console.log('\n'));
agent.on('error', (err) => console.error('❌ Error:', err.message));
// Interactive loop
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log('Agent ready. Type your message (Ctrl+C to exit):\n');
const prompt = () => {
rl.question('You: ', async (input) => {
if (!input.trim()) {
prompt();
return;
}
await agent.send(input);
prompt();
});
};
prompt();
}
main().catch(console.error);Run headless: OPENROUTER_API_KEY=sk-or-... npm run start:headless
Step 4: Ink TUI (Optional Interface)
Create src/cli.tsx - a beautiful terminal UI that uses the agent with items-based streaming:
import React, { useState, useEffect, useCallback } from 'react';
import { render, Box, Text, useInput, useApp } from 'ink';
import type { StreamableOutputItem } from '@openrouter/sdk';
import { createAgent, type Agent, type Message } from './agent.js';
import { defaultTools } from './tools.js';
// Initialize agent (runs independently of UI)
const agent = createAgent({
apiKey: process.env.OPENROUTER_API_KEY!,
model: 'openrouter/auto',
instructions: 'You are a helpful assistant. Be concise.',
tools: defaultTools,
});
function ChatMessage({ message }: { message: Message }) {
const isUser = message.role === 'user';
return (
<Box flexDirection="column" marginBottom={1}>
<Text bold color={isUser ? 'cyan' : 'green'}>
{isUser ? '▶ You' : '◀ Assistant'}
</Text>
<Text wrap="wrap">{message.content}</Text>
</Box>
);
}
// Render streaming items by type using the items-based pattern
function ItemRenderer({ item }: { item: StreamableOutputItem }) {
switch (item.type) {
case 'message': {
const textContent = item.content?.find((c: { type: string }) => c.type === 'output_text');
const text = textContent && 'text' in textContent ? textContent.text : '';
return (
<Box flexDirection="column" marginBottom={1}>
<Text bold color="green">◀ Assistant</Text>
<Text wrap="wrap">{text}</Text>
{item.status !== 'completed' && <Text color="gray">▌</Text>}
</Box>
);
}
case 'function_call':
return (
<Text color="yellow">
{item.status === 'completed' ? ' ✓' : ' 🔧'} {item.name}
{item.status === 'in_progress' && '...'}
</Text>
);
case 'reasoning': {
const reasoningText = item.content?.find((c: { type: string }) => c.type === 'reasoning_text');
const text = reasoningText && 'text' in reasoningText ? reasoningText.text : '';
return (
<Box flexDirection="column" marginBottom={1}>
<Text bold color="magenta">💭 Thinking</Text>
<Text wrap="wrap" color="gray">{text}</Text>
</Box>
);
}
default:
return null;
}
}
function InputField({
value,
onChange,
onSubmit,
disabled,
}: {
value: string;
onChange: (v: string) => void;
onSubmit: () => void;
disabled: boolean;
}) {
useInput((input, key) => {
if (disabled) return;
if (key.return) onSubmit();
else if (key.backspace || key.delete) onChange(value.slice(0, -1));
else if (input && !key.ctrl && !key.meta) onChange(value + input);
});
return (
<Box>
<Text color="yellow">{'> '}</Text>
<Text>{value}</Text>
<Text color="gray">{disabled ? ' ···' : '█'}</Text>
</Box>
);
}
function App() {
const { exit } = useApp();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Use Map keyed by item ID for efficient React state updates (items-based pattern)
const [items, setItems] = useState<Map<string, StreamableOutputItem>>(new Map());
useInput((_, key) => {
if (key.escape) exit();
});
// Subscribe to agent events using items-based streaming
useEffect(() => {
const onThinkingStart = () => {
setIsLoading(true);
setItems(new Map()); // Clear items for new response
};
// Items-based streaming: replace items by ID, don't accumulate
const onItemUpdate = (item: StreamableOutputItem) => {
setItems((prev) => new Map(prev).set(item.id, item));
};
const onMessageAssistant = () => {
setMessages(agent.getMessages());
setItems(new Map()); // Clear streaming items
setIsLoading(false);
};
const onError = (err: Error) => {
setIsLoading(false);
};
agent.on('thinking:start', onThinkingStart);
agent.on('item:update', onItemUpdate);
agent.on('message:assistant', onMessageAssistant);
agent.on('error', onError);
return () => {
agent.off('thinking:start', onThinkingStart);
agent.off('item:update', onItemUpdate);
agent.off('message:assistant', onMessageAssistant);
agent.off('error', onError);
};
}, []);
const sendMessage = useCallback(async () => {
if (!input.trim() || isLoading) return;
const text = input.trim();
setInput('');
setMessages((prev) => [...prev, { role: 'user', content: text }]);
await agent.send(text);
}, [input, isLoading]);
return (
<Box flexDirection="column" padding={1}>
<Box marginBottom={1}>
<Text bold color="magenta">🤖 OpenRouter Agent</Text>
<Text color="gray"> (Esc to exit)</Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
{/* Render completed messages */}
{messages.map((msg, i) => (
<ChatMessage key={i} message={msg} />
))}
{/* Render streaming items by type (items-based pattern) */}
{Array.from(items.values()).map((item) => (
<ItemRenderer key={item.id} item={item} />
))}
</Box>
<Box borderStyle="single" borderColor="gray" paddingX={1}>
<InputField
value={input}
onChange={setInput}
onSubmit={sendMessage}
disabled={isLoading}
/>
</Box>
</Box>
);
}
render(<App />);Run TUI: OPENROUTER_API_KEY=sk-or-... npm start
Understanding Items-Based Streaming
The OpenRouter SDK uses an items-based streaming model - a key paradigm where items are emitted multiple times with the same ID but progressively updated content. Instead of accumulating chunks, you replace items by their ID.
How It Works
Each iteration of getItemsStream() yields a complete item with updated content:
// Iteration 1: Partial message
{ id: "msg_123", type: "message", content: [{ type: "output_text", text: "Hello" }] }
// Iteration 2: Updated message (replace, don't append)
{ id: "msg_123", type: "message", content: [{ type: "output_text", text: "Hello world" }] }For function calls, arguments stream progressively:
// Iteration 1: Partial arguments
{ id: "call_456", type: "function_call", name: "get_weather", arguments: "{\"q" }
// Iteration 2: Complete arguments
{ id: "call_456", type: "function_call", name: "get_weather", arguments: "{\"query\": \"Paris\"}", status: "completed" }Why Items Are Better
Traditional (accumulation required):
let text = '';
for await (const chunk of result.getTextStream()) {
text += chunk; // Manual accumulation
updateUI(text);
}Items (complete replacement):
const items = new Map<string, StreamableOutputItem>();
for await (const item of result.getItemsStream()) {
items.set(item.id, item); // Replace by ID
updateUI(items);
}Benefits:
- No manual chunk management - each item is complete
- Handles concurrent outputs - function calls and messages can stream in parallel
- Full TypeScript inference for all item types
- Natural Map-based state works perfectly with React/UI frameworks
Extending the Agent
Add Custom Hooks
const agent = createAgent({ apiKey: '...' });
// Log all events
agent.on('message:user', (msg) => {
saveToDatabase('user', msg.content);
});
agent.on('message:assistant', (msg) => {
saveToDatabase('assistant', msg.content);
sendWebhook('new_message', msg);
});
agent.on('tool:call', (name, args) => {
analytics.track('tool_used', { name, args });
});
agent.on('error', (err) => {
errorReporting.capture(err);
});Use with HTTP Server
import express from 'express';
import { createAgent } from './agent.js';
const app = express();
app.use(express.json());
// One agent per session (store in memory or Redis)
const sessions = new Map<string, Agent>();
app.post('/chat', async (req, res) => {
const { sessionId, message } = req.body;
let agent = sessions.get(sessionId);
if (!agent) {
agent = createAgent({ apiKey: process.env.OPENROUTER_API_KEY! });
sessions.set(sessionId, agent);
}
const response = await agent.sendSync(message);
res.json({ response, history: agent.getMessages() });
});
app.listen(3000);Use with Discord
import { Client, GatewayIntentBits } from 'discord.js';
import { createAgent } from './agent.js';
const discord = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
});
const agents = new Map<string, Agent>();
discord.on('messageCreate', async (msg) => {
if (msg.author.bot) return;
let agent = agents.get(msg.channelId);
if (!agent) {
agent = createAgent({ apiKey: process.env.OPENROUTER_API_KEY! });
agents.set(msg.channelId, agent);
}
const response = await agent.sendSync(msg.content);
await msg.reply(response);
});
discord.login(process.env.DISCORD_TOKEN);Agent API Reference
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | required | OpenRouter API key |
| model | string | 'openrouter/auto' | Model to use |
| instructions | string | 'You are a helpful assistant.' | System prompt |
| tools | Tool[] | [] | Available tools |
| maxSteps | number | 5 | Max agentic loop iterations |
Methods
| Method | Returns | Description |
|---|---|---|
send(content) | Promise<string> | Send message with streaming |
sendSync(content) | Promise<string> | Send message without streaming |
getMessages() | Message[] | Get conversation history |
clearHistory() | void | Clear conversation |
setInstructions(text) | void | Update system prompt |
addTool(tool) | void | Add tool at runtime |
Events
| Event | Payload | Description |
|---|---|---|
message:user | Message | User message added |
message:assistant | Message | Assistant response complete |
item:update | StreamableOutputItem | Item emitted (replace by ID, don't accumulate) |
stream:start | - | Streaming started |
stream:delta | (delta, accumulated) | New text chunk |
stream:end | fullText | Streaming complete |
tool:call | (name, args) | Tool being called |
tool:result | (name, result) | Tool returned result |
reasoning:update | text | Extended thinking content |
thinking:start | - | Agent processing |
thinking:end | - | Agent done processing |
error | Error | Error occurred |
Item Types (from getItemsStream)
The SDK uses an items-based streaming model where items are emitted multiple times with the same ID but progressively updated content. Replace items by their ID rather than accumulating chunks.
| Type | Purpose |
|---|---|
message | Assistant text responses |
function_call | Tool invocations with streaming arguments |
function_call_output | Results from executed tools |
reasoning | Extended thinking content |
web_search_call | Web search operations |
file_search_call | File search operations |
image_generation_call | Image generation operations |
Discovering Models
Do not hardcode model IDs - they change frequently. Use the models API:
Fetch Available Models
interface OpenRouterModel {
id: string;
name: string;
description?: string;
context_length: number;
pricing: { prompt: string; completion: string };
top_provider?: { is_moderated: boolean };
}
async function fetchModels(): Promise<OpenRouterModel[]> {
const res = await fetch('https://openrouter.ai/api/v1/models');
const data = await res.json();
return data.data;
}
// Find models by criteria
async function findModels(filter: {
author?: string; // e.g., 'anthropic', 'openai', 'google'
minContext?: number; // e.g., 100000 for 100k context
maxPromptPrice?: number; // e.g., 0.001 for cheap models
}): Promise<OpenRouterModel[]> {
const models = await fetchModels();
return models.filter((m) => {
if (filter.author && !m.id.startsWith(filter.author + '/')) return false;
if (filter.minContext && m.context_length < filter.minContext) return false;
if (filter.maxPromptPrice) {
const price = parseFloat(m.pricing.prompt);
if (price > filter.maxPromptPrice) return false;
}
return true;
});
}
// Example: Get latest Claude models
const claudeModels = await findModels({ author: 'anthropic' });
console.log(claudeModels.map((m) => m.id));
// Example: Get models with 100k+ context
const longContextModels = await findModels({ minContext: 100000 });
// Example: Get cheap models
const cheapModels = await findModels({ maxPromptPrice: 0.0005 });Dynamic Model Selection in Agent
// Create agent with dynamic model selection
const models = await fetchModels();
const bestModel = models.find((m) => m.id.includes('claude')) || models[0];
const agent = createAgent({
apiKey: process.env.OPENROUTER_API_KEY!,
model: bestModel.id, // Use discovered model
instructions: 'You are a helpful assistant.',
});Using openrouter/auto
For simplicity, use openrouter/auto which automatically selects the best available model for your request:
const agent = createAgent({
apiKey: process.env.OPENROUTER_API_KEY!,
model: 'openrouter/auto', // Auto-selects best model
});Models API Reference
- Endpoint:
GET https://openrouter.ai/api/v1/models - Response:
{ data: OpenRouterModel[] } - Browse models: https://openrouter.ai/models
Resources
- OpenRouter Docs: https://openrouter.ai/docs
- Models API: https://openrouter.ai/api/v1/models
- Ink Docs: https://github.com/vadimdemedes/ink
- Get API Key: https://openrouter.ai/settings/keys
{
"version": "1.1.0",
"organization": "OpenRouter Inc",
"date": "January 2026",
"abstract": "Complete guide for building modular AI agents with the OpenRouter TypeScript SDK. Features a standalone Agent class with EventEmitter-based hooks for extensibility, items-based streaming model for efficient UI state management, optional Ink TUI for interactive terminal interfaces, and examples for HTTP server and Discord integrations. Includes Zod-based tool definitions, streaming responses with support for reasoning/thinking items, multi-turn conversations, and dynamic model discovery via the OpenRouter Models API.",
"references": [
"https://openrouter.ai/docs/sdks/typescript",
"https://openrouter.ai/docs/sdks/typescript/call-model/working-with-items",
"https://openrouter.ai/docs/api-reference",
"https://openrouter.ai/api/v1/models",
"https://github.com/vadimdemedes/ink"
]
}
Related skills
How it compares
Modular OpenRouter agent scaffold with hookable core; UI layers are optional add-ons.
FAQ
Where should the OpenRouter API key live?
Use environment variables; never commit API keys according to the skill security warning.
Is the Ink TUI required?
No. The Agent core runs headless; Ink is an optional interface layer atop the same core.
What streaming API does send() use?
It iterates result.getItemsStream() and emits item:update events keyed by item id for progressive UI replacement.
Is Create Agent safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.