@jameslovespancakes/pi-plus 1.0.0 → 1.0.1

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 (40) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +190 -190
  3. package/config/pi-plus.example.json +60 -60
  4. package/config/skills/model-routing/SKILL.md +86 -86
  5. package/images/pi-plus.svg +10 -10
  6. package/package.json +67 -67
  7. package/server/board-server.mjs +641 -641
  8. package/server/package.json +17 -17
  9. package/src/core/accounts/registry.ts +93 -93
  10. package/src/core/anthropic/client-identity.ts +241 -241
  11. package/src/core/catalog/quality.ts +314 -314
  12. package/src/core/config.ts +169 -169
  13. package/src/core/env.ts +58 -58
  14. package/src/core/exec/process.ts +146 -146
  15. package/src/core/exec/ssh-config.ts +157 -157
  16. package/src/core/policy/policy.ts +183 -183
  17. package/src/core/quota/pool.ts +64 -64
  18. package/src/core/quota/usage-source.ts +289 -289
  19. package/src/core/store.ts +43 -43
  20. package/src/domains/agents/board-setup.ts +409 -409
  21. package/src/domains/agents/index.ts +462 -462
  22. package/src/domains/models/catalog-tool.ts +361 -361
  23. package/src/domains/models/index.ts +14 -14
  24. package/src/domains/models/policy-gate.ts +169 -169
  25. package/src/domains/models/provider-picker.ts +207 -207
  26. package/src/domains/remote/config-path.ts +41 -41
  27. package/src/domains/remote/index.ts +866 -866
  28. package/src/domains/remote/setup.ts +425 -425
  29. package/src/domains/setup/index.ts +220 -220
  30. package/src/domains/subscriptions/accounts.ts +242 -242
  31. package/src/domains/subscriptions/footer.ts +182 -182
  32. package/src/domains/subscriptions/index.ts +42 -42
  33. package/src/domains/subscriptions/provider.ts +219 -219
  34. package/src/domains/subscriptions/providers/anthropic.ts +149 -149
  35. package/src/domains/subscriptions/providers/codex.ts +148 -148
  36. package/src/domains/subscriptions/routing.ts +72 -72
  37. package/src/services/usage-service.ts +186 -186
  38. package/src/ui/format.ts +73 -73
  39. package/src/ui/usage-bars.ts +154 -154
  40. package/src/vendor/anthropic.ts +109 -109
