@aggc/or-info 0.2.8 → 0.2.10
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/bin/or-info.mjs +5 -2
- package/lib/openrouter.mjs +11 -1
- package/mcp/server.mjs +79 -9
- package/package.json +1 -1
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 ─────────────────────────────────────────────────────────────────
|
|
@@ -105,7 +108,7 @@ program
|
|
|
105
108
|
|
|
106
109
|
if (opts.json) {
|
|
107
110
|
console.log(JSON.stringify({
|
|
108
|
-
|
|
111
|
+
id: model.id,
|
|
109
112
|
pricing: model.pricing,
|
|
110
113
|
context_length: contextLength(model),
|
|
111
114
|
}, null, 2));
|
package/lib/openrouter.mjs
CHANGED
|
@@ -100,7 +100,17 @@ export function supportsFeature(model, feature) {
|
|
|
100
100
|
const featureMap = {
|
|
101
101
|
reasoning: ['include_reasoning', 'reasoning'],
|
|
102
102
|
tools: ['tools', 'tool_choice'],
|
|
103
|
-
vision: () =>
|
|
103
|
+
vision: () => {
|
|
104
|
+
// Prefer the canonical modality string (e.g. "text+image->text") because
|
|
105
|
+
// input_modalities is inconsistently populated by OpenRouter providers.
|
|
106
|
+
const modality = model?.architecture?.modality ?? '';
|
|
107
|
+
if (modality) {
|
|
108
|
+
return modality.split('->')[0].split('+').map((s) => s.trim()).includes('image');
|
|
109
|
+
}
|
|
110
|
+
const inputMods = model?.architecture?.input_modalities;
|
|
111
|
+
if (Array.isArray(inputMods)) return inputMods.includes('image');
|
|
112
|
+
return false;
|
|
113
|
+
},
|
|
104
114
|
structured: ['structured_outputs'],
|
|
105
115
|
};
|
|
106
116
|
const check = featureMap[feature];
|
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: {
|
|
@@ -322,33 +326,99 @@ async function handleTool(name, args) {
|
|
|
322
326
|
return errorContent(`Unknown tool: ${name}`);
|
|
323
327
|
}
|
|
324
328
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
{ name: 'or-info', version
|
|
329
|
+
function makeServer() {
|
|
330
|
+
return new Server(
|
|
331
|
+
{ name: 'or-info', version },
|
|
328
332
|
{ capabilities: { tools: {} } }
|
|
329
333
|
);
|
|
334
|
+
}
|
|
330
335
|
|
|
336
|
+
function wireHandlers(server) {
|
|
331
337
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
338
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
339
|
+
const { name, arguments: args } = req.params;
|
|
340
|
+
try {
|
|
341
|
+
return await handleTool(name, args ?? {});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
const safe = err.message?.replace(/sk-[a-zA-Z0-9-]+/g, '[REDACTED]') ?? 'Unexpected error';
|
|
344
|
+
return errorContent(safe);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function startMcp() {
|
|
350
|
+
// Track in-flight tool calls so we don't exit while a response is still being written.
|
|
351
|
+
// Race condition: stdin EOF fires before the async handleTool completes, causing
|
|
352
|
+
// process.exit(0) to kill the process before the MCP SDK writes the response to stdout.
|
|
353
|
+
let pending = 0;
|
|
354
|
+
let stdinEnded = false;
|
|
355
|
+
let resolveWhenDone;
|
|
356
|
+
const donePromise = new Promise((res) => { resolveWhenDone = res; });
|
|
332
357
|
|
|
358
|
+
function checkDone() {
|
|
359
|
+
if (stdinEnded && pending === 0) resolveWhenDone();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const server = makeServer();
|
|
363
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
333
364
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
334
365
|
const { name, arguments: args } = req.params;
|
|
366
|
+
pending++;
|
|
335
367
|
try {
|
|
336
368
|
return await handleTool(name, args ?? {});
|
|
337
369
|
} catch (err) {
|
|
338
370
|
const safe = err.message?.replace(/sk-[a-zA-Z0-9-]+/g, '[REDACTED]') ?? 'Unexpected error';
|
|
339
371
|
return errorContent(safe);
|
|
372
|
+
} finally {
|
|
373
|
+
pending--;
|
|
374
|
+
// Defer checkDone by one tick so the SDK's response-write microtask runs first.
|
|
375
|
+
setImmediate(checkDone);
|
|
340
376
|
}
|
|
341
377
|
});
|
|
342
378
|
|
|
343
379
|
const transport = new StdioServerTransport();
|
|
344
380
|
await server.connect(transport);
|
|
345
381
|
|
|
346
|
-
// server.connect() returns immediately after wiring up the transport.
|
|
347
|
-
// Block here until stdin closes so the process stays alive while serving.
|
|
348
382
|
if (!process.stdin.destroyed) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
383
|
+
process.stdin.once('close', () => { stdinEnded = true; checkDone(); });
|
|
384
|
+
process.stdin.once('end', () => { stdinEnded = true; checkDone(); });
|
|
385
|
+
await donePromise;
|
|
386
|
+
}
|
|
387
|
+
// One extra tick for any buffered stdout writes before the caller calls process.exit().
|
|
388
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function startHttpMcp() {
|
|
392
|
+
const { createServer } = await import('node:http');
|
|
393
|
+
const port = Number(process.env.PORT) || 8000;
|
|
394
|
+
|
|
395
|
+
// Bridge config values Smithery may inject from smithery.yaml configSchema.
|
|
396
|
+
// Smithery passes schema properties as-is or uppercased depending on version.
|
|
397
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
398
|
+
process.env.OPENROUTER_API_KEY = process.env.api_key ?? process.env.API_KEY ?? '';
|
|
353
399
|
}
|
|
400
|
+
|
|
401
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
402
|
+
const server = makeServer();
|
|
403
|
+
wireHandlers(server);
|
|
404
|
+
await server.connect(transport);
|
|
405
|
+
|
|
406
|
+
const serverCard = JSON.stringify({
|
|
407
|
+
serverInfo: { name: 'or-info', version },
|
|
408
|
+
tools: CANONICAL_TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
createServer(async (req, res) => {
|
|
412
|
+
if (req.url?.startsWith('/mcp')) {
|
|
413
|
+
await transport.handleRequest(req, res);
|
|
414
|
+
} else if (req.method === 'GET' && req.url === '/.well-known/mcp/server-card.json') {
|
|
415
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
416
|
+
res.end(serverCard);
|
|
417
|
+
} else {
|
|
418
|
+
res.writeHead(404);
|
|
419
|
+
res.end();
|
|
420
|
+
}
|
|
421
|
+
}).listen(port, () => {
|
|
422
|
+
process.stderr.write(`or-info HTTP MCP listening on port ${port}\n`);
|
|
423
|
+
});
|
|
354
424
|
}
|