@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.
- package/LICENSE +21 -21
- package/README.md +190 -190
- package/config/pi-plus.example.json +60 -60
- package/config/skills/model-routing/SKILL.md +86 -86
- package/images/pi-plus.svg +10 -10
- package/package.json +67 -67
- package/server/board-server.mjs +641 -641
- package/server/package.json +17 -17
- package/src/core/accounts/registry.ts +93 -93
- package/src/core/anthropic/client-identity.ts +241 -241
- package/src/core/catalog/quality.ts +314 -314
- package/src/core/config.ts +169 -169
- package/src/core/env.ts +58 -58
- package/src/core/exec/process.ts +146 -146
- package/src/core/exec/ssh-config.ts +157 -157
- package/src/core/policy/policy.ts +183 -183
- package/src/core/quota/pool.ts +64 -64
- package/src/core/quota/usage-source.ts +289 -289
- package/src/core/store.ts +43 -43
- package/src/domains/agents/board-setup.ts +409 -409
- package/src/domains/agents/index.ts +462 -462
- package/src/domains/models/catalog-tool.ts +361 -361
- package/src/domains/models/index.ts +14 -14
- package/src/domains/models/policy-gate.ts +169 -169
- package/src/domains/models/provider-picker.ts +207 -207
- package/src/domains/remote/config-path.ts +41 -41
- package/src/domains/remote/index.ts +866 -866
- package/src/domains/remote/setup.ts +425 -425
- package/src/domains/setup/index.ts +220 -220
- package/src/domains/subscriptions/accounts.ts +242 -242
- package/src/domains/subscriptions/footer.ts +182 -182
- package/src/domains/subscriptions/index.ts +42 -42
- package/src/domains/subscriptions/provider.ts +219 -219
- package/src/domains/subscriptions/providers/anthropic.ts +149 -149
- package/src/domains/subscriptions/providers/codex.ts +148 -148
- package/src/domains/subscriptions/routing.ts +72 -72
- package/src/services/usage-service.ts +186 -186
- package/src/ui/format.ts +73 -73
- package/src/ui/usage-bars.ts +154 -154
- package/src/vendor/anthropic.ts +109 -109
|
@@ -1,314 +1,314 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { env } from "../env.ts";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Complete Artificial Analysis model dataset, cached on disk.
|
|
8
|
-
*
|
|
9
|
-
* Every field the API returns is stored verbatim: all 17 benchmark
|
|
10
|
-
* evaluations, all pricing fields, and all latency/throughput measurements.
|
|
11
|
-
* Derived cost-efficiency ratios are computed on top. All network access
|
|
12
|
-
* degrades to the cache so routing never blocks on the network.
|
|
13
|
-
*
|
|
14
|
-
* Refresh semantics deliberately mirror pi's own remote model catalog
|
|
15
|
-
* (`dist/core/remote-catalog-provider.js`): restore from disk first, only go to
|
|
16
|
-
* the network when the entry is older than the refresh interval, and revalidate
|
|
17
|
-
* with `If-None-Match` so an unchanged dataset costs one 304 instead of a
|
|
18
|
-
* multi-megabyte download.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const AA_URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
|
|
22
|
-
/** Matches pi's REMOTE_CATALOG_REFRESH_INTERVAL_MS. */
|
|
23
|
-
export const REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
24
|
-
const TIMEOUT_MS = 30_000;
|
|
25
|
-
|
|
26
|
-
export type Confidence = "measured" | "inferred" | "unrated";
|
|
27
|
-
|
|
28
|
-
/** Every evaluation key Artificial Analysis publishes, with display metadata. */
|
|
29
|
-
export const EVALUATIONS = [
|
|
30
|
-
{ key: "artificial_analysis_intelligence_index", label: "Intelligence Index", scale: "index", group: "composite" },
|
|
31
|
-
{ key: "artificial_analysis_coding_index", label: "Coding Index", scale: "index", group: "composite" },
|
|
32
|
-
{ key: "artificial_analysis_math_index", label: "Math Index", scale: "index", group: "composite" },
|
|
33
|
-
{ key: "terminalbench_hard", label: "Terminal-Bench Hard", scale: "fraction", group: "agentic" },
|
|
34
|
-
{ key: "terminalbench_v2_1", label: "Terminal-Bench 2.1", scale: "fraction", group: "agentic" },
|
|
35
|
-
{ key: "tau2", label: "τ²-bench (agentic tools)", scale: "fraction", group: "agentic" },
|
|
36
|
-
{ key: "tau_banking", label: "τ-bench Banking", scale: "fraction", group: "agentic" },
|
|
37
|
-
{ key: "livecodebench", label: "LiveCodeBench", scale: "fraction", group: "coding" },
|
|
38
|
-
{ key: "scicode", label: "SciCode", scale: "fraction", group: "coding" },
|
|
39
|
-
{ key: "lcr", label: "Long Context Reasoning", scale: "fraction", group: "reasoning" },
|
|
40
|
-
{ key: "gpqa", label: "GPQA Diamond", scale: "fraction", group: "reasoning" },
|
|
41
|
-
{ key: "hle", label: "Humanity's Last Exam", scale: "fraction", group: "reasoning" },
|
|
42
|
-
{ key: "mmlu_pro", label: "MMLU-Pro", scale: "fraction", group: "reasoning" },
|
|
43
|
-
{ key: "ifbench", label: "IFBench (instruction following)", scale: "fraction", group: "reasoning" },
|
|
44
|
-
{ key: "aime_25", label: "AIME 2025", scale: "fraction", group: "math" },
|
|
45
|
-
{ key: "aime", label: "AIME", scale: "fraction", group: "math" },
|
|
46
|
-
{ key: "math_500", label: "MATH-500", scale: "fraction", group: "math" },
|
|
47
|
-
] as const;
|
|
48
|
-
|
|
49
|
-
export type EvaluationKey = (typeof EVALUATIONS)[number]["key"];
|
|
50
|
-
|
|
51
|
-
export interface QualityRecord {
|
|
52
|
-
slug: string;
|
|
53
|
-
name: string;
|
|
54
|
-
creator: string;
|
|
55
|
-
releaseDate?: string;
|
|
56
|
-
evaluations: Partial<Record<EvaluationKey, number>>;
|
|
57
|
-
pricing: {
|
|
58
|
-
inputPer1M?: number;
|
|
59
|
-
outputPer1M?: number;
|
|
60
|
-
blended3to1Per1M?: number;
|
|
61
|
-
};
|
|
62
|
-
performance: {
|
|
63
|
-
outputTokensPerSecond?: number;
|
|
64
|
-
timeToFirstTokenSeconds?: number;
|
|
65
|
-
timeToFirstAnswerTokenSeconds?: number;
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Same shape as pi's ModelsStoreEntry, so the two caches can be reasoned about
|
|
71
|
-
* (and debugged) identically.
|
|
72
|
-
*/
|
|
73
|
-
interface QualityStore {
|
|
74
|
-
records: Record<string, QualityRecord>;
|
|
75
|
-
/** Unix timestamp of the last completed remote check. */
|
|
76
|
-
checkedAt?: number;
|
|
77
|
-
/** Unix timestamp from the remote dataset's Last-Modified header. */
|
|
78
|
-
lastModified?: number;
|
|
79
|
-
/** Opaque ETag validator, stored verbatim and echoed back as If-None-Match. */
|
|
80
|
-
etag?: string;
|
|
81
|
-
source: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let cache: QualityStore | undefined;
|
|
85
|
-
let inFlight: Promise<string[]> | undefined;
|
|
86
|
-
|
|
87
|
-
function agentDir(): string {
|
|
88
|
-
return process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function cachePath(): string {
|
|
92
|
-
return join(agentDir(), "model-quality.json");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function readKey(): string | undefined {
|
|
96
|
-
return env("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** `openrouter/z-ai/glm-5.3` -> `glm-5-3`, `openai-codex/gpt-5.6-luna` -> `gpt-5-6-luna`. */
|
|
100
|
-
export function normalizeSlug(modelId: string): string {
|
|
101
|
-
const bare = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1) : modelId;
|
|
102
|
-
return bare.toLowerCase().replace(/[._]/g, "-").replace(/-latest$/, "").replace(/-\d{8}$/, "");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function num(value: unknown): number | undefined {
|
|
106
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Fractions (0-1) are reported as percentages so every metric shares one scale. */
|
|
110
|
-
export function normalizedScore(key: EvaluationKey, value: number | undefined): number | undefined {
|
|
111
|
-
if (value === undefined) return undefined;
|
|
112
|
-
const definition = EVALUATIONS.find((entry) => entry.key === key);
|
|
113
|
-
return definition?.scale === "fraction" ? value * 100 : value;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function loadCache(): void {
|
|
117
|
-
if (cache) return;
|
|
118
|
-
try {
|
|
119
|
-
const parsed = JSON.parse(readFileSync(cachePath(), "utf8")) as QualityStore & { fetchedAt?: number };
|
|
120
|
-
if (!parsed?.records) return;
|
|
121
|
-
// Migrate the pre-store format, which only had `fetchedAt`.
|
|
122
|
-
cache = {
|
|
123
|
-
records: parsed.records,
|
|
124
|
-
checkedAt: parsed.checkedAt ?? parsed.fetchedAt,
|
|
125
|
-
lastModified: parsed.lastModified,
|
|
126
|
-
etag: parsed.etag,
|
|
127
|
-
source: parsed.source ?? "artificial-analysis",
|
|
128
|
-
};
|
|
129
|
-
} catch { /* first run */ }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function persist(next: QualityStore): void {
|
|
133
|
-
cache = next;
|
|
134
|
-
try {
|
|
135
|
-
writeFileSync(cachePath(), JSON.stringify(next), "utf8");
|
|
136
|
-
} catch { /* cache write is best effort */ }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function qualityAgeMs(): number | undefined {
|
|
140
|
-
loadCache();
|
|
141
|
-
return cache?.checkedAt !== undefined ? Date.now() - cache.checkedAt : undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Cache metadata, for display by /model-info refresh. */
|
|
145
|
-
export function qualityStatus(): { checkedAt?: number; lastModified?: number; records: number; hasEtag: boolean } {
|
|
146
|
-
loadCache();
|
|
147
|
-
return {
|
|
148
|
-
checkedAt: cache?.checkedAt,
|
|
149
|
-
lastModified: cache?.lastModified,
|
|
150
|
-
records: Object.keys(cache?.records ?? {}).length,
|
|
151
|
-
hasEtag: !!cache?.etag,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function qualityRecordCount(): number {
|
|
156
|
-
loadCache();
|
|
157
|
-
return Object.keys(cache?.records ?? {}).length;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function refreshQuality(force = false): Promise<string[]> {
|
|
161
|
-
loadCache();
|
|
162
|
-
|
|
163
|
-
// Restored from disk and still inside the window: nothing to do.
|
|
164
|
-
const age = qualityAgeMs();
|
|
165
|
-
if (!force && age !== undefined && age < REFRESH_INTERVAL_MS) return [];
|
|
166
|
-
if (inFlight) return inFlight;
|
|
167
|
-
|
|
168
|
-
inFlight = (async () => {
|
|
169
|
-
const warnings: string[] = [];
|
|
170
|
-
const key = readKey();
|
|
171
|
-
if (!key) return ["artificial-analysis: no API key, run /model-info setup"];
|
|
172
|
-
|
|
173
|
-
const stored = cache;
|
|
174
|
-
// Only revalidate when a cached body backs the validator, so a 304 can never
|
|
175
|
-
// leave the dataset empty.
|
|
176
|
-
const validator = stored && Object.keys(stored.records).length > 0 ? stored.etag : undefined;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const response = await fetch(AA_URL, {
|
|
180
|
-
headers: {
|
|
181
|
-
"x-api-key": key,
|
|
182
|
-
...(validator ? { "if-none-match": validator } : {}),
|
|
183
|
-
},
|
|
184
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const checkedAt = Date.now();
|
|
188
|
-
|
|
189
|
-
if (response.status === 304 && stored) {
|
|
190
|
-
// Unchanged upstream: keep the dataset and the validator, stamp the check.
|
|
191
|
-
persist({ ...stored, checkedAt });
|
|
192
|
-
return [];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (!response.ok) {
|
|
196
|
-
// Drop the etag so the next attempt re-downloads rather than revalidating
|
|
197
|
-
// against a validator we can no longer trust.
|
|
198
|
-
if (stored) persist({ ...stored, checkedAt, etag: undefined });
|
|
199
|
-
return [`artificial-analysis: HTTP ${response.status}`];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const body = await response.json() as { data?: any[] };
|
|
203
|
-
const records: Record<string, QualityRecord> = {};
|
|
204
|
-
|
|
205
|
-
for (const model of body.data ?? []) {
|
|
206
|
-
const slug = String(model.slug ?? "").toLowerCase();
|
|
207
|
-
if (!slug) continue;
|
|
208
|
-
const source = model.evaluations ?? {};
|
|
209
|
-
const evaluations: Partial<Record<EvaluationKey, number>> = {};
|
|
210
|
-
for (const { key: evaluationKey } of EVALUATIONS) {
|
|
211
|
-
const value = num(source[evaluationKey]);
|
|
212
|
-
if (value !== undefined) evaluations[evaluationKey] = value;
|
|
213
|
-
}
|
|
214
|
-
const pricing = model.pricing ?? {};
|
|
215
|
-
records[slug] = {
|
|
216
|
-
slug,
|
|
217
|
-
name: String(model.name ?? slug),
|
|
218
|
-
creator: String(model.model_creator?.name ?? "unknown"),
|
|
219
|
-
releaseDate: typeof model.release_date === "string" ? model.release_date : undefined,
|
|
220
|
-
evaluations,
|
|
221
|
-
pricing: {
|
|
222
|
-
inputPer1M: num(pricing.price_1m_input_tokens),
|
|
223
|
-
outputPer1M: num(pricing.price_1m_output_tokens),
|
|
224
|
-
blended3to1Per1M: num(pricing.price_1m_blended_3_to_1),
|
|
225
|
-
},
|
|
226
|
-
performance: {
|
|
227
|
-
outputTokensPerSecond: num(model.median_output_tokens_per_second),
|
|
228
|
-
timeToFirstTokenSeconds: num(model.median_time_to_first_token_seconds),
|
|
229
|
-
timeToFirstAnswerTokenSeconds: num(model.median_time_to_first_answer_token),
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (Object.keys(records).length === 0) {
|
|
235
|
-
if (stored) persist({ ...stored, checkedAt, etag: undefined });
|
|
236
|
-
return ["artificial-analysis: empty dataset"];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const lastModifiedHeader = response.headers.get("last-modified");
|
|
240
|
-
const lastModified = lastModifiedHeader ? Date.parse(lastModifiedHeader) : undefined;
|
|
241
|
-
persist({
|
|
242
|
-
records,
|
|
243
|
-
checkedAt,
|
|
244
|
-
lastModified: Number.isNaN(lastModified) ? undefined : lastModified,
|
|
245
|
-
etag: response.headers.get("etag") ?? undefined,
|
|
246
|
-
source: "artificial-analysis",
|
|
247
|
-
});
|
|
248
|
-
} catch (error) {
|
|
249
|
-
// Network failure keeps the old dataset usable but forces a full re-fetch
|
|
250
|
-
// next time rather than trusting a stale validator.
|
|
251
|
-
if (cache) persist({ ...cache, checkedAt: Date.now(), etag: undefined });
|
|
252
|
-
warnings.push(`artificial-analysis: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
-
}
|
|
254
|
-
return warnings;
|
|
255
|
-
})().finally(() => { inFlight = undefined; });
|
|
256
|
-
|
|
257
|
-
return inFlight;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export interface CostEfficiency {
|
|
261
|
-
/** Intelligence index per dollar of blended 3:1 spend. */
|
|
262
|
-
intelligencePerDollar?: number;
|
|
263
|
-
codingPerDollar?: number;
|
|
264
|
-
agenticPerDollar?: number;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function costEfficiency(record: QualityRecord | undefined): CostEfficiency {
|
|
268
|
-
if (!record) return {};
|
|
269
|
-
const price = record.pricing.blended3to1Per1M;
|
|
270
|
-
if (price === undefined || price <= 0) return {};
|
|
271
|
-
const ratio = (value: number | undefined) => (value === undefined ? undefined : Number((value / price).toFixed(2)));
|
|
272
|
-
return {
|
|
273
|
-
intelligencePerDollar: ratio(record.evaluations.artificial_analysis_intelligence_index),
|
|
274
|
-
codingPerDollar: ratio(record.evaluations.artificial_analysis_coding_index),
|
|
275
|
-
agenticPerDollar: ratio(normalizedScore("terminalbench_hard", record.evaluations.terminalbench_hard)),
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export interface QualityLookup {
|
|
280
|
-
record?: QualityRecord;
|
|
281
|
-
efficiency: CostEfficiency;
|
|
282
|
-
confidence: Confidence;
|
|
283
|
-
basis: string;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Resolve a pi model id, preferring the entry matching the requested thinking
|
|
288
|
-
* level, then the base model, then the closest same-family sibling.
|
|
289
|
-
*/
|
|
290
|
-
export function qualityFor(modelId: string, thinkingLevel?: string): QualityLookup {
|
|
291
|
-
loadCache();
|
|
292
|
-
const records = cache?.records ?? {};
|
|
293
|
-
const base = normalizeSlug(modelId);
|
|
294
|
-
|
|
295
|
-
const levelled = thinkingLevel && thinkingLevel !== "off" ? records[`${base}-${thinkingLevel}`] : undefined;
|
|
296
|
-
if (levelled) return { record: levelled, efficiency: costEfficiency(levelled), confidence: "measured", basis: levelled.slug };
|
|
297
|
-
|
|
298
|
-
const exact = records[base];
|
|
299
|
-
if (exact) return { record: exact, efficiency: costEfficiency(exact), confidence: "measured", basis: exact.slug };
|
|
300
|
-
|
|
301
|
-
let best: QualityRecord | undefined;
|
|
302
|
-
for (const record of Object.values(records)) {
|
|
303
|
-
if (!base.includes(record.slug) && !record.slug.includes(base)) continue;
|
|
304
|
-
if (!best || record.slug.length > best.slug.length) best = record;
|
|
305
|
-
}
|
|
306
|
-
if (best) return { record: best, efficiency: costEfficiency(best), confidence: "inferred", basis: `nearest match ${best.slug}` };
|
|
307
|
-
|
|
308
|
-
return { efficiency: {}, confidence: "unrated", basis: "no Artificial Analysis entry" };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function allRecords(): QualityRecord[] {
|
|
312
|
-
loadCache();
|
|
313
|
-
return Object.values(cache?.records ?? {});
|
|
314
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { env } from "../env.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Complete Artificial Analysis model dataset, cached on disk.
|
|
8
|
+
*
|
|
9
|
+
* Every field the API returns is stored verbatim: all 17 benchmark
|
|
10
|
+
* evaluations, all pricing fields, and all latency/throughput measurements.
|
|
11
|
+
* Derived cost-efficiency ratios are computed on top. All network access
|
|
12
|
+
* degrades to the cache so routing never blocks on the network.
|
|
13
|
+
*
|
|
14
|
+
* Refresh semantics deliberately mirror pi's own remote model catalog
|
|
15
|
+
* (`dist/core/remote-catalog-provider.js`): restore from disk first, only go to
|
|
16
|
+
* the network when the entry is older than the refresh interval, and revalidate
|
|
17
|
+
* with `If-None-Match` so an unchanged dataset costs one 304 instead of a
|
|
18
|
+
* multi-megabyte download.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const AA_URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
|
|
22
|
+
/** Matches pi's REMOTE_CATALOG_REFRESH_INTERVAL_MS. */
|
|
23
|
+
export const REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
24
|
+
const TIMEOUT_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
export type Confidence = "measured" | "inferred" | "unrated";
|
|
27
|
+
|
|
28
|
+
/** Every evaluation key Artificial Analysis publishes, with display metadata. */
|
|
29
|
+
export const EVALUATIONS = [
|
|
30
|
+
{ key: "artificial_analysis_intelligence_index", label: "Intelligence Index", scale: "index", group: "composite" },
|
|
31
|
+
{ key: "artificial_analysis_coding_index", label: "Coding Index", scale: "index", group: "composite" },
|
|
32
|
+
{ key: "artificial_analysis_math_index", label: "Math Index", scale: "index", group: "composite" },
|
|
33
|
+
{ key: "terminalbench_hard", label: "Terminal-Bench Hard", scale: "fraction", group: "agentic" },
|
|
34
|
+
{ key: "terminalbench_v2_1", label: "Terminal-Bench 2.1", scale: "fraction", group: "agentic" },
|
|
35
|
+
{ key: "tau2", label: "τ²-bench (agentic tools)", scale: "fraction", group: "agentic" },
|
|
36
|
+
{ key: "tau_banking", label: "τ-bench Banking", scale: "fraction", group: "agentic" },
|
|
37
|
+
{ key: "livecodebench", label: "LiveCodeBench", scale: "fraction", group: "coding" },
|
|
38
|
+
{ key: "scicode", label: "SciCode", scale: "fraction", group: "coding" },
|
|
39
|
+
{ key: "lcr", label: "Long Context Reasoning", scale: "fraction", group: "reasoning" },
|
|
40
|
+
{ key: "gpqa", label: "GPQA Diamond", scale: "fraction", group: "reasoning" },
|
|
41
|
+
{ key: "hle", label: "Humanity's Last Exam", scale: "fraction", group: "reasoning" },
|
|
42
|
+
{ key: "mmlu_pro", label: "MMLU-Pro", scale: "fraction", group: "reasoning" },
|
|
43
|
+
{ key: "ifbench", label: "IFBench (instruction following)", scale: "fraction", group: "reasoning" },
|
|
44
|
+
{ key: "aime_25", label: "AIME 2025", scale: "fraction", group: "math" },
|
|
45
|
+
{ key: "aime", label: "AIME", scale: "fraction", group: "math" },
|
|
46
|
+
{ key: "math_500", label: "MATH-500", scale: "fraction", group: "math" },
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type EvaluationKey = (typeof EVALUATIONS)[number]["key"];
|
|
50
|
+
|
|
51
|
+
export interface QualityRecord {
|
|
52
|
+
slug: string;
|
|
53
|
+
name: string;
|
|
54
|
+
creator: string;
|
|
55
|
+
releaseDate?: string;
|
|
56
|
+
evaluations: Partial<Record<EvaluationKey, number>>;
|
|
57
|
+
pricing: {
|
|
58
|
+
inputPer1M?: number;
|
|
59
|
+
outputPer1M?: number;
|
|
60
|
+
blended3to1Per1M?: number;
|
|
61
|
+
};
|
|
62
|
+
performance: {
|
|
63
|
+
outputTokensPerSecond?: number;
|
|
64
|
+
timeToFirstTokenSeconds?: number;
|
|
65
|
+
timeToFirstAnswerTokenSeconds?: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Same shape as pi's ModelsStoreEntry, so the two caches can be reasoned about
|
|
71
|
+
* (and debugged) identically.
|
|
72
|
+
*/
|
|
73
|
+
interface QualityStore {
|
|
74
|
+
records: Record<string, QualityRecord>;
|
|
75
|
+
/** Unix timestamp of the last completed remote check. */
|
|
76
|
+
checkedAt?: number;
|
|
77
|
+
/** Unix timestamp from the remote dataset's Last-Modified header. */
|
|
78
|
+
lastModified?: number;
|
|
79
|
+
/** Opaque ETag validator, stored verbatim and echoed back as If-None-Match. */
|
|
80
|
+
etag?: string;
|
|
81
|
+
source: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let cache: QualityStore | undefined;
|
|
85
|
+
let inFlight: Promise<string[]> | undefined;
|
|
86
|
+
|
|
87
|
+
function agentDir(): string {
|
|
88
|
+
return process.env.PI_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cachePath(): string {
|
|
92
|
+
return join(agentDir(), "model-quality.json");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readKey(): string | undefined {
|
|
96
|
+
return env("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `openrouter/z-ai/glm-5.3` -> `glm-5-3`, `openai-codex/gpt-5.6-luna` -> `gpt-5-6-luna`. */
|
|
100
|
+
export function normalizeSlug(modelId: string): string {
|
|
101
|
+
const bare = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1) : modelId;
|
|
102
|
+
return bare.toLowerCase().replace(/[._]/g, "-").replace(/-latest$/, "").replace(/-\d{8}$/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function num(value: unknown): number | undefined {
|
|
106
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Fractions (0-1) are reported as percentages so every metric shares one scale. */
|
|
110
|
+
export function normalizedScore(key: EvaluationKey, value: number | undefined): number | undefined {
|
|
111
|
+
if (value === undefined) return undefined;
|
|
112
|
+
const definition = EVALUATIONS.find((entry) => entry.key === key);
|
|
113
|
+
return definition?.scale === "fraction" ? value * 100 : value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loadCache(): void {
|
|
117
|
+
if (cache) return;
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(readFileSync(cachePath(), "utf8")) as QualityStore & { fetchedAt?: number };
|
|
120
|
+
if (!parsed?.records) return;
|
|
121
|
+
// Migrate the pre-store format, which only had `fetchedAt`.
|
|
122
|
+
cache = {
|
|
123
|
+
records: parsed.records,
|
|
124
|
+
checkedAt: parsed.checkedAt ?? parsed.fetchedAt,
|
|
125
|
+
lastModified: parsed.lastModified,
|
|
126
|
+
etag: parsed.etag,
|
|
127
|
+
source: parsed.source ?? "artificial-analysis",
|
|
128
|
+
};
|
|
129
|
+
} catch { /* first run */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function persist(next: QualityStore): void {
|
|
133
|
+
cache = next;
|
|
134
|
+
try {
|
|
135
|
+
writeFileSync(cachePath(), JSON.stringify(next), "utf8");
|
|
136
|
+
} catch { /* cache write is best effort */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function qualityAgeMs(): number | undefined {
|
|
140
|
+
loadCache();
|
|
141
|
+
return cache?.checkedAt !== undefined ? Date.now() - cache.checkedAt : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Cache metadata, for display by /model-info refresh. */
|
|
145
|
+
export function qualityStatus(): { checkedAt?: number; lastModified?: number; records: number; hasEtag: boolean } {
|
|
146
|
+
loadCache();
|
|
147
|
+
return {
|
|
148
|
+
checkedAt: cache?.checkedAt,
|
|
149
|
+
lastModified: cache?.lastModified,
|
|
150
|
+
records: Object.keys(cache?.records ?? {}).length,
|
|
151
|
+
hasEtag: !!cache?.etag,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function qualityRecordCount(): number {
|
|
156
|
+
loadCache();
|
|
157
|
+
return Object.keys(cache?.records ?? {}).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function refreshQuality(force = false): Promise<string[]> {
|
|
161
|
+
loadCache();
|
|
162
|
+
|
|
163
|
+
// Restored from disk and still inside the window: nothing to do.
|
|
164
|
+
const age = qualityAgeMs();
|
|
165
|
+
if (!force && age !== undefined && age < REFRESH_INTERVAL_MS) return [];
|
|
166
|
+
if (inFlight) return inFlight;
|
|
167
|
+
|
|
168
|
+
inFlight = (async () => {
|
|
169
|
+
const warnings: string[] = [];
|
|
170
|
+
const key = readKey();
|
|
171
|
+
if (!key) return ["artificial-analysis: no API key, run /model-info setup"];
|
|
172
|
+
|
|
173
|
+
const stored = cache;
|
|
174
|
+
// Only revalidate when a cached body backs the validator, so a 304 can never
|
|
175
|
+
// leave the dataset empty.
|
|
176
|
+
const validator = stored && Object.keys(stored.records).length > 0 ? stored.etag : undefined;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(AA_URL, {
|
|
180
|
+
headers: {
|
|
181
|
+
"x-api-key": key,
|
|
182
|
+
...(validator ? { "if-none-match": validator } : {}),
|
|
183
|
+
},
|
|
184
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const checkedAt = Date.now();
|
|
188
|
+
|
|
189
|
+
if (response.status === 304 && stored) {
|
|
190
|
+
// Unchanged upstream: keep the dataset and the validator, stamp the check.
|
|
191
|
+
persist({ ...stored, checkedAt });
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
// Drop the etag so the next attempt re-downloads rather than revalidating
|
|
197
|
+
// against a validator we can no longer trust.
|
|
198
|
+
if (stored) persist({ ...stored, checkedAt, etag: undefined });
|
|
199
|
+
return [`artificial-analysis: HTTP ${response.status}`];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const body = await response.json() as { data?: any[] };
|
|
203
|
+
const records: Record<string, QualityRecord> = {};
|
|
204
|
+
|
|
205
|
+
for (const model of body.data ?? []) {
|
|
206
|
+
const slug = String(model.slug ?? "").toLowerCase();
|
|
207
|
+
if (!slug) continue;
|
|
208
|
+
const source = model.evaluations ?? {};
|
|
209
|
+
const evaluations: Partial<Record<EvaluationKey, number>> = {};
|
|
210
|
+
for (const { key: evaluationKey } of EVALUATIONS) {
|
|
211
|
+
const value = num(source[evaluationKey]);
|
|
212
|
+
if (value !== undefined) evaluations[evaluationKey] = value;
|
|
213
|
+
}
|
|
214
|
+
const pricing = model.pricing ?? {};
|
|
215
|
+
records[slug] = {
|
|
216
|
+
slug,
|
|
217
|
+
name: String(model.name ?? slug),
|
|
218
|
+
creator: String(model.model_creator?.name ?? "unknown"),
|
|
219
|
+
releaseDate: typeof model.release_date === "string" ? model.release_date : undefined,
|
|
220
|
+
evaluations,
|
|
221
|
+
pricing: {
|
|
222
|
+
inputPer1M: num(pricing.price_1m_input_tokens),
|
|
223
|
+
outputPer1M: num(pricing.price_1m_output_tokens),
|
|
224
|
+
blended3to1Per1M: num(pricing.price_1m_blended_3_to_1),
|
|
225
|
+
},
|
|
226
|
+
performance: {
|
|
227
|
+
outputTokensPerSecond: num(model.median_output_tokens_per_second),
|
|
228
|
+
timeToFirstTokenSeconds: num(model.median_time_to_first_token_seconds),
|
|
229
|
+
timeToFirstAnswerTokenSeconds: num(model.median_time_to_first_answer_token),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (Object.keys(records).length === 0) {
|
|
235
|
+
if (stored) persist({ ...stored, checkedAt, etag: undefined });
|
|
236
|
+
return ["artificial-analysis: empty dataset"];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lastModifiedHeader = response.headers.get("last-modified");
|
|
240
|
+
const lastModified = lastModifiedHeader ? Date.parse(lastModifiedHeader) : undefined;
|
|
241
|
+
persist({
|
|
242
|
+
records,
|
|
243
|
+
checkedAt,
|
|
244
|
+
lastModified: Number.isNaN(lastModified) ? undefined : lastModified,
|
|
245
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
246
|
+
source: "artificial-analysis",
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Network failure keeps the old dataset usable but forces a full re-fetch
|
|
250
|
+
// next time rather than trusting a stale validator.
|
|
251
|
+
if (cache) persist({ ...cache, checkedAt: Date.now(), etag: undefined });
|
|
252
|
+
warnings.push(`artificial-analysis: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
+
}
|
|
254
|
+
return warnings;
|
|
255
|
+
})().finally(() => { inFlight = undefined; });
|
|
256
|
+
|
|
257
|
+
return inFlight;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface CostEfficiency {
|
|
261
|
+
/** Intelligence index per dollar of blended 3:1 spend. */
|
|
262
|
+
intelligencePerDollar?: number;
|
|
263
|
+
codingPerDollar?: number;
|
|
264
|
+
agenticPerDollar?: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function costEfficiency(record: QualityRecord | undefined): CostEfficiency {
|
|
268
|
+
if (!record) return {};
|
|
269
|
+
const price = record.pricing.blended3to1Per1M;
|
|
270
|
+
if (price === undefined || price <= 0) return {};
|
|
271
|
+
const ratio = (value: number | undefined) => (value === undefined ? undefined : Number((value / price).toFixed(2)));
|
|
272
|
+
return {
|
|
273
|
+
intelligencePerDollar: ratio(record.evaluations.artificial_analysis_intelligence_index),
|
|
274
|
+
codingPerDollar: ratio(record.evaluations.artificial_analysis_coding_index),
|
|
275
|
+
agenticPerDollar: ratio(normalizedScore("terminalbench_hard", record.evaluations.terminalbench_hard)),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface QualityLookup {
|
|
280
|
+
record?: QualityRecord;
|
|
281
|
+
efficiency: CostEfficiency;
|
|
282
|
+
confidence: Confidence;
|
|
283
|
+
basis: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve a pi model id, preferring the entry matching the requested thinking
|
|
288
|
+
* level, then the base model, then the closest same-family sibling.
|
|
289
|
+
*/
|
|
290
|
+
export function qualityFor(modelId: string, thinkingLevel?: string): QualityLookup {
|
|
291
|
+
loadCache();
|
|
292
|
+
const records = cache?.records ?? {};
|
|
293
|
+
const base = normalizeSlug(modelId);
|
|
294
|
+
|
|
295
|
+
const levelled = thinkingLevel && thinkingLevel !== "off" ? records[`${base}-${thinkingLevel}`] : undefined;
|
|
296
|
+
if (levelled) return { record: levelled, efficiency: costEfficiency(levelled), confidence: "measured", basis: levelled.slug };
|
|
297
|
+
|
|
298
|
+
const exact = records[base];
|
|
299
|
+
if (exact) return { record: exact, efficiency: costEfficiency(exact), confidence: "measured", basis: exact.slug };
|
|
300
|
+
|
|
301
|
+
let best: QualityRecord | undefined;
|
|
302
|
+
for (const record of Object.values(records)) {
|
|
303
|
+
if (!base.includes(record.slug) && !record.slug.includes(base)) continue;
|
|
304
|
+
if (!best || record.slug.length > best.slug.length) best = record;
|
|
305
|
+
}
|
|
306
|
+
if (best) return { record: best, efficiency: costEfficiency(best), confidence: "inferred", basis: `nearest match ${best.slug}` };
|
|
307
|
+
|
|
308
|
+
return { efficiency: {}, confidence: "unrated", basis: "no Artificial Analysis entry" };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function allRecords(): QualityRecord[] {
|
|
312
|
+
loadCache();
|
|
313
|
+
return Object.values(cache?.records ?? {});
|
|
314
|
+
}
|