一键导入
openrouter-proxy
OpenRouter API expert - access 400+ models with a unified API, dynamic model discovery, provider routing, fallbacks, and app attribution headers.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
OpenRouter API expert - access 400+ models with a unified API, dynamic model discovery, provider routing, fallbacks, and app attribution headers.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
WCAG 2.2 accessibility audit and remediation guidance for frontend code, UI reviews, keyboard/focus testing, ARIA usage, contrast, forms, and regression checks.
Local-first personal AI agent gateway architecture for Windows with messaging integrations, tool execution, and real-time streaming.
Anthropic Claude API expert - master Messages API requests, responses, streaming, tool use, prompt caching, and current Claude 4-family model practices.
REST and HTTP API design with OpenAPI 3.1, resource modeling, versioning, pagination, idempotency, errors, validation, auth boundaries, and production compatibility.
Expert in converting between AI provider APIs - Anthropic, Gemini, OpenRouter, Groq, Mistral, DeepSeek, and NVIDIA NIM.
Comprehensive authentication system for AI agent gateway with JWT tokens, session management, and multi-factor authentication.
| name | openrouter-proxy |
| type | skill |
| version | 1.0.0 |
| description | OpenRouter API expert - access 400+ models with a unified API, dynamic model discovery, provider routing, fallbacks, and app attribution headers. |
| author | skillregistry |
| license | MIT |
| agents | ["cursor","claude-code","copilot","gemini-cli","codex"] |
| categories | ["backend","ai-ml","architecture"] |
| tags | ["ai","api","openrouter","multi-provider","routing","llm","proxy"] |
Master the OpenRouter API to access hundreds of LLMs from multiple providers through a single, OpenAI-compatible interface. Treat model availability, pricing, context length, and supported parameters as dynamic metadata from /api/v1/models, then pin explicit model IDs for reproducibility or use documented latest-family aliases for flexible routing.
import fetch from 'node-fetch';
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model: '~openai/gpt-latest',
messages: [{ role: 'user', content: 'Hello!' }]
})
});
const data = await response.json();
console.log(data.choices[0].message.content);
curl https://openrouter.ai/api/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_KEY" \
-d '{
"model": "~anthropic/claude-sonnet-latest",
"messages": [{"role": "user", "content": "Hello!"}]
}'
| Category | Examples | Provider | Notes |
|---|---|---|---|
| OpenAI | openai/gpt-5, ~openai/gpt-latest | OpenAI | Official OpenAI models and latest-family aliases |
| Anthropic | anthropic/claude-sonnet-4, ~anthropic/claude-sonnet-latest | Anthropic | Claude models and latest-family aliases |
google/gemini-2.5-pro, google/gemini-2.5-flash | Gemini models | ||
| Meta | meta-llama/* | Meta and hosted providers | Llama family, provider availability varies |
| Mistral | mistralai/* | Mistral and hosted providers | Mistral family |
| DeepSeek | deepseek/* | DeepSeek and hosted providers | DeepSeek chat/reasoning models |
| NVIDIA | Provider-specific slugs from /models | NVIDIA | NIM-backed availability changes over time |
/api/v1/models to read id, context_length, pricing, top_provider, and supported_parameters.~openai/gpt-latest and ~anthropic/claude-sonnet-latest only when automatic upgrades are acceptable.models fallback array and provider routing options for uptime, cost, and latency policy.openai/gpt-5-mini - Cost-efficient GPT-5 familyanthropic/claude-3-haiku - Fast Claudegoogle/gemini-2.5-flash - Fast Gemini/api/v1/models for current low-cost provider-hosted options.https://openrouter.ai/api/v1/chat/completionshttps://openrouter.ai/api/v1/modelshttps://openrouter.ai/api/v1/models/{model_id}{
"model": "~anthropic/claude-sonnet-latest",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
],
"max_tokens": 1024,
"temperature": 0.7,
"top_p": 0.9,
"stream": false
}
| Parameter | Type | Default | Description |
|---|---|---|---|
| model | string | required | Model ID or alias |
| messages | array | required | Message history |
| max_tokens | number | inf | Max output tokens |
| temperature | number | 1.0 | Randomness (0-2) |
| top_p | number | 1.0 | Nucleus sampling |
| top_k | number | null | Top-k sampling |
| stop | string/array | null | Stop sequences |
| stream | boolean | false | Enable streaming |
| presence_penalty | number | 0 | Repetition penalty |
| frequency_penalty | number | 0 | Frequency penalty |
| user | string | null | User identifier |
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1717000000,
"model": "~anthropic/claude-sonnet-latest",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 10,
"total_tokens": 35
}
}
{
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"created": 1717000000,
"model": "~anthropic/claude-sonnet-latest",
"choices": [
{
"index": 0,
"delta": {"content": "Hello"},
"finish_reason": null
}
]
}
Use provider-specific model names:
{
"model": "claude-sonnet-latest", // Same as ~anthropic/claude-sonnet-latest
"messages": [...]
}
Route to your own deployment:
{
"model": "my-deployment/my-model",
"site_url": "https://my-openrouter.example.com",
"messages": [...]
}
Identify your application:
{
"app_name": "MyApp/1.0",
"model": "~anthropic/claude-sonnet-latest",
"messages": [...]
}
Get ranked model recommendations:
curl https://openrouter.ai/api/v1/models \
-H "Authorization: Bearer $OPENROUTER_KEY" \
-H "HTTP-Referer: https://myapp.com" \
-H "X-Title: MyApp"
Response includes:
rank - Popularity rankcost - Price per 1M tokenscontext_length - Max context windowprovider - Model providerSet custom API endpoint:
{
"site_url": "https://my-custom-openrouter.example.com",
"site_name": "My Custom Instance",
"model": "~anthropic/claude-sonnet-latest",
"messages": [...]
}
Pass through provider headers:
{
"model": "~anthropic/claude-sonnet-latest",
"messages": [...],
"provider": {
"anthropic": {
"anthropic_version": "bedrock-2023-05-31"
}
}
}
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
const OPENROUTER_KEY = process.env.OPENROUTER_KEY;
const OPENROUTER_URL = process.env.OPENROUTER_URL || 'https://openrouter.ai/api/v1';
app.post('/v1/chat/completions', async (req, res) => {
try {
const response = await fetch(`${OPENROUTER_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify(req.body)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'OpenRouter error');
}
const data = await response.json();
res.json(data);
} catch (error: any) {
res.status(error.statusCode || 500).json({ error: error.message });
}
});
app.listen(3000);
app.post('/:provider/chat', async (req, res) => {
const { provider } = req.params;
// Map provider to OpenRouter model
const modelMap: Record<string, string> = {
anthropic: '~anthropic/claude-sonnet-latest',
gemini: 'google/gemini-2.5-pro',
openai: 'openai/gpt-5',
mistral: 'mistral/mistral-large',
groq: 'meta-llama/llama-3.3-70b-instruct',
deepseek: 'deepseek/deepseek-chat'
};
const model = modelMap[provider] || req.body.model;
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: req.body.messages,
max_tokens: req.body.max_tokens || 4096,
temperature: req.body.temperature || 0.7
})
});
const data = await response.json();
res.json(data);
});
app.post('/stream', async (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify({
...req.body,
stream: true
})
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
if (chunk.includes('[DONE]')) break;
res.write(`data: ${chunk}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
});
// Round-robin load balancing across models
let modelIndex = 0;
const models = [
'~anthropic/claude-sonnet-latest',
'google/gemini-2.5-pro',
'openai/gpt-5-mini'
];
app.post('/balanced/chat', async (req, res) => {
const model = models[modelIndex % models.length];
modelIndex++;
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: req.body.messages,
max_tokens: req.body.max_tokens || 4096
})
});
const data = await response.json();
res.json({ ...data, model_used: model });
});
{
"error": {
"message": "Invalid request format",
"type": "BadRequestError",
"param": null,
"code": null
}
}
{
"error": {
"message": "Invalid or expired API key",
"type": "AuthenticationError"
}
}
{
"error": {
"message": "Access denied",
"type": "PermissionError"
}
}
{
"error": {
"message": "Model not found",
"type": "NotFoundError"
}
}
{
"error": {
"message": "Rate limit exceeded",
"type": "RateLimitError"
}
}
{
"error": {
"message": "Internal server error",
"type": "InternalServerError"
}
}
{
"error": {
"message": "Anthropic error: Rate limit exceeded",
"type": "ProviderError",
"provider": "anthropic",
"status_code": 429
}
}
// Choose model based on task complexity and budget
function selectModel(task: string, budget: 'low' | 'medium' | 'high'): string {
const modelMap: Record<string, Record<'low' | 'medium' | 'high', string>> = {
chat: {
low: 'meta-llama/llama-3.3-70b-instruct',
medium: 'anthropic/claude-3-haiku',
high: 'openai/gpt-5'
},
coding: {
low: 'meta-llama/llama-3.3-70b-instruct',
medium: '~anthropic/claude-sonnet-latest',
high: 'openai/gpt-5'
},
reasoning: {
low: 'meta-llama/llama-3.3-70b-instruct',
medium: 'google/gemini-2.5-pro',
high: 'anthropic/claude-opus-4'
}
};
return modelMap[task]?.[budget] || '~anthropic/claude-sonnet-latest';
}
async function generateWithFallback(
messages: any[],
primaryModel: string,
fallbackModels: string[]
) {
const models = [primaryModel, ...fallbackModels];
for (const model of models) {
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify({ model, messages })
});
if (response.ok) {
return await response.json();
}
} catch (error) {
console.log(`Failed with ${model}, trying next...`);
}
}
throw new Error('All fallback models failed');
}
async function withRetry(
request: any,
maxRetries = 3,
backoffFactor = 2
) {
let attempt = 0;
while (true) {
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify(request)
});
if (response.ok) {
return await response.json();
}
if (response.status !== 429 || attempt >= maxRetries) {
throw new Error(`OpenRouter error: ${response.status}`);
}
const retryAfter = response.headers.get('retry-after') ||
(backoffFactor ** attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, Number(retryAfter)));
attempt++;
} catch (error) {
if (attempt >= maxRetries) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
attempt++;
}
}
}
// Cache available models
let modelsCache: any[] | null = null;
async function getAvailableModels(forceRefresh = false): Promise<any[]> {
if (modelsCache && !forceRefresh) {
return modelsCache;
}
const response = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Authorization': `Bearer ${OPENROUTER_KEY}`
}
});
const data = await response.json();
modelsCache = data.data;
return modelsCache;
}
// Filter models by provider
async function getModelsByProvider(provider: string) {
const models = await getAvailableModels();
return models.filter(m => m.id.startsWith(`${provider}/`));
}
// Track token usage by model
const tokenUsage: Record<string, { input: number; output: number }> = {};
function trackTokens(model: string, usage: any) {
if (!tokenUsage[model]) {
tokenUsage[model] = { input: 0, output: 0 };
}
tokenUsage[model].input += usage.prompt_tokens;
tokenUsage[model].output += usage.completion_tokens;
return tokenUsage[model];
}
// Calculate cost
function calculateCost(model: string, tokens: { input: number; output: number }) {
const pricing: Record<string, { input: number; output: number }> = {
'openai/gpt-5': { input: 0.00005, output: 0.00015 },
'~anthropic/claude-sonnet-latest': { input: 0.00003, output: 0.00015 },
'google/gemini-2.5-pro': { input: 0.000025, output: 0.0001 },
'meta-llama/llama-3.3-70b-instruct': { input: 0.0000005, output: 0.0000008 }
};
const rates = pricing[model] || { input: 0, output: 0 };
return {
inputCost: tokens.input * rates.input,
outputCost: tokens.output * rates.output,
totalCost: tokens.input * rates.input + tokens.output * rates.output
};
}
import { Agent } from 'https';
import fetch from 'node-fetch';
const httpsAgent = new Agent({
keepAlive: true,
keepAliveMsecs: 60000,
maxSockets: 50,
maxFreeSockets: 10
});
// Use the agent in fetch
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
agent: httpsAgent,
method: 'POST',
// ...
});
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300 }); // 5 minute cache
async function cachedGenerate(request: any) {
const cacheKey = JSON.stringify(request);
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify(request)
});
const data = await response.json();
cache.set(cacheKey, data);
return data;
}
class RequestBatcher {
private queue: Array<{ request: any; resolve: Function; reject: Function }> = [];
private processing = false;
add(request: any) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
this.process();
});
}
async process() {
if (this.processing) return;
this.processing = true;
const batch = this.queue.splice(0, Math.min(this.queue.length, 10));
try {
const responses = await Promise.all(
batch.map(({ request }) =>
fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify(request)
}).then(r => r.json())
)
);
batch.forEach((item, i) => item.resolve(responses[i]));
} catch (error) {
batch.forEach(item => item.reject(error));
} finally {
this.processing = false;
}
}
}
import { describe, it, expect, vi } from 'vitest';
describe('OpenRouter Proxy', () => {
it('generates content', async () => {
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve({
id: 'chatcmpl-123',
object: 'chat.completion',
created: 1717000000,
model: '~anthropic/claude-sonnet-latest',
choices: [{
index: 0,
message: { role: 'assistant', content: 'Hello!' },
finish_reason: 'stop'
}],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
})
}));
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test-key'
},
body: JSON.stringify({
model: '~anthropic/claude-sonnet-latest',
messages: [{ role: 'user', content: 'Hi' }]
})
});
const data = await response.json();
expect(data.choices[0].message.content).toBe('Hello!');
});
});
import { createProxy } from './proxy';
describe('OpenRouter Proxy Integration', () => {
it('proxies to OpenRouter', async () => {
const proxy = createProxy({ port: 3001 });
const response = await fetch('http://localhost:3001/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: '~anthropic/claude-sonnet-latest',
messages: [{ role: 'user', content: 'Test' }]
})
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.model).toBe('~anthropic/claude-sonnet-latest');
});
});
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
COPY .env ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
version: '3.8'
services:
openrouter-proxy:
build: .
ports:
- "3000:3000"
environment:
- OPENROUTER_KEY=${OPENROUTER_KEY}
- PRIMARY_MODEL=~anthropic/claude-sonnet-latest
- FALLBACK_MODELS=google/gemini-2.5-pro,openai/gpt-5-mini
restart: unless-stopped
rate-limiter:
image: redis:alpine
ports:
- "6379:6379"
# Required
OPENROUTER_KEY=sk-or-v1-...
# Optional
OPENROUTER_URL=https://openrouter.ai/api/v1
PRIMARY_MODEL=~anthropic/claude-sonnet-latest
FALLBACK_MODELS=google/gemini-2.5-pro,openai/gpt-5-mini
SITE_URL=
SITE_NAME=
APP_NAME=MyApp/1.0
PORT=3000
LOG_LEVEL=info
MAX_TOKENS=4096
TIMEOUT=30000
import { createHistogram, createCounter, createGauge } from 'prom-client';
const requestDuration = createHistogram({
name: 'openrouter_request_duration_seconds',
help: 'Duration of OpenRouter requests',
labelNames: ['model', 'provider'],
buckets: [0.1, 0.5, 1, 2, 5, 10]
});
const tokenUsage = createCounter({
name: 'openrouter_tokens_total',
help: 'Total tokens used',
labelNames: ['model', 'type']
});
const activeRequests = createGauge({
name: 'openrouter_active_requests',
help: 'Number of active requests',
labelNames: ['model']
});
app.use((req, res, next) => {
const start = process.hrtime.bigint();
const model = req.body.model || 'unknown';
const provider = model.split('/')[0];
activeRequests.labels(model).inc();
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1e9;
requestDuration.labels(model, provider).observe(duration);
activeRequests.labels(model).dec();
});
next();
});
app.post('/v1/chat/completions', async (req, res) => {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_KEY}`
},
body: JSON.stringify(req.body)
});
const data = await response.json();
if (data.usage) {
tokenUsage.labels(req.body.model, 'input').inc(data.usage.prompt_tokens);
tokenUsage.labels(req.body.model, 'output').inc(data.usage.completion_tokens);
}
res.json(data);
});
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'openrouter-proxy.log' })
]
});
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
model: req.body.model,
status: res.statusCode,
durationMs: Date.now() - start,
ip: req.ip
});
});
next();
});
Invalid API Key
sk-or-v1-)Insufficient Credits
Model Not Available
/v1/models~anthropic/claude-sonnet-latest)Rate Limit Exceeded
Provider Errors
Site URL Not Configured
SITE_URL and SITE_NAME are set if using custom endpoint# Enable verbose logging
DEBUG=openrouter:* node index.js
# Or log full requests/responses
import fetch from 'node-fetch';
const originalFetch = global.fetch;
global.fetch = async (url: string, options: any) => {
console.log('Request:', url, options);
const response = await originalFetch(url, options);
const cloned = response.clone();
const body = await cloned.json();
console.log('Response:', body);
return response;
};
| Feature | OpenRouter | Bedrock | Vertex AI | Azure ML |
|---|---|---|---|---|
| Multi-provider | ✅ 200+ models | ✅ AWS only | ✅ Google only | ✅ Various |
| Unified API | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Model Ranking | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Custom Endpoints | ✅ Yes | ❌ No | ❌ No | ❌ No |
| OpenAI Compatible | ✅ Yes | ❌ No | ❌ No | ✅ Partially |
| Free Tier | ✅ $0.00015 free | ❌ No | ❌ No | ✅ Yes |
| Pricing | ✅ Pay-as-you-go | ✅ Pay-as-you-go | ✅ Pay-as-you-go | ✅ Pay-as-you-go |
async function complete(prompt: string, model: string = '~anthropic/claude-sonnet-latest') {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }]
})
});
const data = await response.json();
return data.choices[0].message.content;
}
const PROVIDER_MODELS: Record<string, string> = {
anthropic: '~anthropic/claude-sonnet-latest',
google: 'google/gemini-2.5-pro',
openai: 'openai/gpt-5-mini',
mistral: 'mistral/mistral-large'
};
async function completeWithFallback(prompt: string, providers: string[] = ['anthropic', 'google', 'openai']) {
for (const provider of providers) {
const model = PROVIDER_MODELS[provider];
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }]
})
});
if (response.ok) {
const data = await response.json();
return { ...data, provider, model };
}
} catch (error) {
console.log(`Failed with ${provider}, trying next...`);
}
}
throw new Error('All providers failed');
}
async function* streamComplete(prompt: string, model: string) {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
stream: true
})
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
if (chunk.includes('[DONE]')) break;
try {
const data = JSON.parse(chunk.replace(/^data: /, ''));
if (data.choices?.[0]?.delta?.content) {
yield data.choices[0].delta.content;
}
} catch (error) {
// Ignore parse errors
}
}
}
// Usage
for await (const chunk of streamComplete('Tell me a story', '~anthropic/claude-sonnet-latest')) {
process.stdout.write(chunk);
}
async function calculateCost(prompt: string, model: string) {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
max_tokens: 0 // Only count input tokens
})
});
const data = await response.json();
const inputTokens = data.usage?.prompt_tokens || 0;
// Get model pricing
const modelsResponse = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
}
});
const modelsData = await modelsResponse.json();
const modelInfo = modelsData.data.find((m: any) => m.id === model);
const inputPrice = modelInfo?.pricing?.prompt || 0;
const outputPrice = modelInfo?.pricing?.completion || 0;
return {
inputTokens,
inputCost: inputTokens * inputPrice,
outputPricePerToken: outputPrice
};
}
async function findBestModel(task: string, maxPrice?: number) {
const response = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
}
});
const data = await response.json();
// Filter by task and price
const candidates = data.data.filter((model: any) => {
const matchesTask = model.name.toLowerCase().includes(task.toLowerCase());
const matchesPrice = !maxPrice || (model.pricing?.prompt || 0) <= maxPrice;
return matchesTask && matchesPrice;
});
// Sort by ranking
candidates.sort((a: any, b: any) => (a.rank || 999) - (b.rank || 999));
return candidates[0];
}
async function parallelComplete(prompts: string[], model: string) {
const requests = prompts.map(prompt =>
fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }]
})
}).then(r => r.json())
);
const responses = await Promise.all(requests);
return responses.map(r => r.choices[0].message.content);
}
class OpenRouterConversation {
private messages: any[] = [];
private model: string;
constructor(model: string = '~anthropic/claude-sonnet-latest') {
this.model = model;
}
async send(prompt: string) {
this.messages.push({ role: 'user', content: prompt });
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENROUTER_KEY}`
},
body: JSON.stringify({
model: this.model,
messages: this.messages
})
});
const data = await response.json();
const content = data.choices[0].message.content;
this.messages.push({ role: 'assistant', content });
return content;
}
getHistory() {
return [...this.messages];
}
clear() {
this.messages = [];
}
setModel(model: string) {
this.model = model;
}
}
// Usage
const conv = new OpenRouterConversation();
const reply = await conv.send('Hello!');