用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/OpenRouterTeam/agent-skills --skill create-agent命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | create-agent |
| description | Bootstrap a modular AI agent with OpenRouter SDK, extensible hooks, and optional Ink TUI |
| metadata | {"version":"0.0.0","homepage":"https://openrouter.ai"} |
This skill helps you create a modular AI agent with:
┌─────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Ink TUI │ │ HTTP API │ │ Discord │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Agent Core │ │
│ │ (hooks & lifecycle) │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ OpenRouter SDK │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────┘
Get an OpenRouter API key at: https://openrouter.ai/settings/keys
⚠️ Security: Never commit API keys. Use environment variables.
mkdir my-agent && cd my-agent
npm init -y
npm pkg set type="module"
npm install @openrouter/sdk zod eventemitter3
npm install ink react # Optional: only for TUI
npm install -D typescript @types/react tsx
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}
{
"scripts": {
"start": "tsx src/cli.tsx",
"start:headless": "tsx src/headless.ts",
"dev": "tsx watch src/cli.tsx"
}
}
src/
├── agent.ts # Standalone agent core with hooks
├── tools.ts # Tool definitions
├── cli.tsx # Ink TUI (optional interface)
└── headless.ts # Headless usage example
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': () => ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: ;
}
{
: ;
?: ;
?: ;
?: <z., z.>[];
?: ;
}
<> {
: ;
: [] = [];
: <<, >> & { : };
() {
();
. = ({ : config. });
. = {
: config.,
: config. ?? ,
: config. ?? ,
: config. ?? [],
: config. ?? ,
};
}
(): [] {
[....];
}
(): {
. = [];
}
(: ): {
.. = instructions;
}
(: <z., z.>): {
...(newTool);
}
(: ): <> {
: = { : , content };
..(userMessage);
.(, userMessage);
.();
{
result = ..({
: ..,
: ..,
: ..( ({ : m., : m. })),
: ... > ? .. : ,
: [(..)],
});
.();
fullText = ;
( item result.()) {
.(, item);
(item.) {
:
textContent = item.?.( c. === );
(textContent && textContent) {
newText = textContent.;
(newText !== fullText) {
delta = newText.(fullText.);
fullText = newText;
.(, delta, fullText);
}
}
;
:
(item. === ) {
.(, item., .(item. || ));
}
;
:
.(, item., item.);
;
:
reasoningText = item.?.( c. === );
(reasoningText && reasoningText) {
.(, reasoningText.);
}
;
}
}
(!fullText) {
fullText = result.();
}
.(, fullText);
: = { : , : fullText };
..(assistantMessage);
.(, assistantMessage);
fullText;
} (err) {
error = err ? err : ((err));
.(, error);
error;
} {
.();
}
}
(: ): <> {
: = { : , content };
..(userMessage);
.(, userMessage);
{
result = ..({
: ..,
: ..,
: ..( ({ : m., : m. })),
: ... > ? .. : ,
: [(..)],
});
fullText = result.();
: = { : , : fullText };
..(assistantMessage);
.(, assistantMessage);
fullText;
} (err) {
error = err ? err : ((err));
.(, error);
error;
}
}
}
(): {
(config);
}
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.(, );
result = ()();
{ expression, result };
},
});
defaultTools = [timeTool, calculatorTool];
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) => .(, err.));
readline = ();
rl = readline.({
: process.,
: process.,
});
.();
= () => {
rl.(, (input) => {
(!input.()) {
();
;
}
agent.(input);
();
});
};
();
}
().(.);
Run headless: OPENROUTER_API_KEY=sk-or-... npm run start:headless
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 = ? '' ''}>
{isUser ? '▶ You' : '◀ Assistant'}
{message.content}
);
}
() {
(item.) {
: {
textContent = item.?.( c. === );
text = textContent && textContent ? textContent. : ;
(
);
}
:
(
);
: {
reasoningText = item.?.( c. === );
text = reasoningText && reasoningText ? reasoningText. : ;
(
);
}
:
;
}
}
() {
( {
(disabled) ;
(key.) ();
(key. || key.) (value.(, -));
(input && !key. && !key.) (value + input);
});
(
);
}
() {
{ exit } = ();
[messages, setMessages] = useState<[]>([]);
[input, setInput] = ();
[isLoading, setIsLoading] = ();
[items, setItems] = useState<<, >>( ());
( {
(key.) ();
});
( {
= () => {
();
( ());
};
= () => {
( (prev).(item., item));
};
= () => {
(agent.());
( ());
();
};
= () => {
();
};
agent.(, onThinkingStart);
agent.(, onItemUpdate);
agent.(, onMessageAssistant);
agent.(, onError);
{
agent.(, onThinkingStart);
agent.(, onItemUpdate);
agent.(, onMessageAssistant);
agent.(, onError);
};
}, []);
sendMessage = ( () => {
(!input.() || isLoading) ;
text = input.();
();
( [...prev, { : , : text }]);
agent.(text);
}, [input, isLoading]);
(
);
}
();
Run TUI: OPENROUTER_API_KEY=sk-or-... npm start
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.
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" }
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:
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);
});
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);
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);
| 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 |
| Method | Returns | Description |
|---|---|---|
send(content) | Promise | Send message with streaming |
sendSync(content) | Promise | 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 |
| 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 |
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 |
Do not hardcode model IDs - they change frequently. Use the models API:
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 = ();
models.( {
(filter. && !m..(filter. + )) ;
(filter. && m. < filter.) ;
(filter.) {
price = (m..);
(price > filter.) ;
}
;
});
}
claudeModels = ({ : });
.(claudeModels.( m.));
longContextModels = ({ : });
cheapModels = ({ : });
// 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.',
});
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
});
GET https://openrouter.ai/api/v1/models{ data: OpenRouterModel[] }基于 SOC 职业分类