@@ -1,361 +1,361 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { StringEnum } from "@earendil-works/pi-ai";
3
- import { Type } from "typebox";
4
- import {
5
- EVALUATIONS,
6
- type EvaluationKey,
7
- costEfficiency,
8
- normalizedScore,
9
- qualityAgeMs,
10
- qualityFor,
11
- qualityRecordCount,
12
- qualityStatus,
13
- refreshQuality,
14
- } from "../../core/catalog/quality.ts";
15
- import { ensureFresh, usageState } from "../../services/usage-service.ts";
16
- import { env, isFromProcessEnv, maskSecret, setEnv } from "../../core/env.ts";
17
- import { fitId } from "../../ui/format.ts";
18
-
19
- /**
20
- * Exposes the Artificial Analysis benchmark catalogue to the model so it can
21
- * pick a model per task instead of relying on fixed small/medium/big profiles.
22
- */
23
-
24
- const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding"]);
25
-
26
- type SortKey = "coding" | "intelligence" | "agentic" | "reasoning" | "cost" | "speed" | "cost_efficiency";
27
-
28
- const SORT_ACCESSORS: Record<SortKey, (entry: Entry) => number> = {
29
- coding: (entry) => entry.scores.artificial_analysis_coding_index ?? -1,
30
- intelligence: (entry) => entry.scores.artificial_analysis_intelligence_index ?? -1,
31
- agentic: (entry) => entry.scores.terminalbench_hard ?? entry.scores.terminalbench_v2_1 ?? entry.scores.tau2 ?? -1,
32
- reasoning: (entry) => entry.scores.gpqa ?? entry.scores.hle ?? -1,
33
- cost: (entry) => -(entry.pricing.blended3to1Per1M ?? Number.MAX_SAFE_INTEGER),
34
- speed: (entry) => entry.performance.outputTokensPerSecond ?? -1,
35
- cost_efficiency: (entry) => entry.efficiency.codingPerDollar ?? entry.efficiency.intelligencePerDollar ?? -1,
36
- };
37
-
38
- interface Entry {
39
- id: string;
40
- provider: string;
41
- billing: "subscription" | "metered";
42
- quotaLeftPercent?: number;
43
- contextWindow: number;
44
- maxTokens: number;
45
- reasoning: boolean;
46
- confidence: string;
47
- basis: string;
48
- benchmarkName?: string;
49
- releaseDate?: string;
50
- scores: Partial<Record<EvaluationKey, number>>;
51
- pricing: { inputPer1M?: number; outputPer1M?: number; blended3to1Per1M?: number };
52
- performance: { outputTokensPerSecond?: number; timeToFirstTokenSeconds?: number };
53
- efficiency: { intelligencePerDollar?: number; codingPerDollar?: number; agenticPerDollar?: number };
54
- }
55
-
56
- function quotaFor(provider: string): number | undefined {
57
- const rows = usageState().rows;
58
- const match = provider === "anthropic"
59
- ? rows.find((row) => row.group.includes("pool") && row.label === "5h")
60
- ?? rows.find((row) => row.group.startsWith("Claude ") && row.label === "5h")
61
- : provider === "openai-codex"
62
- ? rows.find((row) => row.group === "Codex" && row.label === "weekly")
63
- : undefined;
64
- return match ? Math.round(match.remaining) : undefined;
65
- }
66
-
67
- function buildEntry(model: any): Entry {
68
- const lookup = qualityFor(`${model.provider}/${model.id}`);
69
- const record = lookup.record;
70
- const scores: Partial<Record<EvaluationKey, number>> = {};
71
- for (const { key } of EVALUATIONS) {
72
- const value = normalizedScore(key, record?.evaluations[key]);
73
- if (value !== undefined) scores[key] = Number(value.toFixed(1));
74
- }
75
- return {
76
- id: `${model.provider}/${model.id}`,
77
- provider: model.provider,
78
- billing: SUBSCRIPTION_PROVIDERS.has(model.provider) ? "subscription" : "metered",
79
- quotaLeftPercent: quotaFor(model.provider),
80
- contextWindow: model.contextWindow,
81
- maxTokens: model.maxTokens,
82
- reasoning: !!model.reasoning,
83
- confidence: lookup.confidence,
84
- basis: lookup.basis,
85
- benchmarkName: record?.name,
86
- releaseDate: record?.releaseDate,
87
- scores,
88
- pricing: record?.pricing ?? {},
89
- performance: {
90
- outputTokensPerSecond: record?.performance.outputTokensPerSecond,
91
- timeToFirstTokenSeconds: record?.performance.timeToFirstTokenSeconds,
92
- },
93
- efficiency: costEfficiency(record),
94
- };
95
- }
96
-
97
- /**
98
- * OpenRouter republishes the same underlying model many times (`:batch`,
99
- * `:free`, `-pro`, `~` prefixes). Collapse them to the best single row unless
100
- * the caller explicitly asked for variants.
101
- */
102
- function dedupe(entries: Entry[]): Entry[] {
103
- const best = new Map<string, Entry>();
104
- for (const entry of entries) {
105
- // Unrated models share one basis string, so they must key on their own id
106
- // or they would collapse into a single row and disappear from the catalogue.
107
- const family = entry.confidence === "unrated"
108
- ? `unrated:${entry.id}`
109
- : entry.basis.replace(/^nearest match /, "");
110
- const current = best.get(family);
111
- if (!current) {
112
- best.set(family, entry);
113
- continue;
114
- }
115
- const better = entry.billing === "subscription" && current.billing !== "subscription"
116
- || (entry.billing === current.billing && entry.confidence === "measured" && current.confidence !== "measured")
117
- || (entry.billing === current.billing && entry.confidence === current.confidence && entry.id.length < current.id.length);
118
- if (better) best.set(family, entry);
119
- }
120
- return [...best.values()];
121
- }
122
-
123
- function formatTable(entries: Entry[]): string {
124
- const cell = (value: number | undefined, width = 6) =>
125
- (value === undefined ? "-".padStart(width) : value.toFixed(1).padStart(width));
126
- const header = [
127
- "model".padEnd(40),
128
- "bill".padEnd(4),
129
- "quota".padStart(5),
130
- "intel".padStart(6),
131
- "code".padStart(6),
132
- "tbHard".padStart(6),
133
- "tb2.1".padStart(6),
134
- "tau2".padStart(6),
135
- "$/1M".padStart(7),
136
- "tok/s".padStart(6),
137
- "code/$".padStart(7),
138
- "conf",
139
- ].join(" ");
140
-
141
- const lines = entries.map((entry) => [
142
- fitId(entry.id, 40),
143
- (entry.billing === "subscription" ? "sub" : "paid").padEnd(4),
144
- (entry.quotaLeftPercent === undefined ? "-" : `${entry.quotaLeftPercent}%`).padStart(5),
145
- cell(entry.scores.artificial_analysis_intelligence_index),
146
- cell(entry.scores.artificial_analysis_coding_index),
147
- cell(entry.scores.terminalbench_hard),
148
- cell(entry.scores.terminalbench_v2_1),
149
- cell(entry.scores.tau2),
150
- cell(entry.pricing.blended3to1Per1M, 7),
151
- cell(entry.performance.outputTokensPerSecond),
152
- cell(entry.efficiency.codingPerDollar, 7),
153
- entry.confidence,
154
- ].join(" "));
155
-
156
- return [header, "-".repeat(header.length), ...lines].join("\n");
157
- }
158
-
159
- function formatFull(entry: Entry): string {
160
- const lines = [
161
- `${entry.id}${entry.benchmarkName ? ` ${entry.benchmarkName}` : ""}`,
162
- ` billing: ${entry.billing}${entry.quotaLeftPercent !== undefined ? ` · quota left ${entry.quotaLeftPercent}%` : ""}`,
163
- ` context: ${entry.contextWindow.toLocaleString()} · max out: ${entry.maxTokens.toLocaleString()} · reasoning: ${entry.reasoning}`,
164
- ` released: ${entry.releaseDate ?? "unknown"} · benchmark confidence: ${entry.confidence} (${entry.basis})`,
165
- " benchmarks:",
166
- ];
167
-
168
- let group = "";
169
- for (const definition of EVALUATIONS) {
170
- const value = entry.scores[definition.key];
171
- if (value === undefined) continue;
172
- if (definition.group !== group) {
173
- group = definition.group;
174
- lines.push(` [${group}]`);
175
- }
176
- lines.push(` ${definition.label.padEnd(34)} ${value.toFixed(1).padStart(6)}`);
177
- }
178
-
179
- lines.push(
180
- " pricing per 1M tokens:",
181
- ` input ${entry.pricing.inputPer1M ?? "-"} · output ${entry.pricing.outputPer1M ?? "-"} · blended 3:1 ${entry.pricing.blended3to1Per1M ?? "-"}`,
182
- " performance:",
183
- ` ${entry.performance.outputTokensPerSecond?.toFixed(1) ?? "-"} tok/s · first token ${entry.performance.timeToFirstTokenSeconds?.toFixed(1) ?? "-"}s`,
184
- " cost efficiency (score per $ blended):",
185
- ` intelligence ${entry.efficiency.intelligencePerDollar ?? "-"} · coding ${entry.efficiency.codingPerDollar ?? "-"} · agentic ${entry.efficiency.agenticPerDollar ?? "-"}`,
186
- );
187
- return lines.join("\n");
188
- }
189
-
190
- export function registerCatalogTool(pi: ExtensionAPI): void {
191
- pi.registerTool({
192
- name: "list_models",
193
- label: "List Models",
194
- description:
195
- "List models available to this pi session with full Artificial Analysis benchmark data: intelligence/coding/math indices, "
196
- + "Terminal-Bench, τ²-bench, LiveCodeBench, SciCode, GPQA, HLE, MMLU-Pro, IFBench, AIME, MATH-500, long-context reasoning, "
197
- + "pricing, throughput, latency, and cost-efficiency ratios. Also reports subscription vs metered billing and remaining "
198
- + "subscription quota. Use it to choose the model for a task instead of guessing.",
199
- promptSnippet: "Inspect available models with benchmark scores, price, speed and remaining quota",
200
- promptGuidelines: [
201
- "Call list_models before choosing a model for a delegated task or workflow stage, and prefer subscription models with quota remaining.",
202
- ],
203
- parameters: Type.Object({
204
- query: Type.Optional(Type.String({ description: "Substring filter on model id" })),
205
- provider: Type.Optional(Type.String({ description: "Filter by provider, e.g. anthropic or openai-codex" })),
206
- sort_by: Type.Optional(StringEnum(["coding", "intelligence", "agentic", "reasoning", "cost", "speed", "cost_efficiency"] as const)),
207
- detail: Type.Optional(StringEnum(["table", "full"] as const)),
208
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
209
- include_variants: Type.Optional(Type.Boolean({ description: "Include duplicate OpenRouter re-publications such as :batch and -pro" })),
210
- }),
211
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
212
- const warnings = await refreshQuality(false);
213
- // Guarantees quota figures even in headless sessions, where nothing else
214
- // would have triggered a usage poll.
215
- await ensureFresh(ctx);
216
- const available = await ctx.modelRegistry.getAvailable();
217
-
218
- let entries = available.map(buildEntry);
219
- if (params.provider) entries = entries.filter((entry) => entry.provider === params.provider);
220
- if (params.query) {
221
- const needle = params.query.toLowerCase();
222
- entries = entries.filter((entry) => entry.id.toLowerCase().includes(needle));
223
- }
224
-
225
- if (!params.include_variants) entries = dedupe(entries);
226
-
227
- const sortKey = (params.sort_by ?? "coding") as SortKey;
228
- entries.sort((a, b) => SORT_ACCESSORS[sortKey](b) - SORT_ACCESSORS[sortKey](a));
229
- entries = entries.slice(0, params.limit ?? 20);
230
-
231
- const ageHours = qualityAgeMs() !== undefined ? Math.round(qualityAgeMs()! / 3_600_000) : undefined;
232
- const headerNote = `Artificial Analysis dataset: ${qualityRecordCount()} models`
233
- + `${ageHours !== undefined ? `, refreshed ${ageHours}h ago` : ""}. Scores normalized to 0-100.`;
234
-
235
- const body = params.detail === "full"
236
- ? entries.map(formatFull).join("\n\n")
237
- : formatTable(entries);
238
-
239
- return {
240
- content: [{ type: "text", text: [headerNote, ...warnings, "", body].join("\n") }],
241
- details: { entries },
242
- };
243
- },
244
- });
245
-
246
- pi.registerCommand("models", {
247
- description: "Show available models ranked by benchmark data (models [coding|intelligence|agentic|cost|speed])",
248
- getArgumentCompletions: (prefix) =>
249
- Object.keys(SORT_ACCESSORS)
250
- .filter((key) => key.startsWith(prefix))
251
- .map((key) => ({ value: key, label: key })),
252
- handler: async (args, ctx) => {
253
- await refreshQuality(false);
254
- await ensureFresh(ctx);
255
- const sortKey = (args.trim() || "coding") as SortKey;
256
- const key = SORT_ACCESSORS[sortKey] ? sortKey : "coding";
257
- const entries = dedupe((await ctx.modelRegistry.getAvailable()).map(buildEntry));
258
- entries.sort((a, b) => SORT_ACCESSORS[key](b) - SORT_ACCESSORS[key](a));
259
- ctx.ui.notify(`sorted by ${key}\n${formatTable(entries.slice(0, 25))}`, "info");
260
- },
261
- });
262
-
263
- pi.registerCommand("model-info", {
264
- description: "Benchmarks for one model id, or `refresh` / `setup`",
265
- getArgumentCompletions: (prefix) =>
266
- [
267
- { value: "refresh", label: "refresh: force a benchmark refresh" },
268
- { value: "setup", label: "setup: add the Artificial Analysis API key" },
269
- ].filter((option) => option.value.startsWith(prefix)),
270
- handler: async (args, ctx) => {
271
- const needle = args.trim().toLowerCase();
272
-
273
- if (needle === "setup") {
274
- const existing = env("ARTIFICIAL_ANALYSIS_API_KEY");
275
-
276
- if (existing && isFromProcessEnv("ARTIFICIAL_ANALYSIS_API_KEY")) {
277
- ctx.ui.notify(
278
- `ARTIFICIAL_ANALYSIS_API_KEY is set in your environment (${maskSecret(existing)}).\n`
279
- + "That always wins over stored settings. Unset it to manage the key here.",
280
- "warning",
281
- );
282
- return;
283
- }
284
-
285
- if (!ctx.hasUI) {
286
- ctx.ui.notify("Run /model-info setup in an interactive session.", "error");
287
- return;
288
- }
289
-
290
- ctx.ui.notify(
291
- [
292
- "Benchmark data comes from Artificial Analysis and needs a free API key.",
293
- "",
294
- " 1. Sign up at https://artificialanalysis.ai/insights/api",
295
- " 2. Copy your key (it looks like aa_…)",
296
- "",
297
- existing ? `A key is already stored (${maskSecret(existing)}). Entering a new one replaces it.` : "",
298
- ].filter(Boolean).join("\n"),
299
- "info",
300
- );
301
-
302
- const entered = await ctx.ui.input("Artificial Analysis API key", existing ? "leave blank to keep current" : "aa_…");
303
- if (entered === undefined) return;
304
- const key = entered.trim();
305
- if (!key) {
306
- ctx.ui.notify(existing ? "Kept the existing key." : "No key entered.", "info");
307
- return;
308
- }
309
-
310
- setEnv("ARTIFICIAL_ANALYSIS_API_KEY", key);
311
- ctx.ui.notify("Key saved. Verifying…", "info");
312
-
313
- const warnings = await refreshQuality(true);
314
- if (warnings.length > 0) {
315
- ctx.ui.notify(
316
- `Saved, but the key did not work:\n${warnings.join("\n")}\n\nRun /model-info setup again to replace it.`,
317
- "warning",
318
- );
319
- return;
320
- }
321
- ctx.ui.notify(`Verified. ${qualityRecordCount()} models loaded from Artificial Analysis.`, "info");
322
- return;
323
- }
324
-
325
- if (needle === "refresh") {
326
- const warnings = await refreshQuality(true);
327
- const status = qualityStatus();
328
- const checked = status.checkedAt ? new Date(status.checkedAt).toLocaleTimeString() : "never";
329
- const upstream = status.lastModified ? new Date(status.lastModified).toLocaleString() : "unknown";
330
- ctx.ui.notify(
331
- warnings.length > 0
332
- ? `Refresh issues:\n${warnings.join("\n")}`
333
- : [
334
- `Artificial Analysis: ${status.records} models`,
335
- ` checked: ${checked}`,
336
- ` dataset dated: ${upstream}`,
337
- ` revalidates: every 4h via ETag`,
338
- ].join("\n"),
339
- warnings.length > 0 ? "warning" : "info",
340
- );
341
- return;
342
- }
343
-
344
- if (!needle) {
345
- ctx.ui.notify("Usage: /model-info <model id substring>, or /model-info refresh | setup", "warning");
346
- return;
347
- }
348
-
349
- await refreshQuality(false);
350
- await ensureFresh(ctx);
351
- const available = await ctx.modelRegistry.getAvailable();
352
- const model = available.find((candidate: any) =>
353
- `${candidate.provider}/${candidate.id}`.toLowerCase().includes(needle));
354
- if (!model) {
355
- ctx.ui.notify(`No available model matched “${needle}”.`, "error");
356
- return;
357
- }
358
- ctx.ui.notify(formatFull(buildEntry(model)), "info");
359
- },
360
- });
361
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { StringEnum } from "@earendil-works/pi-ai";
3
+ import { Type } from "typebox";
4
+ import {
5
+ EVALUATIONS,
6
+ type EvaluationKey,
7
+ costEfficiency,
8
+ normalizedScore,
9
+ qualityAgeMs,
10
+ qualityFor,
11
+ qualityRecordCount,
12
+ qualityStatus,
13
+ refreshQuality,
14
+ } from "../../core/catalog/quality.ts";
15
+ import { ensureFresh, usageState } from "../../services/usage-service.ts";
16
+ import { env, isFromProcessEnv, maskSecret, setEnv } from "../../core/env.ts";
17
+ import { fitId } from "../../ui/format.ts";
18
+
19
+ /**
20
+ * Exposes the Artificial Analysis benchmark catalogue to the model so it can
21
+ * pick a model per task instead of relying on fixed small/medium/big profiles.
22
+ */
23
+
24
+ const SUBSCRIPTION_PROVIDERS = new Set(["anthropic", "openai-codex", "kimi-coding"]);
25
+
26
+ type SortKey = "coding" | "intelligence" | "agentic" | "reasoning" | "cost" | "speed" | "cost_efficiency";
27
+
28
+ const SORT_ACCESSORS: Record<SortKey, (entry: Entry) => number> = {
29
+ coding: (entry) => entry.scores.artificial_analysis_coding_index ?? -1,
30
+ intelligence: (entry) => entry.scores.artificial_analysis_intelligence_index ?? -1,
31
+ agentic: (entry) => entry.scores.terminalbench_hard ?? entry.scores.terminalbench_v2_1 ?? entry.scores.tau2 ?? -1,
32
+ reasoning: (entry) => entry.scores.gpqa ?? entry.scores.hle ?? -1,
33
+ cost: (entry) => -(entry.pricing.blended3to1Per1M ?? Number.MAX_SAFE_INTEGER),
34
+ speed: (entry) => entry.performance.outputTokensPerSecond ?? -1,
35
+ cost_efficiency: (entry) => entry.efficiency.codingPerDollar ?? entry.efficiency.intelligencePerDollar ?? -1,
36
+ };
37
+
38
+ interface Entry {
39
+ id: string;
40
+ provider: string;
41
+ billing: "subscription" | "metered";
42
+ quotaLeftPercent?: number;
43
+ contextWindow: number;
44
+ maxTokens: number;
45
+ reasoning: boolean;
46
+ confidence: string;
47
+ basis: string;
48
+ benchmarkName?: string;
49
+ releaseDate?: string;
50
+ scores: Partial<Record<EvaluationKey, number>>;
51
+ pricing: { inputPer1M?: number; outputPer1M?: number; blended3to1Per1M?: number };
52
+ performance: { outputTokensPerSecond?: number; timeToFirstTokenSeconds?: number };
53
+ efficiency: { intelligencePerDollar?: number; codingPerDollar?: number; agenticPerDollar?: number };
54
+ }
55
+
56
+ function quotaFor(provider: string): number | undefined {
57
+ const rows = usageState().rows;
58
+ const match = provider === "anthropic"
59
+ ? rows.find((row) => row.group.includes("pool") && row.label === "5h")
60
+ ?? rows.find((row) => row.group.startsWith("Claude ") && row.label === "5h")
61
+ : provider === "openai-codex"
62
+ ? rows.find((row) => row.group === "Codex" && row.label === "weekly")
63
+ : undefined;
64
+ return match ? Math.round(match.remaining) : undefined;
65
+ }
66
+
67
+ function buildEntry(model: any): Entry {
68
+ const lookup = qualityFor(`${model.provider}/${model.id}`);
69
+ const record = lookup.record;
70
+ const scores: Partial<Record<EvaluationKey, number>> = {};
71
+ for (const { key } of EVALUATIONS) {
72
+ const value = normalizedScore(key, record?.evaluations[key]);
73
+ if (value !== undefined) scores[key] = Number(value.toFixed(1));
74
+ }
75
+ return {
76
+ id: `${model.provider}/${model.id}`,
77
+ provider: model.provider,
78
+ billing: SUBSCRIPTION_PROVIDERS.has(model.provider) ? "subscription" : "metered",
79
+ quotaLeftPercent: quotaFor(model.provider),
80
+ contextWindow: model.contextWindow,
81
+ maxTokens: model.maxTokens,
82
+ reasoning: !!model.reasoning,
83
+ confidence: lookup.confidence,
84
+ basis: lookup.basis,
85
+ benchmarkName: record?.name,
86
+ releaseDate: record?.releaseDate,
87
+ scores,
88
+ pricing: record?.pricing ?? {},
89
+ performance: {
90
+ outputTokensPerSecond: record?.performance.outputTokensPerSecond,
91
+ timeToFirstTokenSeconds: record?.performance.timeToFirstTokenSeconds,
92
+ },
93
+ efficiency: costEfficiency(record),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * OpenRouter republishes the same underlying model many times (`:batch`,
99
+ * `:free`, `-pro`, `~` prefixes). Collapse them to the best single row unless
100
+ * the caller explicitly asked for variants.
101
+ */
102
+ function dedupe(entries: Entry[]): Entry[] {
103
+ const best = new Map<string, Entry>();
104
+ for (const entry of entries) {
105
+ // Unrated models share one basis string, so they must key on their own id
106
+ // or they would collapse into a single row and disappear from the catalogue.
107
+ const family = entry.confidence === "unrated"
108
+ ? `unrated:${entry.id}`
109
+ : entry.basis.replace(/^nearest match /, "");
110
+ const current = best.get(family);
111
+ if (!current) {
112
+ best.set(family, entry);
113
+ continue;
114
+ }
115
+ const better = entry.billing === "subscription" && current.billing !== "subscription"
116
+ || (entry.billing === current.billing && entry.confidence === "measured" && current.confidence !== "measured")
117
+ || (entry.billing === current.billing && entry.confidence === current.confidence && entry.id.length < current.id.length);
118
+ if (better) best.set(family, entry);
119
+ }
120
+ return [...best.values()];
121
+ }
122
+
123
+ function formatTable(entries: Entry[]): string {
124
+ const cell = (value: number | undefined, width = 6) =>
125
+ (value === undefined ? "-".padStart(width) : value.toFixed(1).padStart(width));
126
+ const header = [
127
+ "model".padEnd(40),
128
+ "bill".padEnd(4),
129
+ "quota".padStart(5),
130
+ "intel".padStart(6),
131
+ "code".padStart(6),
132
+ "tbHard".padStart(6),
133
+ "tb2.1".padStart(6),
134
+ "tau2".padStart(6),
135
+ "$/1M".padStart(7),
136
+ "tok/s".padStart(6),
137
+ "code/$".padStart(7),
138
+ "conf",
139
+ ].join(" ");
140
+
141
+ const lines = entries.map((entry) => [
142
+ fitId(entry.id, 40),
143
+ (entry.billing === "subscription" ? "sub" : "paid").padEnd(4),
144
+ (entry.quotaLeftPercent === undefined ? "-" : `${entry.quotaLeftPercent}%`).padStart(5),
145
+ cell(entry.scores.artificial_analysis_intelligence_index),
146
+ cell(entry.scores.artificial_analysis_coding_index),
147
+ cell(entry.scores.terminalbench_hard),
148
+ cell(entry.scores.terminalbench_v2_1),
149
+ cell(entry.scores.tau2),
150
+ cell(entry.pricing.blended3to1Per1M, 7),
151
+ cell(entry.performance.outputTokensPerSecond),
152
+ cell(entry.efficiency.codingPerDollar, 7),
153
+ entry.confidence,
154
+ ].join(" "));
155
+
156
+ return [header, "-".repeat(header.length), ...lines].join("\n");
157
+ }
158
+
159
+ function formatFull(entry: Entry): string {
160
+ const lines = [
161
+ `${entry.id}${entry.benchmarkName ? ` ${entry.benchmarkName}` : ""}`,
162
+ ` billing: ${entry.billing}${entry.quotaLeftPercent !== undefined ? ` · quota left ${entry.quotaLeftPercent}%` : ""}`,
163
+ ` context: ${entry.contextWindow.toLocaleString()} · max out: ${entry.maxTokens.toLocaleString()} · reasoning: ${entry.reasoning}`,
164
+ ` released: ${entry.releaseDate ?? "unknown"} · benchmark confidence: ${entry.confidence} (${entry.basis})`,
165
+ " benchmarks:",
166
+ ];
167
+
168
+ let group = "";
169
+ for (const definition of EVALUATIONS) {
170
+ const value = entry.scores[definition.key];
171
+ if (value === undefined) continue;
172
+ if (definition.group !== group) {
173
+ group = definition.group;
174
+ lines.push(` [${group}]`);
175
+ }
176
+ lines.push(` ${definition.label.padEnd(34)} ${value.toFixed(1).padStart(6)}`);
177
+ }
178
+
179
+ lines.push(
180
+ " pricing per 1M tokens:",
181
+ ` input ${entry.pricing.inputPer1M ?? "-"} · output ${entry.pricing.outputPer1M ?? "-"} · blended 3:1 ${entry.pricing.blended3to1Per1M ?? "-"}`,
182
+ " performance:",
183
+ ` ${entry.performance.outputTokensPerSecond?.toFixed(1) ?? "-"} tok/s · first token ${entry.performance.timeToFirstTokenSeconds?.toFixed(1) ?? "-"}s`,
184
+ " cost efficiency (score per $ blended):",
185
+ ` intelligence ${entry.efficiency.intelligencePerDollar ?? "-"} · coding ${entry.efficiency.codingPerDollar ?? "-"} · agentic ${entry.efficiency.agenticPerDollar ?? "-"}`,
186
+ );
187
+ return lines.join("\n");
188
+ }
189
+
190
+ export function registerCatalogTool(pi: ExtensionAPI): void {
191
+ pi.registerTool({
192
+ name: "list_models",
193
+ label: "List Models",
194
+ description:
195
+ "List models available to this pi session with full Artificial Analysis benchmark data: intelligence/coding/math indices, "
196
+ + "Terminal-Bench, τ²-bench, LiveCodeBench, SciCode, GPQA, HLE, MMLU-Pro, IFBench, AIME, MATH-500, long-context reasoning, "
197
+ + "pricing, throughput, latency, and cost-efficiency ratios. Also reports subscription vs metered billing and remaining "
198
+ + "subscription quota. Use it to choose the model for a task instead of guessing.",
199
+ promptSnippet: "Inspect available models with benchmark scores, price, speed and remaining quota",
200
+ promptGuidelines: [
201
+ "Call list_models before choosing a model for a delegated task or workflow stage, and prefer subscription models with quota remaining.",
202
+ ],
203
+ parameters: Type.Object({
204
+ query: Type.Optional(Type.String({ description: "Substring filter on model id" })),
205
+ provider: Type.Optional(Type.String({ description: "Filter by provider, e.g. anthropic or openai-codex" })),
206
+ sort_by: Type.Optional(StringEnum(["coding", "intelligence", "agentic", "reasoning", "cost", "speed", "cost_efficiency"] as const)),
207
+ detail: Type.Optional(StringEnum(["table", "full"] as const)),
208
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
209
+ include_variants: Type.Optional(Type.Boolean({ description: "Include duplicate OpenRouter re-publications such as :batch and -pro" })),
210
+ }),
211
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
212
+ const warnings = await refreshQuality(false);
213
+ // Guarantees quota figures even in headless sessions, where nothing else
214
+ // would have triggered a usage poll.
215
+ await ensureFresh(ctx);
216
+ const available = await ctx.modelRegistry.getAvailable();
217
+
218
+ let entries = available.map(buildEntry);
219
+ if (params.provider) entries = entries.filter((entry) => entry.provider === params.provider);
220
+ if (params.query) {
221
+ const needle = params.query.toLowerCase();
222
+ entries = entries.filter((entry) => entry.id.toLowerCase().includes(needle));
223
+ }
224
+
225
+ if (!params.include_variants) entries = dedupe(entries);
226
+
227
+ const sortKey = (params.sort_by ?? "coding") as SortKey;
228
+ entries.sort((a, b) => SORT_ACCESSORS[sortKey](b) - SORT_ACCESSORS[sortKey](a));
229
+ entries = entries.slice(0, params.limit ?? 20);
230
+
231
+ const ageHours = qualityAgeMs() !== undefined ? Math.round(qualityAgeMs()! / 3_600_000) : undefined;
232
+ const headerNote = `Artificial Analysis dataset: ${qualityRecordCount()} models`
233
+ + `${ageHours !== undefined ? `, refreshed ${ageHours}h ago` : ""}. Scores normalized to 0-100.`;
234
+
235
+ const body = params.detail === "full"
236
+ ? entries.map(formatFull).join("\n\n")
237
+ : formatTable(entries);
238
+
239
+ return {
240
+ content: [{ type: "text", text: [headerNote, ...warnings, "", body].join("\n") }],
241
+ details: { entries },
242
+ };
243
+ },
244
+ });
245
+
246
+ pi.registerCommand("models", {
247
+ description: "Show available models ranked by benchmark data (models [coding|intelligence|agentic|cost|speed])",
248
+ getArgumentCompletions: (prefix) =>
249
+ Object.keys(SORT_ACCESSORS)
250
+ .filter((key) => key.startsWith(prefix))
251
+ .map((key) => ({ value: key, label: key })),
252
+ handler: async (args, ctx) => {
253
+ await refreshQuality(false);
254
+ await ensureFresh(ctx);
255
+ const sortKey = (args.trim() || "coding") as SortKey;
256
+ const key = SORT_ACCESSORS[sortKey] ? sortKey : "coding";
257
+ const entries = dedupe((await ctx.modelRegistry.getAvailable()).map(buildEntry));
258
+ entries.sort((a, b) => SORT_ACCESSORS[key](b) - SORT_ACCESSORS[key](a));
259
+ ctx.ui.notify(`sorted by ${key}\n${formatTable(entries.slice(0, 25))}`, "info");
260
+ },
261
+ });
262
+
263
+ pi.registerCommand("model-info", {
264
+ description: "Benchmarks for one model id, or `refresh` / `setup`",
265
+ getArgumentCompletions: (prefix) =>
266
+ [
267
+ { value: "refresh", label: "refresh: force a benchmark refresh" },
268
+ { value: "setup", label: "setup: add the Artificial Analysis API key" },
269
+ ].filter((option) => option.value.startsWith(prefix)),
270
+ handler: async (args, ctx) => {
271
+ const needle = args.trim().toLowerCase();
272
+
273
+ if (needle === "setup") {
274
+ const existing = env("ARTIFICIAL_ANALYSIS_API_KEY");
275
+
276
+ if (existing && isFromProcessEnv("ARTIFICIAL_ANALYSIS_API_KEY")) {
277
+ ctx.ui.notify(
278
+ `ARTIFICIAL_ANALYSIS_API_KEY is set in your environment (${maskSecret(existing)}).\n`
279
+ + "That always wins over stored settings. Unset it to manage the key here.",
280
+ "warning",
281
+ );
282
+ return;
283
+ }
284
+
285
+ if (!ctx.hasUI) {
286
+ ctx.ui.notify("Run /model-info setup in an interactive session.", "error");
287
+ return;
288
+ }
289
+
290
+ ctx.ui.notify(
291
+ [
292
+ "Benchmark data comes from Artificial Analysis and needs a free API key.",
293
+ "",
294
+ " 1. Sign up at https://artificialanalysis.ai/insights/api",
295
+ " 2. Copy your key (it looks like aa_…)",
296
+ "",
297
+ existing ? `A key is already stored (${maskSecret(existing)}). Entering a new one replaces it.` : "",
298
+ ].filter(Boolean).join("\n"),
299
+ "info",
300
+ );
301
+
302
+ const entered = await ctx.ui.input("Artificial Analysis API key", existing ? "leave blank to keep current" : "aa_…");
303
+ if (entered === undefined) return;
304
+ const key = entered.trim();
305
+ if (!key) {
306
+ ctx.ui.notify(existing ? "Kept the existing key." : "No key entered.", "info");
307
+ return;
308
+ }
309
+
310
+ setEnv("ARTIFICIAL_ANALYSIS_API_KEY", key);
311
+ ctx.ui.notify("Key saved. Verifying…", "info");
312
+
313
+ const warnings = await refreshQuality(true);
314
+ if (warnings.length > 0) {
315
+ ctx.ui.notify(
316
+ `Saved, but the key did not work:\n${warnings.join("\n")}\n\nRun /model-info setup again to replace it.`,
317
+ "warning",
318
+ );
319
+ return;
320
+ }
321
+ ctx.ui.notify(`Verified. ${qualityRecordCount()} models loaded from Artificial Analysis.`, "info");
322
+ return;
323
+ }
324
+
325
+ if (needle === "refresh") {
326
+ const warnings = await refreshQuality(true);
327
+ const status = qualityStatus();
328
+ const checked = status.checkedAt ? new Date(status.checkedAt).toLocaleTimeString() : "never";
329
+ const upstream = status.lastModified ? new Date(status.lastModified).toLocaleString() : "unknown";
330
+ ctx.ui.notify(
331
+ warnings.length > 0
332
+ ? `Refresh issues:\n${warnings.join("\n")}`
333
+ : [
334
+ `Artificial Analysis: ${status.records} models`,
335
+ ` checked: ${checked}`,
336
+ ` dataset dated: ${upstream}`,
337
+ ` revalidates: every 4h via ETag`,
338
+ ].join("\n"),
339
+ warnings.length > 0 ? "warning" : "info",
340
+ );
341
+ return;
342
+ }
343
+
344
+ if (!needle) {
345
+ ctx.ui.notify("Usage: /model-info <model id substring>, or /model-info refresh | setup", "warning");
346
+ return;
347
+ }
348
+
349
+ await refreshQuality(false);
350
+ await ensureFresh(ctx);
351
+ const available = await ctx.modelRegistry.getAvailable();
352
+ const model = available.find((candidate: any) =>
353
+ `${candidate.provider}/${candidate.id}`.toLowerCase().includes(needle));
354
+ if (!model) {
355
+ ctx.ui.notify(`No available model matched “${needle}”.`, "error");
356
+ return;
357
+ }
358
+ ctx.ui.notify(formatFull(buildEntry(model)), "info");
359
+ },
360
+ });
361
+ }