@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.
Files changed (3) hide show
  1. package/README.md +12 -6
  2. package/mcp/server.mjs +162 -21
  3. 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
  [![npm version](https://img.shields.io/npm/v/@aggc/or-info.svg)](https://www.npmjs.com/package/@aggc/or-info)
9
9
  [![CI](https://github.com/jmtrs/or-info/actions/workflows/ci.yml/badge.svg)](https://github.com/jmtrs/or-info/actions/workflows/ci.yml)
10
+ [![smithery badge](https://smithery.ai/badge/aggc/or-info)](https://smithery.ai/servers/aggc/or-info)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- | `get_model_info` | Pricing, context, architecture, features and LMArena ELO for a model |
184
- | `list_models` | List models with optional filter, sort and limit |
185
- | `get_benchmarks` | LMArena ELO score, global rank, vote count and confidence interval for a model |
186
- | `compare_models` | Side-by-side comparison of two models |
187
- | `best_for_task` | Ranked top models for coding/reasoning/general/vision/cheap |
188
- | `refresh_cache` | Force-refresh OpenRouter catalog + LMArena ELO |
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 TOOLS = [
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: 'get_model_info',
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: 'list_models',
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: 'get_benchmarks',
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: 'compare_models',
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: 'best_for_task',
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: 'refresh_cache',
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 textContent(obj) {
103
- return [{ type: 'text', text: JSON.stringify(obj, null, 2) }];
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 === 'get_model_info') {
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 { content: textContent({ ...safeModelSummary(model), lmarena_elo: elo ?? null }) };
261
+ return result({ ...safeModelSummary(model), lmarena_elo: elo ?? null });
121
262
  }
122
263
 
123
- if (name === 'list_models') {
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 { content: textContent({ total: models.length, models: models.map(safeModelSummary) }) };
279
+ return result({ total: models.length, models: models.map(safeModelSummary) });
139
280
  }
140
281
 
141
- if (name === 'get_benchmarks') {
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 { content: textContent({ model_id, lmarena_elo: elo ?? null }) };
286
+ return result({ model_id, lmarena_elo: elo ?? null });
146
287
  }
147
288
 
148
- if (name === 'compare_models') {
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 { content: textContent({ a: { ...safeModelSummary(mA), lmarena_elo: eloA }, b: { ...safeModelSummary(mB), lmarena_elo: eloB } }) };
301
+ return result({ a: { ...safeModelSummary(mA), lmarena_elo: eloA }, b: { ...safeModelSummary(mB), lmarena_elo: eloB } });
161
302
  }
162
303
 
163
- if (name === 'best_for_task') {
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 { content: textContent({ task, results: ranked.map((r) => ({ ...safeModelSummary(r.model), score: r.score, lmarena_elo: r.eloEntry })) }) };
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 === 'refresh_cache') {
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 { content: textContent({ refreshed: true, models_count: models.length, elo_entries: elo.length }) };
319
+ return result({ refreshed: true, models_count: models.length, elo_entries: elo.length });
179
320
  }
180
321
 
181
322
  return errorContent(`Unknown tool: ${name}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aggc/or-info",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "CLI + MCP server for OpenRouter models: prices, benchmarks, context and comparisons",
5
5
  "type": "module",
6
6
  "engines": {