catherd-cli 1.4.0 → 2.0.0-beta.20261022113309
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 +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -36
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -373
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- package/src/services/treat-likes.ts +0 -97
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { type SourceTransport, sourceGet } from "./http.ts";
|
|
3
|
-
import { perMillion } from "./openrouter-models.ts";
|
|
4
|
-
import { num } from "./rows.ts";
|
|
5
|
-
|
|
6
|
-
/** Spec 1.2 §3.1: price and effort support, a cross-check only (MIT). */
|
|
7
|
-
export const LITELLM_URL =
|
|
8
|
-
"https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
9
|
-
|
|
10
|
-
export const fetchLiteLlm = async (t: SourceTransport = {}): Promise<unknown> =>
|
|
11
|
-
(await sourceGet(LITELLM_URL, t)).json();
|
|
12
|
-
|
|
13
|
-
/** The efforts LiteLLM flags per model (`supports_<effort>_reasoning_effort`). */
|
|
14
|
-
export const LITELLM_EFFORTS = ["none", "minimal", "low", "xhigh", "max"] as const;
|
|
15
|
-
|
|
16
|
-
export interface LiteLlmModel {
|
|
17
|
-
id: string;
|
|
18
|
-
provider: string;
|
|
19
|
-
/** dollars per million tokens */
|
|
20
|
-
price: { input: number; output: number; cached: number | null } | null;
|
|
21
|
-
context: number | null;
|
|
22
|
-
/** effort → whether LiteLLM says the model supports it; absent when it does not say */
|
|
23
|
-
efforts: Partial<Record<(typeof LITELLM_EFFORTS)[number], boolean>>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Spec 1.2 §3.5: the vendors' own APIs, whose prices the catalog's are. */
|
|
27
|
-
const FIRST_PARTY = new Set(["openai", "anthropic"]);
|
|
28
|
-
|
|
29
|
-
const EntrySchema = z.looseObject({ litellm_provider: z.string() });
|
|
30
|
-
|
|
31
|
-
/** The first-party entries of LiteLLM's price file: an OpenAI or Anthropic provider, a key with no `/`. */
|
|
32
|
-
export function parseLiteLlm(raw: unknown): LiteLlmModel[] {
|
|
33
|
-
if (!raw || typeof raw !== "object") return [];
|
|
34
|
-
const out: LiteLlmModel[] = [];
|
|
35
|
-
for (const [id, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
36
|
-
const e = EntrySchema.safeParse(v);
|
|
37
|
-
if (!e.success || !FIRST_PARTY.has(e.data.litellm_provider) || id.includes("/")) continue;
|
|
38
|
-
const entry = e.data as Record<string, unknown>;
|
|
39
|
-
const input = perMillion(entry.input_cost_per_token);
|
|
40
|
-
const output = perMillion(entry.output_cost_per_token);
|
|
41
|
-
const efforts: LiteLlmModel["efforts"] = {};
|
|
42
|
-
for (const effort of LITELLM_EFFORTS) {
|
|
43
|
-
const flag = entry[`supports_${effort}_reasoning_effort`];
|
|
44
|
-
if (typeof flag === "boolean") efforts[effort] = flag;
|
|
45
|
-
}
|
|
46
|
-
out.push({
|
|
47
|
-
id,
|
|
48
|
-
provider: e.data.litellm_provider,
|
|
49
|
-
price:
|
|
50
|
-
input !== null && output !== null
|
|
51
|
-
? { input, output, cached: perMillion(entry.cache_read_input_token_cost) }
|
|
52
|
-
: null,
|
|
53
|
-
context: num(entry.max_input_tokens),
|
|
54
|
-
efforts,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { type SourceTransport, sourceGet } from "./http.ts";
|
|
3
|
-
import { num } from "./rows.ts";
|
|
4
|
-
|
|
5
|
-
/** Spec 1.2 §3.1: efforts, context, output limit, price, tool calling, modalities, release date (MIT). */
|
|
6
|
-
export const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
7
|
-
|
|
8
|
-
export const fetchModelsDev = async (t: SourceTransport = {}): Promise<unknown> =>
|
|
9
|
-
(await sourceGet(MODELS_DEV_URL, t)).json();
|
|
10
|
-
|
|
11
|
-
/** What catherd reads of one models.dev model; each field null when the source does not say. */
|
|
12
|
-
export interface ModelFacts {
|
|
13
|
-
efforts: string[] | null;
|
|
14
|
-
context: number | null;
|
|
15
|
-
output: number | null;
|
|
16
|
-
/** dollars per million tokens */
|
|
17
|
-
price: { input: number; cached: number; output: number } | null;
|
|
18
|
-
toolUse: boolean | null;
|
|
19
|
-
imageIn: boolean | null;
|
|
20
|
-
reasoning: boolean | null;
|
|
21
|
-
releaseDate: string | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const ModelSchema = z.looseObject({
|
|
25
|
-
reasoning: z.boolean().optional(),
|
|
26
|
-
reasoning_options: z
|
|
27
|
-
.array(z.looseObject({ type: z.string(), values: z.array(z.string()).optional() }))
|
|
28
|
-
.optional(),
|
|
29
|
-
tool_call: z.boolean().optional(),
|
|
30
|
-
release_date: z.string().optional(),
|
|
31
|
-
modalities: z.looseObject({ input: z.array(z.string()).optional() }).optional(),
|
|
32
|
-
limit: z.looseObject({ context: z.unknown(), output: z.unknown() }).optional(),
|
|
33
|
-
cost: z.looseObject({ input: z.unknown(), output: z.unknown(), cache_read: z.unknown() }).optional(),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Spec 1.2 §3.5: `provider`'s model `id` in models.dev's api.json (a provider id such as `openai` or
|
|
38
|
-
* `opencode-go`, and the model id within it); null when absent or unreadable.
|
|
39
|
-
*/
|
|
40
|
-
export function modelsDevFacts(raw: unknown, provider: string, id: string): ModelFacts | null {
|
|
41
|
-
const p = (raw as Record<string, { models?: Record<string, unknown> }> | null)?.[provider];
|
|
42
|
-
const parsed = ModelSchema.safeParse(p?.models?.[id]);
|
|
43
|
-
if (!parsed.success) return null;
|
|
44
|
-
const m = parsed.data;
|
|
45
|
-
const efforts = m.reasoning_options?.find((o) => o.type === "effort")?.values ?? null;
|
|
46
|
-
const input = num(m.cost?.input);
|
|
47
|
-
const output = num(m.cost?.output);
|
|
48
|
-
return {
|
|
49
|
-
efforts,
|
|
50
|
-
context: num(m.limit?.context),
|
|
51
|
-
output: num(m.limit?.output),
|
|
52
|
-
price:
|
|
53
|
-
input !== null && output !== null ? { input, cached: num(m.cost?.cache_read) ?? input, output } : null,
|
|
54
|
-
toolUse: m.tool_call ?? null,
|
|
55
|
-
imageIn: m.modalities?.input ? m.modalities.input.includes("image") : null,
|
|
56
|
-
reasoning: m.reasoning ?? null,
|
|
57
|
-
releaseDate: m.release_date ?? null,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { median } from "../../domain/util.ts";
|
|
2
|
-
import { SourceError, type SourceTransport, sourceGet } from "./http.ts";
|
|
3
|
-
import { isoDay, num, type SourceRow } from "./rows.ts";
|
|
4
|
-
|
|
5
|
-
/** Spec 1.2 §3.1: per provider, uptime, latency and throughput over the last 30 minutes. */
|
|
6
|
-
export const openRouterEndpointsUrl = (id: string): string =>
|
|
7
|
-
`https://openrouter.ai/api/v1/models/${id}/endpoints`;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Plan 13 R-H: one request per OpenRouter id (one per catalog family OpenRouter lists), in parallel. An id
|
|
11
|
-
* OpenRouter no longer knows (404) is left out; any other failure fails the source, which keeps its last answer.
|
|
12
|
-
*/
|
|
13
|
-
export async function fetchOpenRouterEndpoints(
|
|
14
|
-
ids: readonly string[],
|
|
15
|
-
t: SourceTransport = {},
|
|
16
|
-
): Promise<Record<string, unknown>> {
|
|
17
|
-
const got = await Promise.all(
|
|
18
|
-
ids.map(async (id) => {
|
|
19
|
-
try {
|
|
20
|
-
return [id, (await sourceGet(openRouterEndpointsUrl(id), t)).json()] as const;
|
|
21
|
-
} catch (e) {
|
|
22
|
-
if (e instanceof SourceError && e.status === 404) return null;
|
|
23
|
-
throw e instanceof SourceError ? new SourceError(`${id}: ${e.message}`, e.status, e.headers) : e;
|
|
24
|
-
}
|
|
25
|
-
}),
|
|
26
|
-
);
|
|
27
|
-
return Object.fromEntries(got.filter((x) => x !== null));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const ENDPOINT_FIELDS = ["latency_last_30m", "throughput_last_30m", "uptime_last_30m"] as const;
|
|
31
|
-
|
|
32
|
-
/** A statistic as a number, or as a percentile object (`{ p50: … }`) some answers carry. */
|
|
33
|
-
const stat = (v: unknown): number | null =>
|
|
34
|
-
v !== null && typeof v === "object" ? num((v as { p50?: unknown }).p50) : num(v);
|
|
35
|
-
|
|
36
|
-
/** One row per OpenRouter id and field: the median over its endpoints that report the field. */
|
|
37
|
-
export function parseOpenRouterEndpoints(raw: Record<string, unknown>, fetchedAt: string): SourceRow[] {
|
|
38
|
-
const rows: SourceRow[] = [];
|
|
39
|
-
for (const [id, answer] of Object.entries(raw)) {
|
|
40
|
-
const endpoints = (answer as { data?: { endpoints?: unknown[] } } | null)?.data?.endpoints;
|
|
41
|
-
if (!Array.isArray(endpoints)) continue;
|
|
42
|
-
for (const field of ENDPOINT_FIELDS) {
|
|
43
|
-
const xs = endpoints
|
|
44
|
-
.map((e) => stat((e as Record<string, unknown> | null)?.[field]))
|
|
45
|
-
.filter((x): x is number => x !== null);
|
|
46
|
-
const value = median(xs);
|
|
47
|
-
if (value !== null)
|
|
48
|
-
rows.push({ rung: id, field, value, date: isoDay(fetchedAt), url: `https://openrouter.ai/${id}` });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return rows;
|
|
52
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { type SourceTransport, sourceGet } from "./http.ts";
|
|
3
|
-
import { num } from "./rows.ts";
|
|
4
|
-
|
|
5
|
-
/** Spec 1.2 §3.1: price and context cross-check; a model appears here the day it ships. */
|
|
6
|
-
export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
7
|
-
|
|
8
|
-
export const fetchOpenRouterModels = async (t: SourceTransport = {}): Promise<unknown> =>
|
|
9
|
-
(await sourceGet(OPENROUTER_MODELS_URL, t)).json();
|
|
10
|
-
|
|
11
|
-
export interface OpenRouterModel {
|
|
12
|
-
/** `author/slug`, as the endpoints path takes it */
|
|
13
|
-
id: string;
|
|
14
|
-
context: number | null;
|
|
15
|
-
/** dollars per million tokens */
|
|
16
|
-
price: { input: number; output: number; cached: number | null } | null;
|
|
17
|
-
efforts: string[] | null;
|
|
18
|
-
defaultEffort: string | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ModelSchema = z.looseObject({
|
|
22
|
-
id: z.string(),
|
|
23
|
-
context_length: z.unknown(),
|
|
24
|
-
pricing: z
|
|
25
|
-
.looseObject({ prompt: z.unknown(), completion: z.unknown(), input_cache_read: z.unknown() })
|
|
26
|
-
.optional(),
|
|
27
|
-
reasoning: z
|
|
28
|
-
.looseObject({ supported_efforts: z.array(z.string()).optional(), default_effort: z.string().optional() })
|
|
29
|
-
.optional(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
/** A per-token price (a number or decimal string) per million tokens, without float noise (0.000002 × 1e6). */
|
|
33
|
-
export const perMillion = (v: unknown): number | null => {
|
|
34
|
-
const n = num(v);
|
|
35
|
-
return n === null ? null : Math.round(n * 1e12) / 1e6;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** Every model of OpenRouter's `/models` answer; a malformed entry is skipped. */
|
|
39
|
-
export function parseOpenRouterModels(raw: unknown): OpenRouterModel[] {
|
|
40
|
-
const data = (raw as { data?: unknown[] } | null)?.data;
|
|
41
|
-
if (!Array.isArray(data)) return [];
|
|
42
|
-
const out: OpenRouterModel[] = [];
|
|
43
|
-
for (const entry of data) {
|
|
44
|
-
const m = ModelSchema.safeParse(entry);
|
|
45
|
-
if (!m.success) continue;
|
|
46
|
-
const input = perMillion(m.data.pricing?.prompt);
|
|
47
|
-
const output = perMillion(m.data.pricing?.completion);
|
|
48
|
-
out.push({
|
|
49
|
-
id: m.data.id,
|
|
50
|
-
context: num(m.data.context_length),
|
|
51
|
-
price:
|
|
52
|
-
input !== null && output !== null
|
|
53
|
-
? { input, output, cached: perMillion(m.data.pricing?.input_cache_read) }
|
|
54
|
-
: null,
|
|
55
|
-
efforts: m.data.reasoning?.supported_efforts ?? null,
|
|
56
|
-
defaultEffort: m.data.reasoning?.default_effort ?? null,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spec 1.2 §3.3: what a score source's parser returns, one number each. `rung` is the source's own model id,
|
|
3
|
-
* with `#<effort>` when the source names one (catherd's effort word); `date` is the day the source measured or
|
|
4
|
-
* published it, else the day it was fetched; `url` is where the value can be checked.
|
|
5
|
-
*/
|
|
6
|
-
export interface SourceRow {
|
|
7
|
-
rung: string;
|
|
8
|
-
field: string;
|
|
9
|
-
value: number;
|
|
10
|
-
date: string;
|
|
11
|
-
url: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** The `YYYY-MM-DD` of an ISO time. */
|
|
15
|
-
export const isoDay = (iso: string): string => iso.slice(0, 10);
|
|
16
|
-
|
|
17
|
-
/** `candidate` when it is an http(s) URL, else `fallback`. */
|
|
18
|
-
export const urlOr = (candidate: unknown, fallback: string): string =>
|
|
19
|
-
typeof candidate === "string" && /^https?:\/\//.test(candidate.trim()) && URL.canParse(candidate.trim())
|
|
20
|
-
? candidate.trim()
|
|
21
|
-
: fallback;
|
|
22
|
-
|
|
23
|
-
/** `v` as a finite number (numbers and numeric strings), else null. */
|
|
24
|
-
export function num(v: unknown): number | null {
|
|
25
|
-
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : Number.NaN;
|
|
26
|
-
return Number.isFinite(n) ? n : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** One row per rung and field: a source that lists a model twice (Epoch: one row per agent) keeps the highest. */
|
|
30
|
-
export function keepHighest(rows: SourceRow[]): SourceRow[] {
|
|
31
|
-
const best = new Map<string, SourceRow>();
|
|
32
|
-
for (const r of rows) {
|
|
33
|
-
const k = `${r.rung}\u0000${r.field}`;
|
|
34
|
-
const had = best.get(k);
|
|
35
|
-
if (!had || r.value > had.value) best.set(k, r);
|
|
36
|
-
}
|
|
37
|
-
return [...best.values()];
|
|
38
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { type SourceTransport, sourceGet } from "./http.ts";
|
|
2
|
-
import { isoDay, num, type SourceRow } from "./rows.ts";
|
|
3
|
-
|
|
4
|
-
/** Spec 1.2 §3.1: the leaderboard is a markdown table in the repository's README (Apache-2.0). */
|
|
5
|
-
export const VECTARA_URL =
|
|
6
|
-
"https://raw.githubusercontent.com/vectara/hallucination-leaderboard/main/README.md";
|
|
7
|
-
export const VECTARA_PAGE = "https://github.com/vectara/hallucination-leaderboard";
|
|
8
|
-
|
|
9
|
-
export const fetchVectara = async (t: SourceTransport = {}): Promise<string> =>
|
|
10
|
-
(await sourceGet(VECTARA_URL, t)).text();
|
|
11
|
-
|
|
12
|
-
const MONTHS = [
|
|
13
|
-
"january",
|
|
14
|
-
"february",
|
|
15
|
-
"march",
|
|
16
|
-
"april",
|
|
17
|
-
"may",
|
|
18
|
-
"june",
|
|
19
|
-
"july",
|
|
20
|
-
"august",
|
|
21
|
-
"september",
|
|
22
|
-
"october",
|
|
23
|
-
"november",
|
|
24
|
-
"december",
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
/** "Last updated on September 22, 2026" as `2026-09-22`; null when the README has no such line. */
|
|
28
|
-
export function vectaraDate(markdown: string): string | null {
|
|
29
|
-
const m = /Last updated on ([A-Za-z]+) (\d{1,2}), (\d{4})/.exec(markdown);
|
|
30
|
-
const month = m ? MONTHS.indexOf((m[1] as string).toLowerCase()) : -1;
|
|
31
|
-
if (!m || month < 0) return null;
|
|
32
|
-
return `${m[3]}-${String(month + 1).padStart(2, "0")}-${(m[2] as string).padStart(2, "0")}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const cells = (line: string) =>
|
|
36
|
-
line
|
|
37
|
-
.trim()
|
|
38
|
-
.replace(/^\||\|$/g, "")
|
|
39
|
-
.split("|")
|
|
40
|
-
.map((c) => c.trim());
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Two rows per model of the README's table: `factual_consistency` and `hallucination_rate`, in percent. The
|
|
44
|
-
* model keeps its `vendor/` prefix and names no effort.
|
|
45
|
-
*/
|
|
46
|
-
export function parseVectara(markdown: string, fetchedAt: string): SourceRow[] {
|
|
47
|
-
const lines = markdown.split("\n");
|
|
48
|
-
const head = lines.findIndex((l) => /^\|\s*Model\s*\|/i.test(l));
|
|
49
|
-
if (head < 0) return [];
|
|
50
|
-
const columns = cells(lines[head] as string).map((c) => c.toLowerCase());
|
|
51
|
-
const at = (name: string) => columns.findIndex((c) => c.startsWith(name));
|
|
52
|
-
const [consistency, hallucination] = [at("factual consistency"), at("hallucination rate")];
|
|
53
|
-
const date = vectaraDate(markdown) ?? isoDay(fetchedAt);
|
|
54
|
-
const rows: SourceRow[] = [];
|
|
55
|
-
for (const line of lines.slice(head + 2)) {
|
|
56
|
-
if (!line.trim().startsWith("|")) break;
|
|
57
|
-
const c = cells(line);
|
|
58
|
-
const model = c[0];
|
|
59
|
-
if (!model) continue;
|
|
60
|
-
for (const [field, i] of [
|
|
61
|
-
["factual_consistency", consistency],
|
|
62
|
-
["hallucination_rate", hallucination],
|
|
63
|
-
] as const) {
|
|
64
|
-
const value = i < 0 ? null : num((c[i] ?? "").replace("%", ""));
|
|
65
|
-
if (value !== null) rows.push({ rung: model, field, value, date, url: VECTARA_PAGE });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return rows;
|
|
69
|
-
}
|
package/src/infra/sources/zip.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Plan 13 R-F: Epoch ships its tables as one zip. Reading it needs only the central directory and
|
|
2
|
-
// raw deflate (`Bun.inflateSync`), so there is no dependency for it.
|
|
3
|
-
|
|
4
|
-
const EOCD = 0x06054b50;
|
|
5
|
-
const CENTRAL = 0x02014b50;
|
|
6
|
-
const LOCAL = 0x04034b50;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The files of a zip archive whose names `want` accepts, by name: stored (method 0) or deflated (8). Throws
|
|
10
|
-
* on a malformed archive or another method. ZIP64 archives are not read (Epoch's is 2 MB).
|
|
11
|
-
*/
|
|
12
|
-
export function unzip(
|
|
13
|
-
bytes: Uint8Array,
|
|
14
|
-
want: (name: string) => boolean = () => true,
|
|
15
|
-
): Map<string, Uint8Array> {
|
|
16
|
-
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
17
|
-
let end = -1;
|
|
18
|
-
// the end record is 22 bytes plus a comment of up to 64 KiB
|
|
19
|
-
for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 22 - 0xffff); i--)
|
|
20
|
-
if (dv.getUint32(i, true) === EOCD) {
|
|
21
|
-
end = i;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
if (end < 0) throw new Error("not a zip archive");
|
|
25
|
-
const count = dv.getUint16(end + 10, true);
|
|
26
|
-
let at = dv.getUint32(end + 16, true);
|
|
27
|
-
const decode = new TextDecoder();
|
|
28
|
-
const out = new Map<string, Uint8Array>();
|
|
29
|
-
for (let k = 0; k < count; k++) {
|
|
30
|
-
if (dv.getUint32(at, true) !== CENTRAL) throw new Error("a malformed zip directory");
|
|
31
|
-
const method = dv.getUint16(at + 10, true);
|
|
32
|
-
const size = dv.getUint32(at + 20, true);
|
|
33
|
-
const nameLen = dv.getUint16(at + 28, true);
|
|
34
|
-
const extraLen = dv.getUint16(at + 30, true);
|
|
35
|
-
const commentLen = dv.getUint16(at + 32, true);
|
|
36
|
-
const local = dv.getUint32(at + 42, true);
|
|
37
|
-
const name = decode.decode(bytes.subarray(at + 46, at + 46 + nameLen));
|
|
38
|
-
at += 46 + nameLen + extraLen + commentLen;
|
|
39
|
-
if (!want(name)) continue;
|
|
40
|
-
if (dv.getUint32(local, true) !== LOCAL) throw new Error(`a malformed zip entry: ${name}`);
|
|
41
|
-
const start = local + 30 + dv.getUint16(local + 26, true) + dv.getUint16(local + 28, true);
|
|
42
|
-
const data = bytes.subarray(start, start + size);
|
|
43
|
-
if (method === 0) out.set(name, data);
|
|
44
|
-
else if (method === 8) out.set(name, Bun.inflateSync(data.slice()));
|
|
45
|
-
else throw new Error(`zip method ${method} is not read: ${name}`);
|
|
46
|
-
}
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
2
|
-
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { barPool, deriveBars } from "../domain/bars.ts";
|
|
4
|
-
import {
|
|
5
|
-
buildCatalog,
|
|
6
|
-
DIMS,
|
|
7
|
-
type Dim,
|
|
8
|
-
type ModelsFile,
|
|
9
|
-
outranks,
|
|
10
|
-
type Score,
|
|
11
|
-
type ScoresFile,
|
|
12
|
-
scoresOf,
|
|
13
|
-
} from "../domain/catalog.ts";
|
|
14
|
-
import { DIFFICULTIES, type Difficulty, KINDS, type Kind } from "../domain/lane.ts";
|
|
15
|
-
import { defaultEffortOf, type SourcesFile } from "../domain/sources.ts";
|
|
16
|
-
import type { SourceTransport } from "../infra/sources/http.ts";
|
|
17
|
-
import { adjacent, derive, type RawAnswers } from "./source-derive.ts";
|
|
18
|
-
import { cachedAnswers, syncSources } from "./source-sync.ts";
|
|
19
|
-
|
|
20
|
-
/** Spec 1.2 §7: a value that moved by more than this is listed in the refresh PR. */
|
|
21
|
-
const MOVED = 0.05;
|
|
22
|
-
|
|
23
|
-
export interface RefreshReport {
|
|
24
|
-
/** the day the data was read, `scores.json`'s new `version` */
|
|
25
|
-
date: string;
|
|
26
|
-
/** canonical rungs with no value in the shipped file before, and at least one now */
|
|
27
|
-
newlyScored: string[];
|
|
28
|
-
/** values that moved by more than 5 %, and values that appeared or went away on a rung already scored */
|
|
29
|
-
moved: { rung: string; dim: Dim; from: number | null; to: number | null }[];
|
|
30
|
-
/** every default threshold that moved, appeared or went away */
|
|
31
|
-
barsMoved: { kind: Kind; difficulty: Difficulty; dim: Dim; from: number | null; to: number | null }[];
|
|
32
|
-
/** shipped treat-likes dropped because their rung now has a value of its own on every dimension they lent */
|
|
33
|
-
standInsDropped: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const key = (s: Pick<Score, "rung" | "dim">) => `${s.rung} ${s.dim}`;
|
|
37
|
-
|
|
38
|
-
/** One value per rung and dimension, the one that outranks the others (spec 1.2 §4.3), in file order. */
|
|
39
|
-
function best(scores: Score[], now: number): Score[] {
|
|
40
|
-
const out = new Map<string, Score>();
|
|
41
|
-
for (const s of scores) {
|
|
42
|
-
const had = out.get(key(s));
|
|
43
|
-
if (!had || outranks(s, had, now)) out.set(key(s), s);
|
|
44
|
-
}
|
|
45
|
-
return [...out.values()].sort(
|
|
46
|
-
(a, b) => a.rung.localeCompare(b.rung) || DIMS.indexOf(a.dim) - DIMS.indexOf(b.dim),
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Spec 1.2 §5.2, §7: `catalog/scores.json` rebuilt from the keyless sources' answers. It keeps every hand-typed
|
|
52
|
-
* value (one with no `source`), replaces the keyless values with what `raw` says now (Artificial Analysis is
|
|
53
|
-
* never shipped, whatever `raw` holds), spreads every published value to the family's other efforts as
|
|
54
|
-
* `adjacent` (plan 14 Ruling 3), keeps one value per rung and dimension, derives the default bars with their
|
|
55
|
-
* `barsWhy`, and drops a shipped treat-like its rung no longer needs.
|
|
56
|
-
*/
|
|
57
|
-
export function rebuildShipped(
|
|
58
|
-
raw: RawAnswers,
|
|
59
|
-
ctx: { models: ModelsFile; scores: ScoresFile; sources: SourcesFile; now: number },
|
|
60
|
-
): { file: ScoresFile; report: RefreshReport } {
|
|
61
|
-
const date = new Date(ctx.now).toISOString().slice(0, 10);
|
|
62
|
-
const { "artificial-analysis": _aa, ...keyless } = raw;
|
|
63
|
-
// hand-typed values, less the ones an earlier rebuild spread: those are spread again below
|
|
64
|
-
const hand = ctx.scores.scores.filter((s) => s.source === undefined && s.confidence !== "adjacent");
|
|
65
|
-
const synced = derive(keyless, { ...ctx, scores: { ...ctx.scores, scores: hand } }).scores.filter(
|
|
66
|
-
(s) => s.source !== "artificial-analysis" && s.confidence !== "adjacent",
|
|
67
|
-
);
|
|
68
|
-
const direct = best([...hand, ...synced], ctx.now);
|
|
69
|
-
// an inferred value is catherd's guess for its own rung only: it is never carried to another effort
|
|
70
|
-
const spread = adjacent(
|
|
71
|
-
ctx.models.families,
|
|
72
|
-
direct.filter((s) => s.confidence !== "inferred"),
|
|
73
|
-
new Set(),
|
|
74
|
-
ctx.now,
|
|
75
|
-
(f) => defaultEffortOf(ctx.sources, f),
|
|
76
|
-
);
|
|
77
|
-
const scores = best([...direct, ...spread], ctx.now);
|
|
78
|
-
const benchmark = (d: Dim) => {
|
|
79
|
-
const b = ctx.scores.benchmarks[d];
|
|
80
|
-
return `${b.benchmark} ${b.version}`;
|
|
81
|
-
};
|
|
82
|
-
const { bars, barsWhy } = deriveBars(barPool(scores), benchmark, date);
|
|
83
|
-
|
|
84
|
-
const draft: ScoresFile = { ...ctx.scores, version: date, scores, bars, barsWhy };
|
|
85
|
-
const c = buildCatalog({ models: ctx.models, scores: { ...draft, treatLike: ctx.scores.treatLike } });
|
|
86
|
-
const treatLike: ScoresFile["treatLike"] = {};
|
|
87
|
-
const standInsDropped: string[] = [];
|
|
88
|
-
for (const [rung, t] of Object.entries(ctx.scores.treatLike))
|
|
89
|
-
if (scoresOf(c, rung)?.borrowed.length) treatLike[rung] = t;
|
|
90
|
-
else standInsDropped.push(rung);
|
|
91
|
-
const file: ScoresFile = { ...draft, treatLike };
|
|
92
|
-
return { file, report: { date, ...diffShipped(ctx.scores, file), standInsDropped } };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const rungsOf = (f: ScoresFile) => new Set(f.scores.map((s) => s.rung));
|
|
96
|
-
|
|
97
|
-
/** What moved between two shipped files (spec 1.2 §7's PR body). */
|
|
98
|
-
function diffShipped(
|
|
99
|
-
before: ScoresFile,
|
|
100
|
-
after: ScoresFile,
|
|
101
|
-
): Pick<RefreshReport, "newlyScored" | "moved" | "barsMoved"> {
|
|
102
|
-
const had = rungsOf(before);
|
|
103
|
-
const newlyScored = [...rungsOf(after)].filter((r) => !had.has(r)).sort();
|
|
104
|
-
const was = new Map(before.scores.map((s) => [key(s), s.value]));
|
|
105
|
-
const now = new Map(after.scores.map((s) => [key(s), s.value]));
|
|
106
|
-
const moved: RefreshReport["moved"] = [];
|
|
107
|
-
for (const k of [...new Set([...was.keys(), ...now.keys()])].sort()) {
|
|
108
|
-
const [rung, dim] = k.split(" ") as [string, Dim];
|
|
109
|
-
if (newlyScored.includes(rung)) continue;
|
|
110
|
-
const from = was.get(k) ?? null;
|
|
111
|
-
const to = now.get(k) ?? null;
|
|
112
|
-
const far =
|
|
113
|
-
from === null || to === null || (from === 0 ? to !== 0 : Math.abs(to - from) / Math.abs(from) > MOVED);
|
|
114
|
-
if (far) moved.push({ rung, dim, from, to });
|
|
115
|
-
}
|
|
116
|
-
const barsMoved: RefreshReport["barsMoved"] = [];
|
|
117
|
-
for (const kind of KINDS)
|
|
118
|
-
for (const difficulty of DIFFICULTIES)
|
|
119
|
-
for (const dim of DIMS) {
|
|
120
|
-
const from = before.bars[kind]?.[difficulty]?.[dim] ?? null;
|
|
121
|
-
const to = after.bars[kind]?.[difficulty]?.[dim] ?? null;
|
|
122
|
-
if (from !== to) barsMoved.push({ kind, difficulty, dim, from, to });
|
|
123
|
-
}
|
|
124
|
-
return { newlyScored, moved, barsMoved };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Whether the rebuilt file says anything new: its date and `barsWhy` wording alone never count. */
|
|
128
|
-
export function shippedChanged(before: ScoresFile, after: ScoresFile): boolean {
|
|
129
|
-
// as the file reads back: no undefined fields, whatever the key order
|
|
130
|
-
const strip = (f: ScoresFile): unknown => JSON.parse(JSON.stringify({ ...f, version: "", barsWhy: null }));
|
|
131
|
-
return !isDeepStrictEqual(strip(before), strip(after));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const num = (v: number | null) => (v === null ? "none" : String(v));
|
|
135
|
-
|
|
136
|
-
/** Spec 1.2 §7: the refresh PR's body: rungs newly scored, values moved by more than 5 %, bars that moved. */
|
|
137
|
-
export function refreshBody(r: RefreshReport): string {
|
|
138
|
-
const out = [
|
|
139
|
-
`Weekly refresh of the shipped scores from the keyless public sources (data of ${r.date}).`,
|
|
140
|
-
"",
|
|
141
|
-
"Artificial Analysis values are never shipped. Merging this releases a patch through the Release workflow.",
|
|
142
|
-
"",
|
|
143
|
-
`### Rungs newly scored (${r.newlyScored.length})`,
|
|
144
|
-
"",
|
|
145
|
-
...(r.newlyScored.length ? r.newlyScored.map((x) => `- \`${x}\``) : ["none"]),
|
|
146
|
-
"",
|
|
147
|
-
`### Values moved by more than 5 % (${r.moved.length})`,
|
|
148
|
-
"",
|
|
149
|
-
...(r.moved.length
|
|
150
|
-
? ["| rung | dimension | was | now |", "| --- | --- | --- | --- |"].concat(
|
|
151
|
-
r.moved.map((m) => `| \`${m.rung}\` | ${m.dim} | ${num(m.from)} | ${num(m.to)} |`),
|
|
152
|
-
)
|
|
153
|
-
: ["none"]),
|
|
154
|
-
"",
|
|
155
|
-
`### Bars that moved (${r.barsMoved.length})`,
|
|
156
|
-
"",
|
|
157
|
-
...(r.barsMoved.length
|
|
158
|
-
? ["| kind | difficulty | dimension | was | now |", "| --- | --- | --- | --- | --- |"].concat(
|
|
159
|
-
r.barsMoved.map(
|
|
160
|
-
(b) => `| ${b.kind} | ${b.difficulty} | ${b.dim} | ${num(b.from)} | ${num(b.to)} |`,
|
|
161
|
-
),
|
|
162
|
-
)
|
|
163
|
-
: ["none"]),
|
|
164
|
-
];
|
|
165
|
-
if (r.standInsDropped.length)
|
|
166
|
-
out.push(
|
|
167
|
-
"",
|
|
168
|
-
"### Shipped stand-ins no longer needed",
|
|
169
|
-
"",
|
|
170
|
-
...r.standInsDropped.map((x) => `- \`${x}\` now has values of its own`),
|
|
171
|
-
);
|
|
172
|
-
return `${out.join("\n")}\n`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Spec 1.2 §7, `scripts/catalog-refresh.ts`: syncs every keyless source now (no Artificial Analysis key, the
|
|
177
|
-
* TTL ignored) into the current data folder, rebuilds the shipped file from the answers and, when it says
|
|
178
|
-
* anything new, writes it to `out`. A keyless source that fails writes nothing: a refresh never ships a file
|
|
179
|
-
* that lost a source's values (plan 14 Ruling 11).
|
|
180
|
-
*/
|
|
181
|
-
export async function refreshShipped(o: {
|
|
182
|
-
out: string;
|
|
183
|
-
current: ScoresFile;
|
|
184
|
-
models: ModelsFile;
|
|
185
|
-
sources: SourcesFile;
|
|
186
|
-
transport?: SourceTransport;
|
|
187
|
-
now?: () => number;
|
|
188
|
-
}): Promise<{ changed: boolean; failed: { source: string; error: string }[]; report: RefreshReport | null }> {
|
|
189
|
-
const now = o.now ?? Date.now;
|
|
190
|
-
const sync = await syncSources({ force: true, aaKey: null, transport: o.transport, now });
|
|
191
|
-
if (sync.failed.length) return { changed: false, failed: sync.failed, report: null };
|
|
192
|
-
const { file, report } = rebuildShipped(cachedAnswers(), {
|
|
193
|
-
models: o.models,
|
|
194
|
-
scores: o.current,
|
|
195
|
-
sources: o.sources,
|
|
196
|
-
now: now(),
|
|
197
|
-
});
|
|
198
|
-
const changed = shippedChanged(o.current, file);
|
|
199
|
-
if (changed) writeFileSync(o.out, `${JSON.stringify(file, null, 2)}\n`);
|
|
200
|
-
return { changed, failed: [], report };
|
|
201
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { Profile } from "../domain/profile.ts";
|
|
2
|
-
import { type StandInToConfirm, standInsToConfirm } from "../domain/profile-rules.ts";
|
|
3
|
-
import { loadCatalog } from "./catalog-service.ts";
|
|
4
|
-
import type { Check } from "./doctor-checks.ts";
|
|
5
|
-
import { runnableBackends } from "./profile-store.ts";
|
|
6
|
-
import { sourcesStatus, TTL_MS } from "./source-sync.ts";
|
|
7
|
-
|
|
8
|
-
const HOUR = 3_600_000;
|
|
9
|
-
const DAY = 24 * HOUR;
|
|
10
|
-
|
|
11
|
-
/** An age as doctor prints it: minutes under an hour, hours under two days, else days. */
|
|
12
|
-
export function ageText(ms: number): string {
|
|
13
|
-
if (ms < HOUR) return `${Math.max(1, Math.round(ms / 60_000))} min`;
|
|
14
|
-
if (ms < 2 * DAY) return `${Math.round(ms / HOUR)} h`;
|
|
15
|
-
return `${Math.round(ms / DAY)} d`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Spec 1.2 §6.1, §9: the stand-ins to confirm across `profiles`, one per canonical rung. */
|
|
19
|
-
export function standInsToConfirmIn(profiles: Profile[]): StandInToConfirm[] {
|
|
20
|
-
if (profiles.length === 0) return [];
|
|
21
|
-
const c = loadCatalog({ timings: false });
|
|
22
|
-
const seen = new Map<string, StandInToConfirm>();
|
|
23
|
-
for (const p of profiles)
|
|
24
|
-
for (const x of standInsToConfirm(p, c, runnableBackends()))
|
|
25
|
-
if (!seen.has(x.canonical)) seen.set(x.canonical, x);
|
|
26
|
-
return [...seen.values()];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** `gpt-5.6-terra#high (repo_code, terminal, honesty)`, the rung and the dimensions a stand-in fills. */
|
|
30
|
-
const confirmText = (xs: StandInToConfirm[]): string =>
|
|
31
|
-
`stand-ins to confirm: ${xs.map((x) => `${x.canonical} (${x.dims.map((d) => d.dim).join(", ")})`).join(", ")}`;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Spec 1.2 §9: the `sources` row: each source's age and last error, whether an Artificial Analysis key is
|
|
35
|
-
* set, its requests left today (`x-ratelimit-remaining` of its last answer), and the linked profiles'
|
|
36
|
-
* stand-ins to confirm (`confirm`, spec 1.2 §6.1). `info` before the first sync (routing uses the shipped
|
|
37
|
-
* scores); a warning when a source fails or has not been fetched for two TTLs (a day), since background
|
|
38
|
-
* syncs would have refreshed it. It reads the sync's state only: no request.
|
|
39
|
-
*/
|
|
40
|
-
export function sourcesCheck(now = Date.now(), confirm: StandInToConfirm[] = []): Check {
|
|
41
|
-
const s = sourcesStatus();
|
|
42
|
-
const shown = s.sources.filter((x) => x.source !== "artificial-analysis" || s.aaKey);
|
|
43
|
-
const keyless = s.sources.filter((x) => x.source !== "artificial-analysis");
|
|
44
|
-
const sameDay =
|
|
45
|
-
s.rateLimitAt !== null && s.rateLimitAt.slice(0, 10) === new Date(now).toISOString().slice(0, 10);
|
|
46
|
-
const left =
|
|
47
|
-
s.rateLimitRemaining === null
|
|
48
|
-
? ""
|
|
49
|
-
: sameDay
|
|
50
|
-
? `, ${s.rateLimitRemaining} requests left today`
|
|
51
|
-
: `, ${s.rateLimitRemaining} requests left on ${s.rateLimitAt?.slice(0, 10)}`;
|
|
52
|
-
const aa = `${s.aaKey ? `Artificial Analysis key set${left}` : "no Artificial Analysis key"}${confirm.length ? `; ${confirmText(confirm)}` : ""}`;
|
|
53
|
-
const base = { id: "sources", label: "sources" };
|
|
54
|
-
if (keyless.every((x) => x.fetchedAt === null && x.error === null))
|
|
55
|
-
return {
|
|
56
|
-
...base,
|
|
57
|
-
state: "info",
|
|
58
|
-
word: "not synced",
|
|
59
|
-
detail: `routing uses the shipped scores; ${aa}`,
|
|
60
|
-
fix: "catherd catalog sync",
|
|
61
|
-
};
|
|
62
|
-
const parts = shown.map(
|
|
63
|
-
(x) =>
|
|
64
|
-
`${x.source} ${x.fetchedAt ? ageText(now - Date.parse(x.fetchedAt)) : "never"}${x.error ? ` (${x.error})` : ""}`,
|
|
65
|
-
);
|
|
66
|
-
const failing = shown.some((x) => x.error !== null);
|
|
67
|
-
const stale = keyless.some((x) => x.fetchedAt === null || now - Date.parse(x.fetchedAt) > 2 * TTL_MS);
|
|
68
|
-
const warn = failing || stale;
|
|
69
|
-
return {
|
|
70
|
-
...base,
|
|
71
|
-
state: warn ? "warn" : "ok",
|
|
72
|
-
word: failing ? "failing" : stale ? "stale" : "fresh",
|
|
73
|
-
detail: `${parts.join(", ")}; ${aa}`,
|
|
74
|
-
...(warn ? { fix: "catherd catalog sync --force" } : {}),
|
|
75
|
-
};
|
|
76
|
-
}
|