@aggc/or-info 0.2.6 → 0.2.8
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 +12 -6
- package/mcp/server.mjs +162 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ to make informed decisions about which model to use.
|
|
|
7
7
|
|
|
8
8
|
[](https://www.npmjs.com/package/@aggc/or-info)
|
|
9
9
|
[](https://github.com/jmtrs/or-info/actions/workflows/ci.yml)
|
|
10
|
+
[](https://smithery.ai/servers/aggc/or-info)
|
|
10
11
|
[](LICENSE)
|
|
11
12
|
|
|
12
13
|
## Install
|
|
@@ -180,12 +181,17 @@ or-info refresh # Force-refresh OpenRouter catalog + LMArena ELO
|
|
|
180
181
|
|
|
181
182
|
| Tool | Description |
|
|
182
183
|
|------|-------------|
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
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.
|
|
189
195
|
|
|
190
196
|
### Register in Claude Code
|
|
191
197
|
|
package/mcp/server.mjs
CHANGED
|
@@ -6,9 +6,38 @@ import { getElo, getAllElo, loadLeaderboard } from '../lib/lmarena.mjs';
|
|
|
6
6
|
import { rankModels } from '../lib/scorer.mjs';
|
|
7
7
|
import { getApiKey } from '../lib/secrets.mjs';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const MODEL_SUMMARY_SCHEMA = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'string' },
|
|
13
|
+
name: { type: 'string' },
|
|
14
|
+
input_per_m: { type: ['number', 'null'], description: 'Input price per 1M tokens (USD)' },
|
|
15
|
+
output_per_m: { type: ['number', 'null'], description: 'Output price per 1M tokens (USD)' },
|
|
16
|
+
image_per_m: { type: ['number', 'null'] },
|
|
17
|
+
cache_read_per_m: { type: ['number', 'null'] },
|
|
18
|
+
context_length: { type: ['integer', 'null'] },
|
|
19
|
+
features: { type: 'array', items: { type: 'string' } },
|
|
20
|
+
modality: { type: ['string', 'null'] },
|
|
21
|
+
tokenizer: { type: ['string', 'null'] },
|
|
22
|
+
max_output_tokens: { type: ['integer', 'null'] },
|
|
23
|
+
supported_parameters: { type: 'array', items: { type: 'string' } },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ELO_SCHEMA = { type: ['object', 'null'], description: 'LMArena ELO entry or null when not tracked' };
|
|
28
|
+
|
|
29
|
+
const TOOL_ALIASES = {
|
|
30
|
+
get_model_info: 'models.get',
|
|
31
|
+
list_models: 'models.list',
|
|
32
|
+
compare_models: 'models.compare',
|
|
33
|
+
best_for_task: 'models.top',
|
|
34
|
+
get_benchmarks: 'benchmarks.get',
|
|
35
|
+
refresh_cache: 'cache.refresh',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CANONICAL_TOOLS = [
|
|
10
39
|
{
|
|
11
|
-
name: '
|
|
40
|
+
name: 'models.get',
|
|
12
41
|
description: 'Get pricing, context length, architecture and features for a specific OpenRouter model',
|
|
13
42
|
inputSchema: {
|
|
14
43
|
type: 'object',
|
|
@@ -17,9 +46,19 @@ const TOOLS = [
|
|
|
17
46
|
},
|
|
18
47
|
required: ['model_id'],
|
|
19
48
|
},
|
|
49
|
+
outputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { ...MODEL_SUMMARY_SCHEMA.properties, lmarena_elo: ELO_SCHEMA },
|
|
52
|
+
},
|
|
53
|
+
annotations: {
|
|
54
|
+
title: 'Get model',
|
|
55
|
+
readOnlyHint: true,
|
|
56
|
+
idempotentHint: true,
|
|
57
|
+
openWorldHint: true,
|
|
58
|
+
},
|
|
20
59
|
},
|
|
21
60
|
{
|
|
22
|
-
name: '
|
|
61
|
+
name: 'models.list',
|
|
23
62
|
description: 'List OpenRouter models with pricing. Optionally filter by name/id, sort, and limit results.',
|
|
24
63
|
inputSchema: {
|
|
25
64
|
type: 'object',
|
|
@@ -30,9 +69,23 @@ const TOOLS = [
|
|
|
30
69
|
free_only: { type: 'boolean', description: 'Return only free models' },
|
|
31
70
|
},
|
|
32
71
|
},
|
|
72
|
+
outputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
total: { type: 'integer' },
|
|
76
|
+
models: { type: 'array', items: MODEL_SUMMARY_SCHEMA },
|
|
77
|
+
},
|
|
78
|
+
required: ['total', 'models'],
|
|
79
|
+
},
|
|
80
|
+
annotations: {
|
|
81
|
+
title: 'List models',
|
|
82
|
+
readOnlyHint: true,
|
|
83
|
+
idempotentHint: true,
|
|
84
|
+
openWorldHint: true,
|
|
85
|
+
},
|
|
33
86
|
},
|
|
34
87
|
{
|
|
35
|
-
name: '
|
|
88
|
+
name: 'benchmarks.get',
|
|
36
89
|
description: 'Get LMArena ELO ranking for a model: score, global rank, vote count and confidence interval',
|
|
37
90
|
inputSchema: {
|
|
38
91
|
type: 'object',
|
|
@@ -41,9 +94,23 @@ const TOOLS = [
|
|
|
41
94
|
},
|
|
42
95
|
required: ['model_id'],
|
|
43
96
|
},
|
|
97
|
+
outputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
model_id: { type: 'string' },
|
|
101
|
+
lmarena_elo: ELO_SCHEMA,
|
|
102
|
+
},
|
|
103
|
+
required: ['model_id'],
|
|
104
|
+
},
|
|
105
|
+
annotations: {
|
|
106
|
+
title: 'Get benchmark',
|
|
107
|
+
readOnlyHint: true,
|
|
108
|
+
idempotentHint: true,
|
|
109
|
+
openWorldHint: true,
|
|
110
|
+
},
|
|
44
111
|
},
|
|
45
112
|
{
|
|
46
|
-
name: '
|
|
113
|
+
name: 'models.compare',
|
|
47
114
|
description: 'Side-by-side comparison of two models: pricing, context, benchmarks and features',
|
|
48
115
|
inputSchema: {
|
|
49
116
|
type: 'object',
|
|
@@ -53,9 +120,23 @@ const TOOLS = [
|
|
|
53
120
|
},
|
|
54
121
|
required: ['model_a', 'model_b'],
|
|
55
122
|
},
|
|
123
|
+
outputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
a: { type: 'object', properties: { ...MODEL_SUMMARY_SCHEMA.properties, lmarena_elo: ELO_SCHEMA } },
|
|
127
|
+
b: { type: 'object', properties: { ...MODEL_SUMMARY_SCHEMA.properties, lmarena_elo: ELO_SCHEMA } },
|
|
128
|
+
},
|
|
129
|
+
required: ['a', 'b'],
|
|
130
|
+
},
|
|
131
|
+
annotations: {
|
|
132
|
+
title: 'Compare models',
|
|
133
|
+
readOnlyHint: true,
|
|
134
|
+
idempotentHint: true,
|
|
135
|
+
openWorldHint: true,
|
|
136
|
+
},
|
|
56
137
|
},
|
|
57
138
|
{
|
|
58
|
-
name: '
|
|
139
|
+
name: 'models.top',
|
|
59
140
|
description: 'Rank the best models for a specific task, optionally within a price budget',
|
|
60
141
|
inputSchema: {
|
|
61
142
|
type: 'object',
|
|
@@ -73,14 +154,68 @@ const TOOLS = [
|
|
|
73
154
|
},
|
|
74
155
|
required: ['task'],
|
|
75
156
|
},
|
|
157
|
+
outputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
task: { type: 'string' },
|
|
161
|
+
results: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: { ...MODEL_SUMMARY_SCHEMA.properties, score: { type: 'number' }, lmarena_elo: ELO_SCHEMA },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ['task', 'results'],
|
|
170
|
+
},
|
|
171
|
+
annotations: {
|
|
172
|
+
title: 'Top models for task',
|
|
173
|
+
readOnlyHint: true,
|
|
174
|
+
idempotentHint: true,
|
|
175
|
+
openWorldHint: true,
|
|
176
|
+
},
|
|
76
177
|
},
|
|
77
178
|
{
|
|
78
|
-
name: '
|
|
179
|
+
name: 'cache.refresh',
|
|
79
180
|
description: 'Force-refresh the local cache: OpenRouter model catalog + LMArena ELO data',
|
|
80
181
|
inputSchema: { type: 'object', properties: {} },
|
|
182
|
+
outputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
refreshed: { type: 'boolean' },
|
|
186
|
+
models_count: { type: 'integer' },
|
|
187
|
+
elo_entries: { type: 'integer' },
|
|
188
|
+
},
|
|
189
|
+
required: ['refreshed', 'models_count', 'elo_entries'],
|
|
190
|
+
},
|
|
191
|
+
annotations: {
|
|
192
|
+
title: 'Refresh cache',
|
|
193
|
+
readOnlyHint: false,
|
|
194
|
+
idempotentHint: true,
|
|
195
|
+
openWorldHint: true,
|
|
196
|
+
},
|
|
81
197
|
},
|
|
82
198
|
];
|
|
83
199
|
|
|
200
|
+
// Legacy flat names kept advertised in tools/list for discoverability,
|
|
201
|
+
// derived from the canonical tools so input/output schemas stay in sync.
|
|
202
|
+
const LEGACY_BY_CANONICAL = Object.fromEntries(
|
|
203
|
+
Object.entries(TOOL_ALIASES).map(([legacy, canonical]) => [canonical, legacy])
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const LEGACY_TOOLS = CANONICAL_TOOLS.flatMap((tool) => {
|
|
207
|
+
const legacyName = LEGACY_BY_CANONICAL[tool.name];
|
|
208
|
+
if (!legacyName) return [];
|
|
209
|
+
return [{
|
|
210
|
+
...tool,
|
|
211
|
+
name: legacyName,
|
|
212
|
+
description: `[Deprecated] Alias of \`${tool.name}\`. Use \`${tool.name}\` instead.`,
|
|
213
|
+
annotations: { ...tool.annotations, title: `[Deprecated] ${tool.annotations.title}` },
|
|
214
|
+
}];
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const TOOLS = [...CANONICAL_TOOLS, ...LEGACY_TOOLS];
|
|
218
|
+
|
|
84
219
|
function safeModelSummary(model) {
|
|
85
220
|
const price = pricePerMillion(model);
|
|
86
221
|
return {
|
|
@@ -99,8 +234,11 @@ function safeModelSummary(model) {
|
|
|
99
234
|
};
|
|
100
235
|
}
|
|
101
236
|
|
|
102
|
-
function
|
|
103
|
-
return
|
|
237
|
+
function result(obj) {
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }],
|
|
240
|
+
structuredContent: obj,
|
|
241
|
+
};
|
|
104
242
|
}
|
|
105
243
|
|
|
106
244
|
function errorContent(msg) {
|
|
@@ -108,19 +246,22 @@ function errorContent(msg) {
|
|
|
108
246
|
}
|
|
109
247
|
|
|
110
248
|
async function handleTool(name, args) {
|
|
249
|
+
// Accept legacy flat names (get_model_info, list_models, ...) by mapping
|
|
250
|
+
// them to the dot-notation canonical names exposed in tools/list.
|
|
251
|
+
name = TOOL_ALIASES[name] ?? name;
|
|
111
252
|
const key = await getApiKey();
|
|
112
253
|
|
|
113
|
-
if (name === '
|
|
254
|
+
if (name === 'models.get') {
|
|
114
255
|
const { model_id } = args;
|
|
115
256
|
if (!model_id || typeof model_id !== 'string') return errorContent('model_id is required');
|
|
116
257
|
const models = await fetchModels({ apiKey: key });
|
|
117
258
|
const model = findModel(models, model_id);
|
|
118
259
|
if (!model) return errorContent(`Model not found: ${model_id}`);
|
|
119
260
|
const elo = await getElo(model_id);
|
|
120
|
-
return
|
|
261
|
+
return result({ ...safeModelSummary(model), lmarena_elo: elo ?? null });
|
|
121
262
|
}
|
|
122
263
|
|
|
123
|
-
if (name === '
|
|
264
|
+
if (name === 'models.list') {
|
|
124
265
|
const filter = String(args.filter ?? '').toLowerCase();
|
|
125
266
|
const sortBy = args.sort_by ?? 'name';
|
|
126
267
|
const limit = Math.min(200, Math.max(1, args.limit ?? 50));
|
|
@@ -135,17 +276,17 @@ async function handleTool(name, args) {
|
|
|
135
276
|
else models.sort((a, b) => a.id.localeCompare(b.id));
|
|
136
277
|
|
|
137
278
|
models = models.slice(0, limit);
|
|
138
|
-
return
|
|
279
|
+
return result({ total: models.length, models: models.map(safeModelSummary) });
|
|
139
280
|
}
|
|
140
281
|
|
|
141
|
-
if (name === '
|
|
282
|
+
if (name === 'benchmarks.get') {
|
|
142
283
|
const { model_id } = args;
|
|
143
284
|
if (!model_id || typeof model_id !== 'string') return errorContent('model_id is required');
|
|
144
285
|
const elo = await getElo(model_id);
|
|
145
|
-
return
|
|
286
|
+
return result({ model_id, lmarena_elo: elo ?? null });
|
|
146
287
|
}
|
|
147
288
|
|
|
148
|
-
if (name === '
|
|
289
|
+
if (name === 'models.compare') {
|
|
149
290
|
const { model_a, model_b } = args;
|
|
150
291
|
if (!model_a || !model_b) return errorContent('model_a and model_b are required');
|
|
151
292
|
const [models, eloA, eloB] = await Promise.all([
|
|
@@ -157,25 +298,25 @@ async function handleTool(name, args) {
|
|
|
157
298
|
const mB = findModel(models, model_b);
|
|
158
299
|
if (!mA) return errorContent(`Model not found: ${model_a}`);
|
|
159
300
|
if (!mB) return errorContent(`Model not found: ${model_b}`);
|
|
160
|
-
return
|
|
301
|
+
return result({ a: { ...safeModelSummary(mA), lmarena_elo: eloA }, b: { ...safeModelSummary(mB), lmarena_elo: eloB } });
|
|
161
302
|
}
|
|
162
303
|
|
|
163
|
-
if (name === '
|
|
304
|
+
if (name === 'models.top') {
|
|
164
305
|
const task = args.task ?? 'general';
|
|
165
306
|
const limit = Math.min(20, Math.max(1, args.limit ?? 5));
|
|
166
307
|
const maxPrice = args.max_price_per_m_output ?? undefined;
|
|
167
308
|
|
|
168
309
|
const [models, allElo] = await Promise.all([fetchModels({ apiKey: key }), getAllElo()]);
|
|
169
310
|
const ranked = rankModels(models, allElo, { task, maxPricePerMOutput: maxPrice, limit });
|
|
170
|
-
return
|
|
311
|
+
return result({ task, results: ranked.map((r) => ({ ...safeModelSummary(r.model), score: r.score, lmarena_elo: r.eloEntry })) });
|
|
171
312
|
}
|
|
172
313
|
|
|
173
|
-
if (name === '
|
|
314
|
+
if (name === 'cache.refresh') {
|
|
174
315
|
const [models, elo] = await Promise.all([
|
|
175
316
|
fetchModels({ force: true, apiKey: key }),
|
|
176
317
|
loadLeaderboard({ force: true }),
|
|
177
318
|
]);
|
|
178
|
-
return
|
|
319
|
+
return result({ refreshed: true, models_count: models.length, elo_entries: elo.length });
|
|
179
320
|
}
|
|
180
321
|
|
|
181
322
|
return errorContent(`Unknown tool: ${name}`);
|