@aggc/or-info 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/bin/or-info.mjs +4 -1
- package/mcp/server.mjs +96 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,12 +181,17 @@ or-info refresh # Force-refresh OpenRouter catalog + LMArena ELO
|
|
|
181
181
|
|
|
182
182
|
| Tool | Description |
|
|
183
183
|
|------|-------------|
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
184
|
+
| `models.get` | Pricing, context, architecture, features and LMArena ELO for a model |
|
|
185
|
+
| `models.list` | List models with optional filter, sort and limit |
|
|
186
|
+
| `models.compare` | Side-by-side comparison of two models |
|
|
187
|
+
| `models.top` | Ranked top models for coding/reasoning/general/vision/cheap |
|
|
188
|
+
| `benchmarks.get` | LMArena ELO score, global rank, vote count and confidence interval for a model |
|
|
189
|
+
| `cache.refresh` | Force-refresh OpenRouter catalog + LMArena ELO |
|
|
190
|
+
|
|
191
|
+
Legacy flat names (`get_model_info`, `list_models`, `get_benchmarks`,
|
|
192
|
+
`compare_models`, `best_for_task`, `refresh_cache`) are still advertised in
|
|
193
|
+
`tools/list` as deprecated aliases (same schemas, prefixed `[Deprecated]`)
|
|
194
|
+
and remain callable. The dot-notation names are the canonical ones.
|
|
190
195
|
|
|
191
196
|
### Register in Claude Code
|
|
192
197
|
|
package/bin/or-info.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { InvalidArgumentError, program } from 'commander';
|
|
3
4
|
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
4
7
|
import { fetchModels, findModel, pricePerMillion, contextLength } from '../lib/openrouter.mjs';
|
|
5
8
|
import { getElo, getAllElo, loadLeaderboard } from '../lib/lmarena.mjs';
|
|
6
9
|
import { rankModels } from '../lib/scorer.mjs';
|
|
@@ -40,7 +43,7 @@ async function apiKey() {
|
|
|
40
43
|
program
|
|
41
44
|
.name('or-info')
|
|
42
45
|
.description('OpenRouter model info: prices, benchmarks, context and comparisons')
|
|
43
|
-
.version(
|
|
46
|
+
.version(version)
|
|
44
47
|
.option('--mcp', 'Start MCP server (stdio transport)');
|
|
45
48
|
|
|
46
49
|
// ── models ─────────────────────────────────────────────────────────────────
|
package/mcp/server.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
6
|
import { fetchModels, findModel, pricePerMillion, contextLength, modelTags } from '../lib/openrouter.mjs';
|
|
5
7
|
import { getElo, getAllElo, loadLeaderboard } from '../lib/lmarena.mjs';
|
|
6
8
|
import { rankModels } from '../lib/scorer.mjs';
|
|
7
9
|
import { getApiKey } from '../lib/secrets.mjs';
|
|
8
10
|
|
|
11
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
12
|
+
|
|
9
13
|
const MODEL_SUMMARY_SCHEMA = {
|
|
10
14
|
type: 'object',
|
|
11
15
|
properties: {
|
|
@@ -26,9 +30,18 @@ const MODEL_SUMMARY_SCHEMA = {
|
|
|
26
30
|
|
|
27
31
|
const ELO_SCHEMA = { type: ['object', 'null'], description: 'LMArena ELO entry or null when not tracked' };
|
|
28
32
|
|
|
29
|
-
const
|
|
33
|
+
const TOOL_ALIASES = {
|
|
34
|
+
get_model_info: 'models.get',
|
|
35
|
+
list_models: 'models.list',
|
|
36
|
+
compare_models: 'models.compare',
|
|
37
|
+
best_for_task: 'models.top',
|
|
38
|
+
get_benchmarks: 'benchmarks.get',
|
|
39
|
+
refresh_cache: 'cache.refresh',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CANONICAL_TOOLS = [
|
|
30
43
|
{
|
|
31
|
-
name: '
|
|
44
|
+
name: 'models.get',
|
|
32
45
|
description: 'Get pricing, context length, architecture and features for a specific OpenRouter model',
|
|
33
46
|
inputSchema: {
|
|
34
47
|
type: 'object',
|
|
@@ -42,14 +55,14 @@ const TOOLS = [
|
|
|
42
55
|
properties: { ...MODEL_SUMMARY_SCHEMA.properties, lmarena_elo: ELO_SCHEMA },
|
|
43
56
|
},
|
|
44
57
|
annotations: {
|
|
45
|
-
title: 'Get model
|
|
58
|
+
title: 'Get model',
|
|
46
59
|
readOnlyHint: true,
|
|
47
60
|
idempotentHint: true,
|
|
48
61
|
openWorldHint: true,
|
|
49
62
|
},
|
|
50
63
|
},
|
|
51
64
|
{
|
|
52
|
-
name: '
|
|
65
|
+
name: 'models.list',
|
|
53
66
|
description: 'List OpenRouter models with pricing. Optionally filter by name/id, sort, and limit results.',
|
|
54
67
|
inputSchema: {
|
|
55
68
|
type: 'object',
|
|
@@ -76,7 +89,7 @@ const TOOLS = [
|
|
|
76
89
|
},
|
|
77
90
|
},
|
|
78
91
|
{
|
|
79
|
-
name: '
|
|
92
|
+
name: 'benchmarks.get',
|
|
80
93
|
description: 'Get LMArena ELO ranking for a model: score, global rank, vote count and confidence interval',
|
|
81
94
|
inputSchema: {
|
|
82
95
|
type: 'object',
|
|
@@ -94,14 +107,14 @@ const TOOLS = [
|
|
|
94
107
|
required: ['model_id'],
|
|
95
108
|
},
|
|
96
109
|
annotations: {
|
|
97
|
-
title: 'Get
|
|
110
|
+
title: 'Get benchmark',
|
|
98
111
|
readOnlyHint: true,
|
|
99
112
|
idempotentHint: true,
|
|
100
113
|
openWorldHint: true,
|
|
101
114
|
},
|
|
102
115
|
},
|
|
103
116
|
{
|
|
104
|
-
name: '
|
|
117
|
+
name: 'models.compare',
|
|
105
118
|
description: 'Side-by-side comparison of two models: pricing, context, benchmarks and features',
|
|
106
119
|
inputSchema: {
|
|
107
120
|
type: 'object',
|
|
@@ -127,7 +140,7 @@ const TOOLS = [
|
|
|
127
140
|
},
|
|
128
141
|
},
|
|
129
142
|
{
|
|
130
|
-
name: '
|
|
143
|
+
name: 'models.top',
|
|
131
144
|
description: 'Rank the best models for a specific task, optionally within a price budget',
|
|
132
145
|
inputSchema: {
|
|
133
146
|
type: 'object',
|
|
@@ -160,14 +173,14 @@ const TOOLS = [
|
|
|
160
173
|
required: ['task', 'results'],
|
|
161
174
|
},
|
|
162
175
|
annotations: {
|
|
163
|
-
title: '
|
|
176
|
+
title: 'Top models for task',
|
|
164
177
|
readOnlyHint: true,
|
|
165
178
|
idempotentHint: true,
|
|
166
179
|
openWorldHint: true,
|
|
167
180
|
},
|
|
168
181
|
},
|
|
169
182
|
{
|
|
170
|
-
name: '
|
|
183
|
+
name: 'cache.refresh',
|
|
171
184
|
description: 'Force-refresh the local cache: OpenRouter model catalog + LMArena ELO data',
|
|
172
185
|
inputSchema: { type: 'object', properties: {} },
|
|
173
186
|
outputSchema: {
|
|
@@ -188,6 +201,25 @@ const TOOLS = [
|
|
|
188
201
|
},
|
|
189
202
|
];
|
|
190
203
|
|
|
204
|
+
// Legacy flat names kept advertised in tools/list for discoverability,
|
|
205
|
+
// derived from the canonical tools so input/output schemas stay in sync.
|
|
206
|
+
const LEGACY_BY_CANONICAL = Object.fromEntries(
|
|
207
|
+
Object.entries(TOOL_ALIASES).map(([legacy, canonical]) => [canonical, legacy])
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const LEGACY_TOOLS = CANONICAL_TOOLS.flatMap((tool) => {
|
|
211
|
+
const legacyName = LEGACY_BY_CANONICAL[tool.name];
|
|
212
|
+
if (!legacyName) return [];
|
|
213
|
+
return [{
|
|
214
|
+
...tool,
|
|
215
|
+
name: legacyName,
|
|
216
|
+
description: `[Deprecated] Alias of \`${tool.name}\`. Use \`${tool.name}\` instead.`,
|
|
217
|
+
annotations: { ...tool.annotations, title: `[Deprecated] ${tool.annotations.title}` },
|
|
218
|
+
}];
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const TOOLS = [...CANONICAL_TOOLS, ...LEGACY_TOOLS];
|
|
222
|
+
|
|
191
223
|
function safeModelSummary(model) {
|
|
192
224
|
const price = pricePerMillion(model);
|
|
193
225
|
return {
|
|
@@ -218,9 +250,12 @@ function errorContent(msg) {
|
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
async function handleTool(name, args) {
|
|
253
|
+
// Accept legacy flat names (get_model_info, list_models, ...) by mapping
|
|
254
|
+
// them to the dot-notation canonical names exposed in tools/list.
|
|
255
|
+
name = TOOL_ALIASES[name] ?? name;
|
|
221
256
|
const key = await getApiKey();
|
|
222
257
|
|
|
223
|
-
if (name === '
|
|
258
|
+
if (name === 'models.get') {
|
|
224
259
|
const { model_id } = args;
|
|
225
260
|
if (!model_id || typeof model_id !== 'string') return errorContent('model_id is required');
|
|
226
261
|
const models = await fetchModels({ apiKey: key });
|
|
@@ -230,7 +265,7 @@ async function handleTool(name, args) {
|
|
|
230
265
|
return result({ ...safeModelSummary(model), lmarena_elo: elo ?? null });
|
|
231
266
|
}
|
|
232
267
|
|
|
233
|
-
if (name === '
|
|
268
|
+
if (name === 'models.list') {
|
|
234
269
|
const filter = String(args.filter ?? '').toLowerCase();
|
|
235
270
|
const sortBy = args.sort_by ?? 'name';
|
|
236
271
|
const limit = Math.min(200, Math.max(1, args.limit ?? 50));
|
|
@@ -248,14 +283,14 @@ async function handleTool(name, args) {
|
|
|
248
283
|
return result({ total: models.length, models: models.map(safeModelSummary) });
|
|
249
284
|
}
|
|
250
285
|
|
|
251
|
-
if (name === '
|
|
286
|
+
if (name === 'benchmarks.get') {
|
|
252
287
|
const { model_id } = args;
|
|
253
288
|
if (!model_id || typeof model_id !== 'string') return errorContent('model_id is required');
|
|
254
289
|
const elo = await getElo(model_id);
|
|
255
290
|
return result({ model_id, lmarena_elo: elo ?? null });
|
|
256
291
|
}
|
|
257
292
|
|
|
258
|
-
if (name === '
|
|
293
|
+
if (name === 'models.compare') {
|
|
259
294
|
const { model_a, model_b } = args;
|
|
260
295
|
if (!model_a || !model_b) return errorContent('model_a and model_b are required');
|
|
261
296
|
const [models, eloA, eloB] = await Promise.all([
|
|
@@ -270,7 +305,7 @@ async function handleTool(name, args) {
|
|
|
270
305
|
return result({ a: { ...safeModelSummary(mA), lmarena_elo: eloA }, b: { ...safeModelSummary(mB), lmarena_elo: eloB } });
|
|
271
306
|
}
|
|
272
307
|
|
|
273
|
-
if (name === '
|
|
308
|
+
if (name === 'models.top') {
|
|
274
309
|
const task = args.task ?? 'general';
|
|
275
310
|
const limit = Math.min(20, Math.max(1, args.limit ?? 5));
|
|
276
311
|
const maxPrice = args.max_price_per_m_output ?? undefined;
|
|
@@ -280,7 +315,7 @@ async function handleTool(name, args) {
|
|
|
280
315
|
return result({ task, results: ranked.map((r) => ({ ...safeModelSummary(r.model), score: r.score, lmarena_elo: r.eloEntry })) });
|
|
281
316
|
}
|
|
282
317
|
|
|
283
|
-
if (name === '
|
|
318
|
+
if (name === 'cache.refresh') {
|
|
284
319
|
const [models, elo] = await Promise.all([
|
|
285
320
|
fetchModels({ force: true, apiKey: key }),
|
|
286
321
|
loadLeaderboard({ force: true }),
|
|
@@ -291,14 +326,15 @@ async function handleTool(name, args) {
|
|
|
291
326
|
return errorContent(`Unknown tool: ${name}`);
|
|
292
327
|
}
|
|
293
328
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
{ name: 'or-info', version
|
|
329
|
+
function makeServer() {
|
|
330
|
+
return new Server(
|
|
331
|
+
{ name: 'or-info', version },
|
|
297
332
|
{ capabilities: { tools: {} } }
|
|
298
333
|
);
|
|
334
|
+
}
|
|
299
335
|
|
|
336
|
+
function wireHandlers(server) {
|
|
300
337
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
301
|
-
|
|
302
338
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
303
339
|
const { name, arguments: args } = req.params;
|
|
304
340
|
try {
|
|
@@ -308,6 +344,11 @@ export async function startMcp() {
|
|
|
308
344
|
return errorContent(safe);
|
|
309
345
|
}
|
|
310
346
|
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function startMcp() {
|
|
350
|
+
const server = makeServer();
|
|
351
|
+
wireHandlers(server);
|
|
311
352
|
|
|
312
353
|
const transport = new StdioServerTransport();
|
|
313
354
|
await server.connect(transport);
|
|
@@ -321,3 +362,38 @@ export async function startMcp() {
|
|
|
321
362
|
});
|
|
322
363
|
}
|
|
323
364
|
}
|
|
365
|
+
|
|
366
|
+
export async function startHttpMcp() {
|
|
367
|
+
const { createServer } = await import('node:http');
|
|
368
|
+
const port = Number(process.env.PORT) || 8000;
|
|
369
|
+
|
|
370
|
+
// Bridge config values Smithery may inject from smithery.yaml configSchema.
|
|
371
|
+
// Smithery passes schema properties as-is or uppercased depending on version.
|
|
372
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
373
|
+
process.env.OPENROUTER_API_KEY = process.env.api_key ?? process.env.API_KEY ?? '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
377
|
+
const server = makeServer();
|
|
378
|
+
wireHandlers(server);
|
|
379
|
+
await server.connect(transport);
|
|
380
|
+
|
|
381
|
+
const serverCard = JSON.stringify({
|
|
382
|
+
serverInfo: { name: 'or-info', version },
|
|
383
|
+
tools: CANONICAL_TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
createServer(async (req, res) => {
|
|
387
|
+
if (req.url?.startsWith('/mcp')) {
|
|
388
|
+
await transport.handleRequest(req, res);
|
|
389
|
+
} else if (req.method === 'GET' && req.url === '/.well-known/mcp/server-card.json') {
|
|
390
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
391
|
+
res.end(serverCard);
|
|
392
|
+
} else {
|
|
393
|
+
res.writeHead(404);
|
|
394
|
+
res.end();
|
|
395
|
+
}
|
|
396
|
+
}).listen(port, () => {
|
|
397
|
+
process.stderr.write(`or-info HTTP MCP listening on port ${port}\n`);
|
|
398
|
+
});
|
|
399
|
+
}
|