omp-llm-role 1.0.0 → 1.2.0
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 +202 -12
- package/agents/designer.md +4 -3
- package/docs/dev/README.md +44 -7
- package/docs/dev/agent-authoring.md +59 -1
- package/docs/dev/architecture.md +106 -28
- package/docs/dev/ci.md +163 -0
- package/docs/dev/current-state.md +364 -24
- package/docs/dev/data-sources.md +231 -38
- package/docs/dev/explorer.md +175 -46
- package/docs/dev/quirks.md +77 -4
- package/docs/dev/releasing.md +130 -17
- package/docs/dev/scoring.md +851 -27
- package/docs/dev/testing.md +253 -0
- package/docs/dev/writes-and-state.md +232 -28
- package/docs/llm-role-rankings.md +149 -148
- package/package.json +814 -80
- package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
- package/src/agent-architect.ts +148 -19
- package/src/agent-create.ts +800 -40
- package/src/agent-file.ts +2 -5
- package/src/architect-provenance.ts +16 -0
- package/src/availability.ts +191 -15
- package/src/benchmark-author.ts +9 -4
- package/src/benchmark-sources.ts +283 -18
- package/src/cli/create-role.ts +29 -1
- package/src/cli/llm-role-rank.ts +40 -1
- package/src/config-edit.ts +207 -80
- package/src/engine.ts +519 -75
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +128 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +126 -33
- package/src/extension.ts +464 -48
- package/src/features.ts +105 -0
- package/src/project-registry.ts +65 -0
- package/src/project-setup.ts +382 -0
- package/src/prompts/agent-creation-architect.md +2 -0
- package/src/prompts/project-profile-architect.md +33 -0
- package/src/prompts/project-profile-user.md +8 -0
- package/src/provider-display.ts +67 -0
- package/src/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +211 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +234 -37
- package/web/app.js +434 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
package/src/benchmark-sources.ts
CHANGED
|
@@ -44,6 +44,8 @@ const UA =
|
|
|
44
44
|
const WRITING_URL = "https://llm-stats.com/research/best-ai-for-writing/evidence.json";
|
|
45
45
|
/** The llm-stats backend that serves one benchmark's leaderboard as JSON. */
|
|
46
46
|
const LLM_STATS_BENCHMARK_URL = "https://api.zeroeval.com/leaderboard/benchmarks";
|
|
47
|
+
/** The catalog cache file under `CACHE_DIR` (shared by the loader and `cachedSourceInfo`). */
|
|
48
|
+
const CATALOG_CACHE_FILE = "benchmark-catalog-fetched-data.json";
|
|
47
49
|
|
|
48
50
|
/** How a source's ids map to the llm-stats bare id. */
|
|
49
51
|
export type JoinRule = "direct" | "slug-suffix" | "normalized";
|
|
@@ -121,6 +123,16 @@ export function catalogMetric(benchmarkId: string): string {
|
|
|
121
123
|
return SHIPPED_CATALOG_METRICS[benchmarkId] ?? externalMetricKey("bench", benchmarkId);
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
/** The declaration a catalog benchmark id needs so its metric key resolves on
|
|
127
|
+
* the next update, or `null` when none is needed: a shipped id (its static
|
|
128
|
+
* source already resolves) or a dot-free id the generic metric-key fallback
|
|
129
|
+
* reconstructs. `normalizeMetricKey` folds separators (`.`, case, …), so a
|
|
130
|
+
* dotted id's raw form survives only here — the declaration must carry it. */
|
|
131
|
+
export function catalogBenchmarkDeclaration(benchmarkId: string): SourceDeclaration | null {
|
|
132
|
+
if (SHIPPED_CATALOG_METRICS[benchmarkId] !== undefined) return null;
|
|
133
|
+
return normalizeMetricKey(benchmarkId) !== benchmarkId ? llmStatsBenchmarkDeclaration(benchmarkId) : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
// ---------------------------------------------------------------------------
|
|
125
137
|
// Parsers
|
|
126
138
|
// ---------------------------------------------------------------------------
|
|
@@ -164,6 +176,104 @@ export function parseLlmStatsBenchmark(v: unknown): Record<string, number> | nul
|
|
|
164
176
|
return out;
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The per-entry summary of an llm-stats benchmark payload, read in one pass:
|
|
181
|
+
* the trust flags (`self_reported`/`verified`) and the `multimodal` flag.
|
|
182
|
+
*
|
|
183
|
+
* `trust` is `null` when no parsed entry carried a boolean `self_reported` — a
|
|
184
|
+
* declared or writing-evidence payload has no entry flags, so the axis degrades
|
|
185
|
+
* to `unknown` rather than a silent `ok`. `verified` is uniformly `false` today
|
|
186
|
+
* (the source never sets it); it is kept for when the source starts to.
|
|
187
|
+
*
|
|
188
|
+
* `modality` is the per-entry `multimodal` summary: how many of the `entries[]`
|
|
189
|
+
* carry a boolean `multimodal` (the share denominator) and how many of those are
|
|
190
|
+
* `true`. It is `null` when no parsed entry carried the flag, so the axis
|
|
191
|
+
* degrades to `unknown` rather than a silent `ok`.
|
|
192
|
+
*
|
|
193
|
+
* `provenance` is the per-entry org distribution: each row's `organization_id`
|
|
194
|
+
* (falling back to `provider_id` when `organization_id` is not a non-empty
|
|
195
|
+
* string) is the lab a model's score comes from. `covered` counts the entries
|
|
196
|
+
* carrying a non-empty org string, `orgs` the distinct org count, and
|
|
197
|
+
* `dominantOrg`/`dominantShare` the most frequent org and its count / `covered`.
|
|
198
|
+
* It is `null` when no entry carried an org, so the axis degrades to `unknown`
|
|
199
|
+
* rather than a silent `ok`.
|
|
200
|
+
*
|
|
201
|
+
* `crossSource` is the per-entry **second source** of the price/throughput/
|
|
202
|
+
* context quantities, keyed by the entry's `model_id`: each row's
|
|
203
|
+
* `input_cost_per_million`/`output_cost_per_million`/`speed_rps`/`context_window`
|
|
204
|
+
* (each a finite number, else `null`). It is `null` when no entry carried any of
|
|
205
|
+
* the four. The focus-metric cross-source axis (scoring.md) compares it against
|
|
206
|
+
* the plugin's own `price`/`throughput`/`context`; it never feeds a weight.
|
|
207
|
+
*/
|
|
208
|
+
export type BenchmarkPayloadMeta = {
|
|
209
|
+
trust: { selfReported: number; verified: number; covered: number } | null;
|
|
210
|
+
modality: { multimodal: number; covered: number } | null;
|
|
211
|
+
provenance: { dominantOrg: string | null; dominantShare: number; orgs: number; covered: number } | null;
|
|
212
|
+
crossSource: Record<string, { input: number | null; output: number | null; speed: number | null; context: number | null }> | null;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Pure: the per-entry summary of a raw llm-stats benchmark payload, or `null`
|
|
217
|
+
* when the payload has no llm-stats entries shape. Junk rows are skipped exactly
|
|
218
|
+
* like `parseLlmStatsBenchmark`; `verified` counts the covered rows (those
|
|
219
|
+
* carrying a boolean `self_reported`), so `verified <= covered` holds. The four
|
|
220
|
+
* summaries have independent denominators (a row may carry one flag and not the
|
|
221
|
+
* others).
|
|
222
|
+
*/
|
|
223
|
+
export function parseBenchmarkPayloadMeta(v: unknown): BenchmarkPayloadMeta | null {
|
|
224
|
+
if (!isRecord(v) || !Array.isArray(v.entries)) return null;
|
|
225
|
+
let selfReported = 0;
|
|
226
|
+
let verified = 0;
|
|
227
|
+
let covered = 0;
|
|
228
|
+
let multimodal = 0;
|
|
229
|
+
let modalityCovered = 0;
|
|
230
|
+
const orgCounts = new Map<string, number>();
|
|
231
|
+
let orgCovered = 0;
|
|
232
|
+
const crossSource: Record<string, { input: number | null; output: number | null; speed: number | null; context: number | null }> = {};
|
|
233
|
+
let crossSourceCovered = 0;
|
|
234
|
+
for (const row of v.entries) {
|
|
235
|
+
if (!isRecord(row)) continue;
|
|
236
|
+
if (typeof row.self_reported === "boolean") {
|
|
237
|
+
covered++;
|
|
238
|
+
if (row.self_reported) selfReported++;
|
|
239
|
+
if (row.verified === true) verified++;
|
|
240
|
+
}
|
|
241
|
+
if (typeof row.multimodal === "boolean") {
|
|
242
|
+
modalityCovered++;
|
|
243
|
+
if (row.multimodal) multimodal++;
|
|
244
|
+
}
|
|
245
|
+
const org = typeof row.organization_id === "string" && row.organization_id !== "" ? row.organization_id : typeof row.provider_id === "string" && row.provider_id !== "" ? row.provider_id : null;
|
|
246
|
+
if (org !== null) {
|
|
247
|
+
orgCovered++;
|
|
248
|
+
orgCounts.set(org, (orgCounts.get(org) ?? 0) + 1);
|
|
249
|
+
}
|
|
250
|
+
if (typeof row.model_id === "string") {
|
|
251
|
+
const input = typeof row.input_cost_per_million === "number" && Number.isFinite(row.input_cost_per_million) ? row.input_cost_per_million : null;
|
|
252
|
+
const output = typeof row.output_cost_per_million === "number" && Number.isFinite(row.output_cost_per_million) ? row.output_cost_per_million : null;
|
|
253
|
+
const speed = typeof row.speed_rps === "number" && Number.isFinite(row.speed_rps) ? row.speed_rps : null;
|
|
254
|
+
const context = typeof row.context_window === "number" && Number.isFinite(row.context_window) ? row.context_window : null;
|
|
255
|
+
if (input !== null || output !== null || speed !== null || context !== null) {
|
|
256
|
+
crossSource[row.model_id] = { input, output, speed, context };
|
|
257
|
+
crossSourceCovered++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let dominantOrg: string | null = null;
|
|
262
|
+
let dominantCount = 0;
|
|
263
|
+
for (const [org, count] of orgCounts) {
|
|
264
|
+
if (count > dominantCount) {
|
|
265
|
+
dominantOrg = org;
|
|
266
|
+
dominantCount = count;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
trust: covered === 0 ? null : { selfReported, verified, covered },
|
|
271
|
+
modality: modalityCovered === 0 ? null : { multimodal, covered: modalityCovered },
|
|
272
|
+
provenance: orgCovered === 0 ? null : { dominantOrg, dominantShare: dominantCount / orgCovered, orgs: orgCounts.size, covered: orgCovered },
|
|
273
|
+
crossSource: crossSourceCovered === 0 ? null : crossSource,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
167
277
|
/** One row of the llm-stats benchmark catalog (`GET /leaderboard/benchmarks`). */
|
|
168
278
|
export type BenchmarkCatalogEntry = {
|
|
169
279
|
id: string;
|
|
@@ -171,6 +281,39 @@ export type BenchmarkCatalogEntry = {
|
|
|
171
281
|
description: string;
|
|
172
282
|
categories: string[];
|
|
173
283
|
modelCount: number;
|
|
284
|
+
/** The catalog row's `is_community` flag (a community-submitted benchmark);
|
|
285
|
+
* `false` when absent or non-boolean. The only catalog-level trust signal. */
|
|
286
|
+
isCommunity: boolean;
|
|
287
|
+
/** The catalog row's `modality` (e.g. `"text"`, `"image"`, `"audio"`,
|
|
288
|
+
* `"video"`, `"multimodal"`); `null` when absent or non-string. Kept as an
|
|
289
|
+
* open string (not a closed enum) so a future value survives. */
|
|
290
|
+
modality: string | null;
|
|
291
|
+
/** The catalog row's `updated_at` (when the dataset was last updated); `null`
|
|
292
|
+
* when absent or non-string. Drives the maintenance axis's dataset age
|
|
293
|
+
* (distinct from the model-date freshness axis). */
|
|
294
|
+
updatedAt: string | null;
|
|
295
|
+
/** The catalog row's `version_count` (how many revisions the dataset has);
|
|
296
|
+
* `null` when absent or non-finite. Supporting detail for the maintenance
|
|
297
|
+
* axis (churn). */
|
|
298
|
+
versionCount: number | null;
|
|
299
|
+
/** The catalog row's `latest_version_row_count` (the current dataset size);
|
|
300
|
+
* `null` when absent or non-finite. Supporting detail for the maintenance
|
|
301
|
+
* axis. */
|
|
302
|
+
latestVersionRowCount: number | null;
|
|
303
|
+
/** The catalog row's `star_count`; `null` when absent or non-finite.
|
|
304
|
+
* Supporting detail for the maintenance axis. */
|
|
305
|
+
starCount: number | null;
|
|
306
|
+
/** The catalog row's `dataset_id` (the benchmark dataset's id); `null` when
|
|
307
|
+
* absent or non-string. Provenance detail (the dataset the benchmark is built
|
|
308
|
+
* on). */
|
|
309
|
+
datasetId: string | null;
|
|
310
|
+
/** The catalog row's `dataset_org_id` (the owning organisation of the
|
|
311
|
+
* benchmark dataset); `null` when absent or non-string. The provenance axis's
|
|
312
|
+
* owner — who publishes the benchmark. */
|
|
313
|
+
datasetOrgId: string | null;
|
|
314
|
+
/** The catalog row's `dataset_slug`; `null` when absent or non-string. The
|
|
315
|
+
* provenance axis's owner fallback when `dataset_org_id` is absent. */
|
|
316
|
+
datasetSlug: string | null;
|
|
174
317
|
};
|
|
175
318
|
|
|
176
319
|
/** Pure: the catalog payload (a top-level array) -> entries; null when unusable. */
|
|
@@ -188,14 +331,54 @@ export function parseBenchmarkCatalog(v: unknown): BenchmarkCatalogEntry[] | nul
|
|
|
188
331
|
description: typeof row.description === "string" ? row.description : "",
|
|
189
332
|
categories: Array.isArray(row.categories) ? row.categories.filter((c): c is string => typeof c === "string") : [],
|
|
190
333
|
modelCount: typeof row.model_count === "number" && Number.isFinite(row.model_count) ? row.model_count : 0,
|
|
334
|
+
isCommunity: row.is_community === true,
|
|
335
|
+
modality: typeof row.modality === "string" && row.modality !== "" ? row.modality : null,
|
|
336
|
+
updatedAt: typeof row.updated_at === "string" ? row.updated_at : null,
|
|
337
|
+
versionCount: typeof row.version_count === "number" && Number.isFinite(row.version_count) ? row.version_count : null,
|
|
338
|
+
latestVersionRowCount:
|
|
339
|
+
typeof row.latest_version_row_count === "number" && Number.isFinite(row.latest_version_row_count) ? row.latest_version_row_count : null,
|
|
340
|
+
starCount: typeof row.star_count === "number" && Number.isFinite(row.star_count) ? row.star_count : null,
|
|
341
|
+
datasetId: typeof row.dataset_id === "string" && row.dataset_id !== "" ? row.dataset_id : null,
|
|
342
|
+
datasetOrgId: typeof row.dataset_org_id === "string" && row.dataset_org_id !== "" ? row.dataset_org_id : null,
|
|
343
|
+
datasetSlug: typeof row.dataset_slug === "string" && row.dataset_slug !== "" ? row.dataset_slug : null,
|
|
191
344
|
});
|
|
192
345
|
}
|
|
193
346
|
return out;
|
|
194
347
|
}
|
|
195
348
|
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
349
|
+
/** The llm-stats per-benchmark endpoint's hard cap on `entries`: it returns at
|
|
350
|
+
* most this many rows regardless of `limit`/`offset`/`page`/`per_page`, so a
|
|
351
|
+
* generic `bench:<id>` metric can never load more and no pagination loop can
|
|
352
|
+
* help. `total_models` still reports the full set, so `loaded < total` marks a
|
|
353
|
+
* capped load. */
|
|
354
|
+
export const BENCHMARK_ENTRY_CAP = 20;
|
|
355
|
+
|
|
356
|
+
/** A parsed benchmark payload: the joined scores plus the loadable entry count
|
|
357
|
+
* and the payload's declared total (`total_models`), so the caller can annotate
|
|
358
|
+
* a capped load. `meta` carries the payload's per-entry trust summary; it is
|
|
359
|
+
* `null` when the payload has no llm-stats entries shape (a declared or
|
|
360
|
+
* writing-evidence payload carries no entry flags). */
|
|
361
|
+
export type BenchmarkPayload = {
|
|
362
|
+
scores: Record<string, number>;
|
|
363
|
+
/** Entries actually read from the payload — the loadable count. */
|
|
364
|
+
loaded: number;
|
|
365
|
+
/** The payload's declared total model count, when it carries one. */
|
|
366
|
+
total: number | null;
|
|
367
|
+
/** The payload's per-entry trust summary; `null` when the payload has no
|
|
368
|
+
* llm-stats entries shape. */
|
|
369
|
+
meta: BenchmarkPayloadMeta | null;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** Pure: run a source's parser over a payload, carrying the loadable count (the
|
|
373
|
+
* post-fetch coverage count) and the payload's declared total so the caller can
|
|
374
|
+
* annotate `loaded < total` — never the catalog's `model_count`. The trust meta
|
|
375
|
+
* is read independently of `source.parse`, so a declared/writing source (whose
|
|
376
|
+
* parser reads no entry flags) still reports `meta: null`. */
|
|
377
|
+
export function parseBenchmarkPayload(source: BenchmarkSource, payload: unknown): BenchmarkPayload | null {
|
|
378
|
+
const scores = source.parse(payload);
|
|
379
|
+
if (scores === null) return null;
|
|
380
|
+
const total = isRecord(payload) && typeof payload.total_models === "number" && Number.isFinite(payload.total_models) ? payload.total_models : null;
|
|
381
|
+
return { scores, loaded: Object.keys(scores).length, total, meta: parseBenchmarkPayloadMeta(payload) };
|
|
199
382
|
}
|
|
200
383
|
|
|
201
384
|
// ---------------------------------------------------------------------------
|
|
@@ -587,10 +770,11 @@ export function applyBenchmarkScores(models: Model[], source: BenchmarkSource, s
|
|
|
587
770
|
// Cache chain
|
|
588
771
|
// ---------------------------------------------------------------------------
|
|
589
772
|
|
|
590
|
-
type ScoresCacheFile = { fetchedAt: string; source: string; scores: Record<string, number
|
|
773
|
+
type ScoresCacheFile = { fetchedAt: string; source: string; scores: Record<string, number>; total?: number | null; meta?: BenchmarkPayloadMeta | null };
|
|
591
774
|
|
|
592
|
-
/** Daily cache: current when fetchedAt is the current UTC day; requireFresh=false accepts stale.
|
|
593
|
-
|
|
775
|
+
/** Daily cache: current when fetchedAt is the current UTC day; requireFresh=false accepts stale.
|
|
776
|
+
* Exported for the shape-guard test (a cache predating the payload-meta fields must be rejected). */
|
|
777
|
+
export function readScoresCache(path: string, requireFresh: boolean): ScoresCacheFile | null {
|
|
594
778
|
let parsed: ScoresCacheFile;
|
|
595
779
|
try {
|
|
596
780
|
parsed = JSON.parse(readFileSync(path, "utf8")) as ScoresCacheFile;
|
|
@@ -598,19 +782,45 @@ function readScoresCache(path: string, requireFresh: boolean): ScoresCacheFile |
|
|
|
598
782
|
return null;
|
|
599
783
|
}
|
|
600
784
|
if (typeof parsed?.scores !== "object" || parsed.scores === null) return null;
|
|
785
|
+
// A cache written before the payload-meta fields (#34/#35/#37/#38) landed lacks
|
|
786
|
+
// the `meta` key; refetch rather than silently reporting `undefined` (the four
|
|
787
|
+
// payload-derived focus axes would go inert). `meta` is legitimately `null` for
|
|
788
|
+
// a declared/writing payload, so the guard keys on the key, not its value.
|
|
789
|
+
if (!("meta" in parsed)) return null;
|
|
601
790
|
if (requireFresh && parsed.fetchedAt?.slice(0, 10) !== new Date().toISOString().slice(0, 10)) return null;
|
|
602
791
|
return parsed;
|
|
603
792
|
}
|
|
604
793
|
|
|
605
794
|
/** Pretty-printed with sorted keys for scannable diffs, mirroring the other caches. */
|
|
606
|
-
function writeScoresCache(path: string, fetchedAt: string, source: string, scores: Record<string, number
|
|
607
|
-
const cache: ScoresCacheFile = { fetchedAt, source, scores };
|
|
795
|
+
function writeScoresCache(path: string, fetchedAt: string, source: string, scores: Record<string, number>, total: number | null, meta: BenchmarkPayloadMeta | null): void {
|
|
796
|
+
const cache: ScoresCacheFile = { fetchedAt, source, scores, total, meta };
|
|
608
797
|
mkdirSync(dirname(path), { recursive: true });
|
|
609
798
|
writeFileSync(path, JSON.stringify(cache, null, 2));
|
|
610
799
|
console.error(`wrote ${path}`);
|
|
611
800
|
}
|
|
612
801
|
|
|
613
|
-
|
|
802
|
+
/** Annotate a capped load: the llm-stats endpoint returns at most
|
|
803
|
+
* `BENCHMARK_ENTRY_CAP` entries, so the loadable count (the post-fetch coverage
|
|
804
|
+
* count) is below the payload's `total_models`. */
|
|
805
|
+
function annotateCappedLoad(source: BenchmarkSource, loaded: number, total: number | null): void {
|
|
806
|
+
if (total !== null && loaded < total) {
|
|
807
|
+
console.error(
|
|
808
|
+
`${source.id}: loaded ${loaded}/${total} models — the endpoint caps entries at ${BENCHMARK_ENTRY_CAP}; ${source.metric} covers only the loadable ${loaded}`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export type BenchmarkScores = {
|
|
814
|
+
scores: Record<string, number>;
|
|
815
|
+
label: string;
|
|
816
|
+
/** Entries actually loaded — the post-fetch coverage count, never the catalog count. */
|
|
817
|
+
loaded: number;
|
|
818
|
+
/** The payload's declared total model count, when known. */
|
|
819
|
+
total: number | null;
|
|
820
|
+
/** The payload's per-entry trust summary; `null` when the payload carried no
|
|
821
|
+
* entry flags (a declared/writing source) or the cache predates the field. */
|
|
822
|
+
meta: BenchmarkPayloadMeta | null;
|
|
823
|
+
};
|
|
614
824
|
|
|
615
825
|
/** Fetch a URL as JSON (the cache chain and the authoring dry-run). Throws on a
|
|
616
826
|
* non-200 or a non-JSON body. */
|
|
@@ -646,16 +856,20 @@ export async function loadBenchmarkScores(source: BenchmarkSource, refresh: bool
|
|
|
646
856
|
const cached = readScoresCache(path, true);
|
|
647
857
|
if (cached) {
|
|
648
858
|
console.error(`using cache ${path} (fetched ${cached.fetchedAt})`);
|
|
649
|
-
|
|
859
|
+
const loaded = Object.keys(cached.scores).length;
|
|
860
|
+
const total = cached.total ?? null;
|
|
861
|
+
annotateCappedLoad(source, loaded, total);
|
|
862
|
+
return { scores: cached.scores, label: source.label, loaded, total, meta: cached.meta ?? null };
|
|
650
863
|
}
|
|
651
864
|
}
|
|
652
865
|
if (source.fetch) {
|
|
653
866
|
try {
|
|
654
867
|
const payload = await fetchJson(source.fetch.url, source.fetch.method, source.fetch.body);
|
|
655
|
-
const
|
|
656
|
-
if (
|
|
657
|
-
writeScoresCache(path, new Date().toISOString(), source.fetch.url, scores);
|
|
658
|
-
|
|
868
|
+
const parsed = parseBenchmarkPayload(source, payload);
|
|
869
|
+
if (parsed && parsed.loaded > 0) {
|
|
870
|
+
writeScoresCache(path, new Date().toISOString(), source.fetch.url, parsed.scores, parsed.total, parsed.meta);
|
|
871
|
+
annotateCappedLoad(source, parsed.loaded, parsed.total);
|
|
872
|
+
return { scores: parsed.scores, label: source.labelFromPayload?.(payload) ?? source.label, loaded: parsed.loaded, total: parsed.total, meta: parsed.meta };
|
|
659
873
|
}
|
|
660
874
|
console.error(`${source.id}: unexpected payload shape; skipping`);
|
|
661
875
|
} catch (err) {
|
|
@@ -665,15 +879,20 @@ export async function loadBenchmarkScores(source: BenchmarkSource, refresh: bool
|
|
|
665
879
|
const stale = readScoresCache(path, false);
|
|
666
880
|
if (stale) {
|
|
667
881
|
console.error(`using stale cache ${path} (fetched ${stale.fetchedAt})`);
|
|
668
|
-
|
|
882
|
+
const loaded = Object.keys(stale.scores).length;
|
|
883
|
+
const total = stale.total ?? null;
|
|
884
|
+
annotateCappedLoad(source, loaded, total);
|
|
885
|
+
return { scores: stale.scores, label: source.label, loaded, total, meta: stale.meta ?? null };
|
|
669
886
|
}
|
|
670
887
|
return null;
|
|
671
888
|
}
|
|
672
889
|
|
|
673
890
|
type CatalogCacheFile = { fetchedAt: string; source: string; entries: BenchmarkCatalogEntry[] };
|
|
674
891
|
|
|
675
|
-
/** Daily cache: current when fetchedAt is the current UTC day; requireFresh=false accepts stale.
|
|
676
|
-
|
|
892
|
+
/** Daily cache: current when fetchedAt is the current UTC day; requireFresh=false accepts stale.
|
|
893
|
+
* Exported for the shape-guard test (a cache predating `isCommunity`/`modality`, the
|
|
894
|
+
* maintenance fields or the provenance fields must be rejected). */
|
|
895
|
+
export function readCatalogCache(path: string, requireFresh: boolean): CatalogCacheFile | null {
|
|
677
896
|
let parsed: CatalogCacheFile;
|
|
678
897
|
try {
|
|
679
898
|
parsed = JSON.parse(readFileSync(path, "utf8")) as CatalogCacheFile;
|
|
@@ -681,6 +900,27 @@ function readCatalogCache(path: string, requireFresh: boolean): CatalogCacheFile
|
|
|
681
900
|
return null;
|
|
682
901
|
}
|
|
683
902
|
if (!Array.isArray(parsed?.entries)) return null;
|
|
903
|
+
// A cache written before `isCommunity`/`modality` (#34/#35), the maintenance
|
|
904
|
+
// fields `updatedAt`/`versionCount`/`latestVersionRowCount`/`starCount` (#36)
|
|
905
|
+
// or the provenance fields `datasetId`/`datasetOrgId`/`datasetSlug` (#37)
|
|
906
|
+
// landed lacks the field; refetch rather than silently reporting `undefined`
|
|
907
|
+
// (mirrors `readEndpointsCache`). An empty catalog is still `null` (nothing to
|
|
908
|
+
// sample, nothing to discover).
|
|
909
|
+
const sample = parsed.entries[0];
|
|
910
|
+
if (
|
|
911
|
+
typeof sample !== "object" ||
|
|
912
|
+
sample === null ||
|
|
913
|
+
!("isCommunity" in sample) ||
|
|
914
|
+
!("modality" in sample) ||
|
|
915
|
+
!("updatedAt" in sample) ||
|
|
916
|
+
!("versionCount" in sample) ||
|
|
917
|
+
!("latestVersionRowCount" in sample) ||
|
|
918
|
+
!("starCount" in sample) ||
|
|
919
|
+
!("datasetId" in sample) ||
|
|
920
|
+
!("datasetOrgId" in sample) ||
|
|
921
|
+
!("datasetSlug" in sample)
|
|
922
|
+
)
|
|
923
|
+
return null;
|
|
684
924
|
if (requireFresh && parsed.fetchedAt?.slice(0, 10) !== new Date().toISOString().slice(0, 10)) return null;
|
|
685
925
|
return parsed;
|
|
686
926
|
}
|
|
@@ -700,7 +940,7 @@ function writeCatalogCache(path: string, fetchedAt: string, source: string, entr
|
|
|
700
940
|
* payload is never cached, so the next run retries.
|
|
701
941
|
*/
|
|
702
942
|
export async function loadBenchmarkCatalog(refresh: boolean, cacheDir = CACHE_DIR): Promise<BenchmarkCatalogEntry[] | null> {
|
|
703
|
-
const path = join(cacheDir,
|
|
943
|
+
const path = join(cacheDir, CATALOG_CACHE_FILE);
|
|
704
944
|
if (!refresh) {
|
|
705
945
|
const cached = readCatalogCache(path, true);
|
|
706
946
|
if (cached) {
|
|
@@ -727,6 +967,31 @@ export async function loadBenchmarkCatalog(refresh: boolean, cacheDir = CACHE_DI
|
|
|
727
967
|
return null;
|
|
728
968
|
}
|
|
729
969
|
|
|
970
|
+
/** The cached source inputs for one focus metric: the payload's per-entry
|
|
971
|
+
* summary (trust/modality/provenance/cross-source, from the source's scores
|
|
972
|
+
* cache) and the catalog row (from the catalog cache). Either half is `null`
|
|
973
|
+
* when its cache is absent or predates the field. */
|
|
974
|
+
export type FocusSourceInfo = {
|
|
975
|
+
payload?: BenchmarkPayloadMeta | null;
|
|
976
|
+
catalog?: BenchmarkCatalogEntry | null;
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* The cached source inputs for a focus metric, with no network: the source's
|
|
981
|
+
* scores cache (if any) for the payload meta, and the catalog cache (if any) for
|
|
982
|
+
* the matching row. The metric is reverse-mapped to a catalog id through
|
|
983
|
+
* `catalogMetric` (a shipped key via `SHIPPED_CATALOG_METRICS`, else the
|
|
984
|
+
* `bench:<local>` form), so a shipped metric and a generic `bench:<id>` both
|
|
985
|
+
* resolve. `null` for either half when the cache is absent or the metric is
|
|
986
|
+
* unknown to the catalog.
|
|
987
|
+
*/
|
|
988
|
+
export function cachedSourceInfo(metric: string, declared: readonly SourceDeclaration[] = [], cacheDir = CACHE_DIR): FocusSourceInfo {
|
|
989
|
+
const source = sourceForMetric(metric, declared);
|
|
990
|
+
const payload = source === null ? null : readScoresCache(join(cacheDir, source.cacheFile), false)?.meta ?? null;
|
|
991
|
+
const catalog = readCatalogCache(join(cacheDir, CATALOG_CACHE_FILE), false);
|
|
992
|
+
return { payload, catalog: catalog?.entries.find((entry) => catalogMetric(entry.id) === metric) ?? null };
|
|
993
|
+
}
|
|
994
|
+
|
|
730
995
|
// ---------------------------------------------------------------------------
|
|
731
996
|
// Dry run (the authoring step's guardrail)
|
|
732
997
|
// ---------------------------------------------------------------------------
|
package/src/cli/create-role.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import type { RoleDef, SuffixLevel } from "../engine.ts";
|
|
23
|
+
import { FEATURES, featureById, formatFeatures } from "../features.ts";
|
|
23
24
|
import { writeRoleSettings } from "../role-settings.ts";
|
|
24
25
|
|
|
25
26
|
type CreateRoleArgs = {
|
|
@@ -30,6 +31,8 @@ type CreateRoleArgs = {
|
|
|
30
31
|
description: string;
|
|
31
32
|
image: boolean;
|
|
32
33
|
lambda: number | undefined;
|
|
34
|
+
features: Record<string, boolean> | undefined;
|
|
35
|
+
preferOwnProvider: boolean;
|
|
33
36
|
lockPath: string;
|
|
34
37
|
dryRun: boolean;
|
|
35
38
|
json: boolean;
|
|
@@ -45,6 +48,10 @@ const USAGE = [
|
|
|
45
48
|
" --description <text> one-line role description",
|
|
46
49
|
" --image require image input (filters.image)",
|
|
47
50
|
" --lambda <n> explicit λ override ($ per quality point)",
|
|
51
|
+
" --feature <id,...> capability flag(s) to author on the role (see --list-features);",
|
|
52
|
+
" repeatable, comma-separated, merged",
|
|
53
|
+
" --prefer-own-provider bind the role to each model's best provider route where one exists",
|
|
54
|
+
" --list-features print the capability flags and the settings each applies, then exit",
|
|
48
55
|
" --lock <path> settings lock file (default ~/.omp/plugins/omp-plugins.lock.json)",
|
|
49
56
|
" --dry-run validate and print without writing",
|
|
50
57
|
" --json print the result as JSON",
|
|
@@ -84,6 +91,8 @@ function parseArgs(argv: string[]): CreateRoleArgs {
|
|
|
84
91
|
description: "",
|
|
85
92
|
image: false,
|
|
86
93
|
lambda: undefined,
|
|
94
|
+
features: undefined,
|
|
95
|
+
preferOwnProvider: false,
|
|
87
96
|
lockPath: join(homedir(), ".omp", "plugins", "omp-plugins.lock.json"),
|
|
88
97
|
dryRun: false,
|
|
89
98
|
json: false,
|
|
@@ -94,6 +103,10 @@ function parseArgs(argv: string[]): CreateRoleArgs {
|
|
|
94
103
|
args.image = true;
|
|
95
104
|
continue;
|
|
96
105
|
}
|
|
106
|
+
if (flag === "--prefer-own-provider") {
|
|
107
|
+
args.preferOwnProvider = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
97
110
|
if (flag === "--dry-run") {
|
|
98
111
|
args.dryRun = true;
|
|
99
112
|
continue;
|
|
@@ -102,6 +115,10 @@ function parseArgs(argv: string[]): CreateRoleArgs {
|
|
|
102
115
|
args.json = true;
|
|
103
116
|
continue;
|
|
104
117
|
}
|
|
118
|
+
if (flag === "--list-features") {
|
|
119
|
+
console.log(formatFeatures());
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
105
122
|
if (flag === "--help" || flag === "-h") {
|
|
106
123
|
console.log(USAGE);
|
|
107
124
|
process.exit(0);
|
|
@@ -114,7 +131,16 @@ function parseArgs(argv: string[]): CreateRoleArgs {
|
|
|
114
131
|
else if (flag === "--thinking") args.thinking = value as SuffixLevel;
|
|
115
132
|
else if (flag === "--description") args.description = value;
|
|
116
133
|
else if (flag === "--lambda") args.lambda = Number(value);
|
|
117
|
-
else if (flag === "--
|
|
134
|
+
else if (flag === "--feature") {
|
|
135
|
+
const ids = parseList(value);
|
|
136
|
+
if (ids.length === 0) fail(`--feature: no capability id given\n\n${USAGE}`);
|
|
137
|
+
const features = args.features ?? {};
|
|
138
|
+
for (const id of ids) {
|
|
139
|
+
if (featureById(id) === null) fail(`--feature: unknown capability "${id}" — one of ${FEATURES.map((f) => f.id).join(", ")}`);
|
|
140
|
+
features[id] = true;
|
|
141
|
+
}
|
|
142
|
+
args.features = features;
|
|
143
|
+
} else if (flag === "--lock") args.lockPath = value;
|
|
118
144
|
else fail(`unknown flag "${flag}"\n\n${USAGE}`);
|
|
119
145
|
}
|
|
120
146
|
if (!args.name) fail(`--name is required\n\n${USAGE}`);
|
|
@@ -131,6 +157,8 @@ function buildRoleDef(args: CreateRoleArgs): RoleDef {
|
|
|
131
157
|
if (args.thinking !== undefined) def.thinking = args.thinking;
|
|
132
158
|
if (args.image) def.filters = { image: true };
|
|
133
159
|
if (args.lambda !== undefined) def.lambda = args.lambda;
|
|
160
|
+
if (args.features !== undefined) def.features = args.features;
|
|
161
|
+
if (args.preferOwnProvider) def.preferOwnProvider = true;
|
|
134
162
|
return def;
|
|
135
163
|
}
|
|
136
164
|
|
package/src/cli/llm-role-rank.ts
CHANGED
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
import { execFile } from "node:child_process";
|
|
29
29
|
import { writeFileSync } from "node:fs";
|
|
30
30
|
import { promisify } from "node:util";
|
|
31
|
+
import { assessFocusMetric, type FocusMetricAssessment } from "../agent-create.ts";
|
|
31
32
|
import { catalogFromOmpModelsJson, enrichThinkingLevels, resolveVariant, type CatalogEntry } from "../availability.ts";
|
|
33
|
+
import { cachedSourceInfo, loadDeclaredSources } from "../benchmark-sources.ts";
|
|
32
34
|
import { computeRankings, loadRankData, paretoFrontier, roleLambda, thinkingPriceFactor, type Model, type Ranked, type RoleDef } from "../engine.ts";
|
|
35
|
+
import { focusMetricsOf } from "../explorer/explain.ts";
|
|
33
36
|
import { DEFAULT_ROLES as ROLES } from "../settings.ts";
|
|
34
37
|
|
|
35
38
|
const execFileP = promisify(execFile);
|
|
@@ -65,6 +68,37 @@ function metricAbbr(metric: string): string {
|
|
|
65
68
|
return local.slice(0, 10);
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** One focus-metric annotation line: the nine signals, same rule as the report. */
|
|
72
|
+
function focusLine(metric: string, a: FocusMetricAssessment): string {
|
|
73
|
+
const c = a.coverage;
|
|
74
|
+
const coverage =
|
|
75
|
+
c.status === "unknown" ? "unknown" : `${c.covered}/${c.total} (${c.share === null ? "?" : `${(c.share * 100).toFixed(1)}%`}) ${c.status}`;
|
|
76
|
+
const dispersion = a.dispersion.status === "unknown" ? "unknown" : `${a.dispersion.value?.toFixed(3)} ${a.dispersion.status}`;
|
|
77
|
+
const composition =
|
|
78
|
+
a.composition.status === "unknown" ? "unknown" : a.composition.status === "ok" ? "ok" : `omits ${a.composition.omittedOrgs.join(", ")}`;
|
|
79
|
+
const freshness = a.freshness.status === "unknown" ? "unknown" : `${a.freshness.monthsBehind?.toFixed(1)}mo ${a.freshness.status}`;
|
|
80
|
+
const trust =
|
|
81
|
+
a.trust.status === "unknown"
|
|
82
|
+
? a.trust.community
|
|
83
|
+
? "community"
|
|
84
|
+
: "unknown"
|
|
85
|
+
: `${a.trust.selfReported}/${a.trust.covered} ${a.trust.status}${a.trust.community ? " community" : ""}`;
|
|
86
|
+
const modality =
|
|
87
|
+
a.modality.status === "unknown"
|
|
88
|
+
? "unknown"
|
|
89
|
+
: `${a.modality.value ?? "?"}${a.modality.multimodalShare === null ? "" : ` ${(a.modality.multimodalShare * 100).toFixed(0)}% multimodal`}`;
|
|
90
|
+
const maintenance = a.maintenance.status === "unknown" ? "unknown" : `${a.maintenance.monthsOld?.toFixed(1)}mo ${a.maintenance.status}`;
|
|
91
|
+
const provenance =
|
|
92
|
+
a.provenance.status === "unknown"
|
|
93
|
+
? "unknown"
|
|
94
|
+
: `${a.provenance.owner ?? "?"}${a.provenance.dominantShare === null ? "" : ` ${(a.provenance.dominantShare * 100).toFixed(0)}% ${a.provenance.dominantOrg ?? "?"}`} ${a.provenance.compositionAgreement}`;
|
|
95
|
+
const crossSource =
|
|
96
|
+
a.crossSource.status === "unknown"
|
|
97
|
+
? "unknown"
|
|
98
|
+
: `${a.crossSource.compared} priceΔ${a.crossSource.priceDivergence === null ? "?" : `${(a.crossSource.priceDivergence * 100).toFixed(0)}%`} ctxΔ${a.crossSource.contextDivergence === null ? "?" : `${(a.crossSource.contextDivergence * 100).toFixed(0)}%`} ${a.crossSource.status}`;
|
|
99
|
+
return `focus: ${metric} — coverage ${coverage}; dispersion ${dispersion}; composition ${composition}; freshness ${freshness}; trust ${trust}; modality ${modality}; maintenance ${maintenance}; provenance ${provenance}; cross-source ${crossSource}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
68
102
|
function formatRankings(
|
|
69
103
|
rankings: Record<string, Ranked[]>,
|
|
70
104
|
roles: Record<string, RoleDef>,
|
|
@@ -85,7 +119,7 @@ function formatRankings(
|
|
|
85
119
|
"(no ranks): llm-stats index_* affine (v+20)/80 (interval scale, observed −16..+60),",
|
|
86
120
|
"benchmarks chance-anchored pass rates, throughput log-anchored 10..300 tok/s.",
|
|
87
121
|
"q = Σ weight × metric over the quality metrics (weights renormalized excluding",
|
|
88
|
-
"price); value = q − λ·$/M sorts each role. λ =
|
|
122
|
+
"price); value = q − λ·$/M sorts each role. λ = w/(1−w) ÷ $20 (w = price weight), per-role",
|
|
89
123
|
"override via plugin settings roles.<role>.lambda. Metric columns show weighted",
|
|
90
124
|
"contributions and sum to q; — = metric missing (contributes 0).",
|
|
91
125
|
"Roles with a thinking level rank on the thinking-adjusted price: the billed",
|
|
@@ -124,6 +158,7 @@ function formatRankings(
|
|
|
124
158
|
);
|
|
125
159
|
lines.push("");
|
|
126
160
|
|
|
161
|
+
const declared = loadDeclaredSources();
|
|
127
162
|
for (const [role, def] of Object.entries(roles)) {
|
|
128
163
|
const ranked = rankings[role] ?? [];
|
|
129
164
|
lines.push(`## @${role} — ${def.description}`);
|
|
@@ -189,6 +224,10 @@ function formatRankings(
|
|
|
189
224
|
`| ${widths.map((w, c) => (c === 3 || c === 4 ? "-".repeat(w) : `${"-".repeat(w - 1)}:`)).join(" | ")} |`,
|
|
190
225
|
);
|
|
191
226
|
for (let ri = 1; ri < padded.length; ri++) lines.push(`| ${padded[ri].join(" | ")} |`);
|
|
227
|
+
// A role's focus metrics (the benchmarks it was ranked on) carry the same
|
|
228
|
+
// nine-axis annotation as the create-agent report. Shipped roles weight index
|
|
229
|
+
// metrics, so this is empty for them.
|
|
230
|
+
for (const metric of focusMetricsOf(def, declared)) lines.push(focusLine(metric, assessFocusMetric(models, metric, declared, cachedSourceInfo(metric, declared))));
|
|
192
231
|
lines.push("");
|
|
193
232
|
}
|
|
194
233
|
return lines.join("\n");
|