modelrig 0.0.1 → 0.1.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/LICENSE +201 -0
- package/README.md +115 -8
- package/dist/adapters/anthropic.d.ts +46 -0
- package/dist/adapters/anthropic.d.ts.map +1 -0
- package/dist/adapters/anthropic.js +202 -0
- package/dist/adapters/deepseek.d.ts +41 -0
- package/dist/adapters/deepseek.d.ts.map +1 -0
- package/dist/adapters/deepseek.js +163 -0
- package/dist/adapters/gemini.d.ts +44 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +222 -0
- package/dist/adapters/grok.d.ts +44 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +168 -0
- package/dist/adapters/openai-compat.d.ts +74 -0
- package/dist/adapters/openai-compat.d.ts.map +1 -0
- package/dist/adapters/openai-compat.js +187 -0
- package/dist/adapters/openai.d.ts +49 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +222 -0
- package/dist/bakeoff/engine.d.ts +52 -0
- package/dist/bakeoff/engine.d.ts.map +1 -0
- package/dist/bakeoff/engine.js +163 -0
- package/dist/bakeoff/replay.d.ts +52 -0
- package/dist/bakeoff/replay.d.ts.map +1 -0
- package/dist/bakeoff/replay.js +94 -0
- package/dist/bakeoff/samples.d.ts +62 -0
- package/dist/bakeoff/samples.d.ts.map +1 -0
- package/dist/bakeoff/samples.js +128 -0
- package/dist/bakeoff/stats.d.ts +13 -0
- package/dist/bakeoff/stats.d.ts.map +1 -0
- package/dist/bakeoff/stats.js +48 -0
- package/dist/bakeoff/types.d.ts +37 -0
- package/dist/bakeoff/types.d.ts.map +1 -0
- package/dist/bakeoff/types.js +10 -0
- package/dist/cli.d.ts +27 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +572 -0
- package/dist/config.d.ts +104 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +148 -0
- package/dist/feedback/coverage.d.ts +32 -0
- package/dist/feedback/coverage.d.ts.map +1 -0
- package/dist/feedback/coverage.js +95 -0
- package/dist/feedback/feedback.d.ts +35 -0
- package/dist/feedback/feedback.d.ts.map +1 -0
- package/dist/feedback/feedback.js +39 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +448 -0
- package/dist/init/finder.d.ts +79 -0
- package/dist/init/finder.d.ts.map +1 -0
- package/dist/init/finder.js +297 -0
- package/dist/init/run.d.ts +99 -0
- package/dist/init/run.d.ts.map +1 -0
- package/dist/init/run.js +281 -0
- package/dist/init/signatures.d.ts +39 -0
- package/dist/init/signatures.d.ts.map +1 -0
- package/dist/init/signatures.js +116 -0
- package/dist/matrix/aggregate.d.ts +89 -0
- package/dist/matrix/aggregate.d.ts.map +1 -0
- package/dist/matrix/aggregate.js +186 -0
- package/dist/query/grammar.d.ts +35 -0
- package/dist/query/grammar.d.ts.map +1 -0
- package/dist/query/grammar.js +279 -0
- package/dist/query/run.d.ts +8 -0
- package/dist/query/run.d.ts.map +1 -0
- package/dist/query/run.js +21 -0
- package/dist/registry/build.d.ts +126 -0
- package/dist/registry/build.d.ts.map +1 -0
- package/dist/registry/build.js +192 -0
- package/dist/registry/call-notes.d.ts +11 -0
- package/dist/registry/call-notes.d.ts.map +1 -0
- package/dist/registry/call-notes.js +72 -0
- package/dist/registry/claims.d.ts +63 -0
- package/dist/registry/claims.d.ts.map +1 -0
- package/dist/registry/claims.js +107 -0
- package/dist/registry/facts.d.ts +32 -0
- package/dist/registry/facts.d.ts.map +1 -0
- package/dist/registry/facts.js +120 -0
- package/dist/registry/fingerprint.d.ts +20 -0
- package/dist/registry/fingerprint.d.ts.map +1 -0
- package/dist/registry/fingerprint.js +150 -0
- package/dist/registry/parity.d.ts +26 -0
- package/dist/registry/parity.d.ts.map +1 -0
- package/dist/registry/parity.js +41 -0
- package/dist/registry/pricing.d.ts +19 -0
- package/dist/registry/pricing.d.ts.map +1 -0
- package/dist/registry/pricing.js +69 -0
- package/dist/registry/rows.d.ts +16 -0
- package/dist/registry/rows.d.ts.map +1 -0
- package/dist/registry/rows.js +57 -0
- package/dist/registry/seed.d.ts +43 -0
- package/dist/registry/seed.d.ts.map +1 -0
- package/dist/registry/seed.js +97 -0
- package/dist/registry/select.d.ts +68 -0
- package/dist/registry/select.d.ts.map +1 -0
- package/dist/registry/select.js +141 -0
- package/dist/registry/staleness.d.ts +18 -0
- package/dist/registry/staleness.d.ts.map +1 -0
- package/dist/registry/staleness.js +39 -0
- package/dist/routes/draft.d.ts +83 -0
- package/dist/routes/draft.d.ts.map +1 -0
- package/dist/routes/draft.js +135 -0
- package/dist/routes/loader.d.ts +26 -0
- package/dist/routes/loader.d.ts.map +1 -0
- package/dist/routes/loader.js +436 -0
- package/dist/routes/manifest.d.ts +24 -0
- package/dist/routes/manifest.d.ts.map +1 -0
- package/dist/routes/manifest.js +118 -0
- package/dist/routes/serveability.d.ts +52 -0
- package/dist/routes/serveability.d.ts.map +1 -0
- package/dist/routes/serveability.js +73 -0
- package/dist/routes/template.d.ts +18 -0
- package/dist/routes/template.d.ts.map +1 -0
- package/dist/routes/template.js +94 -0
- package/dist/run/cost.d.ts +26 -0
- package/dist/run/cost.d.ts.map +1 -0
- package/dist/run/cost.js +41 -0
- package/dist/run/envelope.d.ts +21 -0
- package/dist/run/envelope.d.ts.map +1 -0
- package/dist/run/envelope.js +45 -0
- package/dist/run/grounding.d.ts +56 -0
- package/dist/run/grounding.d.ts.map +1 -0
- package/dist/run/grounding.js +89 -0
- package/dist/run/repair.d.ts +36 -0
- package/dist/run/repair.d.ts.map +1 -0
- package/dist/run/repair.js +61 -0
- package/dist/run/resolve.d.ts +26 -0
- package/dist/run/resolve.d.ts.map +1 -0
- package/dist/run/resolve.js +81 -0
- package/dist/run/retry.d.ts +19 -0
- package/dist/run/retry.d.ts.map +1 -0
- package/dist/run/retry.js +50 -0
- package/dist/run/run.d.ts +41 -0
- package/dist/run/run.d.ts.map +1 -0
- package/dist/run/run.js +0 -0
- package/dist/run/validate.d.ts +15 -0
- package/dist/run/validate.d.ts.map +1 -0
- package/dist/run/validate.js +56 -0
- package/dist/savings/emit.d.ts +25 -0
- package/dist/savings/emit.d.ts.map +1 -0
- package/dist/savings/emit.js +75 -0
- package/dist/savings/odometer.d.ts +32 -0
- package/dist/savings/odometer.d.ts.map +1 -0
- package/dist/savings/odometer.js +70 -0
- package/dist/savings/swap-execute.d.ts +44 -0
- package/dist/savings/swap-execute.d.ts.map +1 -0
- package/dist/savings/swap-execute.js +87 -0
- package/dist/savings/swaps-client.d.ts +41 -0
- package/dist/savings/swaps-client.d.ts.map +1 -0
- package/dist/savings/swaps-client.js +81 -0
- package/dist/telemetry/capture.d.ts +28 -0
- package/dist/telemetry/capture.d.ts.map +1 -0
- package/dist/telemetry/capture.js +74 -0
- package/dist/telemetry/exporter.d.ts +62 -0
- package/dist/telemetry/exporter.d.ts.map +1 -0
- package/dist/telemetry/exporter.js +233 -0
- package/dist/telemetry/ingest-protocol.d.ts +161 -0
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -0
- package/dist/telemetry/ingest-protocol.js +163 -0
- package/dist/telemetry/sinks.d.ts +54 -0
- package/dist/telemetry/sinks.d.ts.map +1 -0
- package/dist/telemetry/sinks.js +89 -0
- package/dist/telemetry/store.d.ts +38 -0
- package/dist/telemetry/store.d.ts.map +1 -0
- package/dist/telemetry/store.js +332 -0
- package/dist/telemetry/supabase-exporter.d.ts +64 -0
- package/dist/telemetry/supabase-exporter.d.ts.map +1 -0
- package/dist/telemetry/supabase-exporter.js +211 -0
- package/dist/tenancy/api-key-format.d.ts +67 -0
- package/dist/tenancy/api-key-format.d.ts.map +1 -0
- package/dist/tenancy/api-key-format.js +91 -0
- package/dist/tenancy/constants.d.ts +23 -0
- package/dist/tenancy/constants.d.ts.map +1 -0
- package/dist/tenancy/constants.js +25 -0
- package/dist/tenancy/scrub.d.ts +47 -0
- package/dist/tenancy/scrub.d.ts.map +1 -0
- package/dist/tenancy/scrub.js +85 -0
- package/dist/types/failures-teaching.d.ts +56 -0
- package/dist/types/failures-teaching.d.ts.map +1 -0
- package/dist/types/failures-teaching.js +177 -0
- package/dist/types/failures.d.ts +27 -0
- package/dist/types/failures.d.ts.map +1 -0
- package/dist/types/failures.js +41 -0
- package/dist/types/provider.d.ts +79 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/provider.js +20 -0
- package/dist/types/route.d.ts +127 -0
- package/dist/types/route.d.ts.map +1 -0
- package/dist/types/route.js +6 -0
- package/dist/types/telemetry.d.ts +89 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/dist/types/telemetry.js +6 -0
- package/dist/watch/budget.d.ts +19 -0
- package/dist/watch/budget.d.ts.map +1 -0
- package/dist/watch/budget.js +35 -0
- package/dist/watch/evaluate.d.ts +66 -0
- package/dist/watch/evaluate.d.ts.map +1 -0
- package/dist/watch/evaluate.js +94 -0
- package/dist/watch/proposals.d.ts +73 -0
- package/dist/watch/proposals.d.ts.map +1 -0
- package/dist/watch/proposals.js +169 -0
- package/dist/watch/run-record.d.ts +64 -0
- package/dist/watch/run-record.d.ts.map +1 -0
- package/dist/watch/run-record.js +86 -0
- package/dist/watch/run-reported.d.ts +20 -0
- package/dist/watch/run-reported.d.ts.map +1 -0
- package/dist/watch/run-reported.js +53 -0
- package/dist/watch/run.d.ts +40 -0
- package/dist/watch/run.d.ts.map +1 -0
- package/dist/watch/run.js +44 -0
- package/dist/watch/state-store.d.ts +48 -0
- package/dist/watch/state-store.d.ts.map +1 -0
- package/dist/watch/state-store.js +86 -0
- package/dist/watch/state.d.ts +21 -0
- package/dist/watch/state.d.ts.map +1 -0
- package/dist/watch/state.js +93 -0
- package/package.json +50 -14
- package/index.js +0 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bake-off engine — Phase 3 spec WS5 (closed core). Replays the last N
|
|
4
|
+
* captures through each named variant, aggregates per-variant metrics
|
|
5
|
+
* (stats.ts), and assembles the BakeoffArtifact with the proposal verdict.
|
|
6
|
+
*
|
|
7
|
+
* Proposal gate (builder decision, flagged): the incumbent is the "default"
|
|
8
|
+
* variant when compared, else the FIRST listed variant. A challenger
|
|
9
|
+
* proposes when (a) its conformance CI lower bound ≥ the incumbent's CI
|
|
10
|
+
* lower bound, and (b) its effective $/1K-conformant is strictly lower.
|
|
11
|
+
* Value gates beyond mechanical conformance arrive with Phase 4 judges —
|
|
12
|
+
* the artifact records value_accuracy where the hook supplies it.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.runBakeoff = runBakeoff;
|
|
16
|
+
exports.computeProposal = computeProposal;
|
|
17
|
+
exports.mirrorBakeoff = mirrorBakeoff;
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
const loader_1 = require("../routes/loader");
|
|
20
|
+
const replay_1 = require("./replay");
|
|
21
|
+
const stats_1 = require("./stats");
|
|
22
|
+
const samples_1 = require("./samples");
|
|
23
|
+
/** Arm metadata for the reviewer: which model/rung/scaffold served it. */
|
|
24
|
+
function deriveArmMeta(route, arm) {
|
|
25
|
+
if (arm === "default") {
|
|
26
|
+
return { model: route.candidates[0]?.model ?? null, rung: route.policy.json };
|
|
27
|
+
}
|
|
28
|
+
const variant = route.variants.find((v) => v.name === arm);
|
|
29
|
+
const model = (variant?.candidates?.[0] ?? route.candidates[0])?.model ?? null;
|
|
30
|
+
return {
|
|
31
|
+
model,
|
|
32
|
+
rung: variant?.json ?? route.policy.json,
|
|
33
|
+
...(variant?.scaffold !== undefined ? { scaffold: true } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function runBakeoff(deps, captures, req, samplesSink) {
|
|
37
|
+
if (req.variants.length === 0) {
|
|
38
|
+
throw new loader_1.RouteConfigError("bakeoff needs at least one variant (use \"default\" for the base config)");
|
|
39
|
+
}
|
|
40
|
+
const route = deps.routes.get(req.route);
|
|
41
|
+
for (const name of req.variants) {
|
|
42
|
+
if (name !== "default" && !route.variants.some((v) => v.name === name)) {
|
|
43
|
+
throw new loader_1.RouteConfigError(`route "${req.route}" has no variant "${name}" — defined: [default, ${route.variants
|
|
44
|
+
.map((v) => v.name)
|
|
45
|
+
.join(", ")}]`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const keepOutputs = req.keepOutputs !== undefined && samplesSink !== undefined;
|
|
49
|
+
const bakeoffId = (0, node_crypto_1.randomUUID)();
|
|
50
|
+
const metrics = [];
|
|
51
|
+
const rawByArm = new Map();
|
|
52
|
+
let replayN = 0;
|
|
53
|
+
for (const variant of req.variants) {
|
|
54
|
+
const samples = await (0, replay_1.replayVariant)(deps, captures, {
|
|
55
|
+
route: req.route,
|
|
56
|
+
variant,
|
|
57
|
+
lastN: req.replayLastN,
|
|
58
|
+
envelopeUsd: req.envelopeUsdPerVariant,
|
|
59
|
+
...(keepOutputs ? { keepOutputs: true } : {}),
|
|
60
|
+
});
|
|
61
|
+
replayN = Math.max(replayN, samples.length);
|
|
62
|
+
metrics.push((0, stats_1.aggregateVariantMetrics)(variant, samples));
|
|
63
|
+
if (keepOutputs)
|
|
64
|
+
rawByArm.set(variant, samples);
|
|
65
|
+
}
|
|
66
|
+
const proposal = computeProposal(req.variants, metrics);
|
|
67
|
+
// Persist review samples (A3) — AFTER the winner is known, for the selected
|
|
68
|
+
// arms only. Never touches the artifact (payloads stay off the anon table).
|
|
69
|
+
if (keepOutputs && samplesSink !== undefined) {
|
|
70
|
+
const incumbentArm = req.variants.includes("default") ? "default" : req.variants[0];
|
|
71
|
+
const keptArms = (0, samples_1.selectArmsToKeep)(req.keepOutputs, incumbentArm, proposal?.variant ?? null, req.variants);
|
|
72
|
+
const arms = keptArms.map((arm) => ({
|
|
73
|
+
arm,
|
|
74
|
+
armMeta: deriveArmMeta(route, arm),
|
|
75
|
+
samples: (rawByArm.get(arm) ?? []).map((s) => ({
|
|
76
|
+
inputRef: s.captureId,
|
|
77
|
+
inputText: s.inputText ?? "",
|
|
78
|
+
outputText: s.outputText ?? null,
|
|
79
|
+
conform: s.conform,
|
|
80
|
+
costUsd: s.costUsd,
|
|
81
|
+
latencyMs: s.latencyMs,
|
|
82
|
+
})),
|
|
83
|
+
}));
|
|
84
|
+
// Best-effort: a hiccup persisting the SECONDARY review samples must not
|
|
85
|
+
// lose the PRIMARY metrics artifact of an expensive bake-off. Warn and
|
|
86
|
+
// continue; the artifact still returns, mirrors, and writes to disk.
|
|
87
|
+
try {
|
|
88
|
+
await samplesSink({ bakeoffId, route: route.route, arms });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.warn(`[modelrig] bake-off ${bakeoffId}: review samples not persisted ` +
|
|
92
|
+
`(${err instanceof Error ? err.message : String(err)}) — metrics unaffected; ` +
|
|
93
|
+
"re-run with --keep-outputs to retry the samples.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
id: bakeoffId,
|
|
98
|
+
route: route.route,
|
|
99
|
+
routeVersion: route.version,
|
|
100
|
+
ranAt: new Date().toISOString(),
|
|
101
|
+
replayN,
|
|
102
|
+
variants: metrics,
|
|
103
|
+
proposal,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Winner among gate-passing challengers; null when nobody beats the
|
|
107
|
+
* incumbent. Exported for direct unit testing. */
|
|
108
|
+
function computeProposal(requested, metrics) {
|
|
109
|
+
const incumbentName = requested.includes("default") ? "default" : requested[0];
|
|
110
|
+
const incumbent = metrics.find((m) => m.variant === incumbentName);
|
|
111
|
+
// A1: unpriced cost columns under-count — an unpriced incumbent can't be
|
|
112
|
+
// compared against, and an unpriced challenger must never look cheap.
|
|
113
|
+
if (incumbent === undefined || incumbent.samples === 0 || incumbent.pricingMissing)
|
|
114
|
+
return null;
|
|
115
|
+
const challengers = metrics.filter((m) => m.variant !== incumbentName &&
|
|
116
|
+
m.samples > 0 &&
|
|
117
|
+
!m.pricingMissing &&
|
|
118
|
+
m.conformCi95[0] >= incumbent.conformCi95[0] &&
|
|
119
|
+
m.effectiveUsdPer1kConformant < incumbent.effectiveUsdPer1kConformant);
|
|
120
|
+
if (challengers.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
const winner = [...challengers].sort((a, b) => a.effectiveUsdPer1kConformant - b.effectiveUsdPer1kConformant)[0];
|
|
123
|
+
const savings = incumbent.effectiveUsdPer1kConformant === 0 ||
|
|
124
|
+
!Number.isFinite(incumbent.effectiveUsdPer1kConformant)
|
|
125
|
+
? 0
|
|
126
|
+
: ((incumbent.effectiveUsdPer1kConformant - winner.effectiveUsdPer1kConformant) /
|
|
127
|
+
incumbent.effectiveUsdPer1kConformant) *
|
|
128
|
+
100;
|
|
129
|
+
return { variant: winner.variant, vsIncumbentSavingsPct: savings };
|
|
130
|
+
}
|
|
131
|
+
/** Upsert the artifact into the `bakeoffs` mirror table (console read path).
|
|
132
|
+
* Same PostgREST transport pattern as the telemetry exporter; captures have
|
|
133
|
+
* no cloud surface — ONLY aggregate metrics travel. */
|
|
134
|
+
async function mirrorBakeoff(artifact, config) {
|
|
135
|
+
const fetchFn = config.fetchFn ?? fetch;
|
|
136
|
+
const base = config.supabaseUrl.replace(/\/$/, "");
|
|
137
|
+
const response = await fetchFn(`${base}/rest/v1/bakeoffs`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"content-type": "application/json",
|
|
141
|
+
apikey: config.serviceRoleKey,
|
|
142
|
+
authorization: `Bearer ${config.serviceRoleKey}`,
|
|
143
|
+
prefer: "resolution=merge-duplicates,return=minimal",
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify([
|
|
146
|
+
{
|
|
147
|
+
id: artifact.id,
|
|
148
|
+
route: artifact.route,
|
|
149
|
+
route_version: artifact.routeVersion,
|
|
150
|
+
ran_at: artifact.ranAt,
|
|
151
|
+
variants: artifact.variants.map((m) => m.variant),
|
|
152
|
+
// JSON has no Infinity: a variant with zero conformant outputs
|
|
153
|
+
// serializes effectiveUsdPer1kConformant as null (console renders ∞).
|
|
154
|
+
matrix: JSON.parse(JSON.stringify(artifact.variants)),
|
|
155
|
+
replay_n: artifact.replayN,
|
|
156
|
+
source_id: config.sourceId,
|
|
157
|
+
},
|
|
158
|
+
]),
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(`bakeoff mirror upsert → ${response.status} ${await response.text().catch(() => "")}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline shadow replay — Phase 3 spec WS4/WS5. Re-runs locally captured
|
|
3
|
+
* inputs against candidate variants with ZERO production exposure. Replay
|
|
4
|
+
* dispatches are real (billed) adapter calls, but: envelope-guarded, tagged
|
|
5
|
+
* as replay (distinct from production telemetry), and never written to
|
|
6
|
+
* captures (no replay-of-replay).
|
|
7
|
+
*
|
|
8
|
+
* Builder decisions (flagged for architect review):
|
|
9
|
+
* - Replay rows go into the SAME local store under the `replay:<id>` TAG
|
|
10
|
+
* NAMESPACE (the scaffold's sanctioned option A): every replay attempt row
|
|
11
|
+
* carries tags {replay, replay_capture, variant} — production queries
|
|
12
|
+
* filter them out by tag, and per-sample cost is reconstructed from the
|
|
13
|
+
* same rows (base + retries + repair + search — F1-correct).
|
|
14
|
+
* - ReplaySampleResult gained `repairCostUsd` (additive) so the engine can
|
|
15
|
+
* report repair cost separately from total cost.
|
|
16
|
+
* - Envelope breach stops the pass typed-ly: the breaching sample is
|
|
17
|
+
* recorded with failureClass budget_exhausted and the PARTIAL result set
|
|
18
|
+
* is returned (honest Ns beat discarded spend).
|
|
19
|
+
*/
|
|
20
|
+
import type { CaptureStore } from "../telemetry/capture";
|
|
21
|
+
import type { RunDeps } from "../run/run";
|
|
22
|
+
export interface ReplayRequest {
|
|
23
|
+
readonly route: string;
|
|
24
|
+
readonly variant: string;
|
|
25
|
+
readonly lastN: number;
|
|
26
|
+
readonly envelopeUsd: number;
|
|
27
|
+
/** Addendum A3: retain the rendered input + this arm's output on each
|
|
28
|
+
* sample for the human value review. Off by default (bounds memory + keeps
|
|
29
|
+
* the pure-metrics path clean). */
|
|
30
|
+
readonly keepOutputs?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface ReplaySampleResult {
|
|
33
|
+
readonly captureId: string;
|
|
34
|
+
readonly conform: boolean;
|
|
35
|
+
readonly repaired: boolean;
|
|
36
|
+
readonly valueAccuracy: number | null;
|
|
37
|
+
readonly costUsd: number;
|
|
38
|
+
/** Portion of costUsd spent on repair calls (repaired_by rows). */
|
|
39
|
+
readonly repairCostUsd: number;
|
|
40
|
+
/** Addendum A1: any attempt row lacked pricing — costUsd under-counts and
|
|
41
|
+
* this sample must never rank a variant as cheap. */
|
|
42
|
+
readonly pricingMissing: boolean;
|
|
43
|
+
readonly latencyMs: number;
|
|
44
|
+
readonly failureClass: string | null;
|
|
45
|
+
/** Addendum A3 (keepOutputs only): the replayed input, verbatim. */
|
|
46
|
+
readonly inputText?: string;
|
|
47
|
+
/** Addendum A3 (keepOutputs only): this arm's output for the review, or
|
|
48
|
+
* null when the arm produced none (a failure). */
|
|
49
|
+
readonly outputText?: string | null;
|
|
50
|
+
}
|
|
51
|
+
export declare function replayVariant(deps: RunDeps, captures: CaptureStore, req: ReplayRequest): Promise<readonly ReplaySampleResult[]>;
|
|
52
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/bakeoff/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;uCAEmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;yDACqD;IACrD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;sDACkD;IAClD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAsExC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Offline shadow replay — Phase 3 spec WS4/WS5. Re-runs locally captured
|
|
4
|
+
* inputs against candidate variants with ZERO production exposure. Replay
|
|
5
|
+
* dispatches are real (billed) adapter calls, but: envelope-guarded, tagged
|
|
6
|
+
* as replay (distinct from production telemetry), and never written to
|
|
7
|
+
* captures (no replay-of-replay).
|
|
8
|
+
*
|
|
9
|
+
* Builder decisions (flagged for architect review):
|
|
10
|
+
* - Replay rows go into the SAME local store under the `replay:<id>` TAG
|
|
11
|
+
* NAMESPACE (the scaffold's sanctioned option A): every replay attempt row
|
|
12
|
+
* carries tags {replay, replay_capture, variant} — production queries
|
|
13
|
+
* filter them out by tag, and per-sample cost is reconstructed from the
|
|
14
|
+
* same rows (base + retries + repair + search — F1-correct).
|
|
15
|
+
* - ReplaySampleResult gained `repairCostUsd` (additive) so the engine can
|
|
16
|
+
* report repair cost separately from total cost.
|
|
17
|
+
* - Envelope breach stops the pass typed-ly: the breaching sample is
|
|
18
|
+
* recorded with failureClass budget_exhausted and the PARTIAL result set
|
|
19
|
+
* is returned (honest Ns beat discarded spend).
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.replayVariant = replayVariant;
|
|
23
|
+
const node_crypto_1 = require("node:crypto");
|
|
24
|
+
const failures_1 = require("../types/failures");
|
|
25
|
+
const run_1 = require("../run/run");
|
|
26
|
+
async function replayVariant(deps, captures, req) {
|
|
27
|
+
const rows = captures.readLast(req.route, req.lastN);
|
|
28
|
+
const replayId = (0, node_crypto_1.randomUUID)();
|
|
29
|
+
const envelopeKey = `replay:${replayId}`;
|
|
30
|
+
// Zero production exposure knobs: no capture writes (no replay-of-replay),
|
|
31
|
+
// and the whole pass shares one hard envelope.
|
|
32
|
+
const replayDeps = {
|
|
33
|
+
...deps,
|
|
34
|
+
captures: null,
|
|
35
|
+
envelopeBudgetUsd: req.envelopeUsd,
|
|
36
|
+
};
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const capture of rows) {
|
|
39
|
+
const sampleTag = `${replayId}:${capture.inferenceId}`;
|
|
40
|
+
const started = Date.now();
|
|
41
|
+
let conform = false;
|
|
42
|
+
let repaired = false;
|
|
43
|
+
let failureClass = null;
|
|
44
|
+
let outputText = null;
|
|
45
|
+
try {
|
|
46
|
+
// Through the REAL run loop: the variant's template re-renders against
|
|
47
|
+
// the replay candidate's capability flags (never the captured prompt
|
|
48
|
+
// text — plan §10 replay-validity risk), repair/grounding rungs and
|
|
49
|
+
// retry budgets all behave exactly as production serving would.
|
|
50
|
+
const result = await (0, run_1.run)(replayDeps, req.route, {
|
|
51
|
+
input: JSON.parse(capture.variablesJson),
|
|
52
|
+
tags: { replay: replayId, replay_capture: sampleTag, variant: req.variant },
|
|
53
|
+
...(req.variant !== "default" ? { variant: req.variant } : {}),
|
|
54
|
+
budget: { envelope: envelopeKey },
|
|
55
|
+
});
|
|
56
|
+
conform = result.meta.validated;
|
|
57
|
+
repaired = result.meta.repairedBy !== null;
|
|
58
|
+
if (req.keepOutputs)
|
|
59
|
+
outputText = JSON.stringify(result.output, null, 2);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (!(err instanceof failures_1.RigFailureError))
|
|
63
|
+
throw err;
|
|
64
|
+
failureClass = err.failure.class;
|
|
65
|
+
}
|
|
66
|
+
const latencyMs = Date.now() - started;
|
|
67
|
+
// Per-sample cost from the tagged attempt rows — base + retries + repair
|
|
68
|
+
// + search, exactly what telemetry billed (F1-corrected).
|
|
69
|
+
const attemptRows = deps.telemetry.queryByTag("replay_capture", sampleTag);
|
|
70
|
+
const costUsd = attemptRows.reduce((a, r) => a + r.costEstimateUsd, 0);
|
|
71
|
+
const repairCostUsd = attemptRows
|
|
72
|
+
.filter((r) => r.repairedBy !== null)
|
|
73
|
+
.reduce((a, r) => a + r.costEstimateUsd, 0);
|
|
74
|
+
const pricingMissing = attemptRows.some((r) => r.pricingMissing);
|
|
75
|
+
results.push({
|
|
76
|
+
captureId: capture.inferenceId,
|
|
77
|
+
conform,
|
|
78
|
+
repaired,
|
|
79
|
+
valueAccuracy: null, // golden-set hook — expected values are Phase 4's judge/goldens work
|
|
80
|
+
costUsd,
|
|
81
|
+
repairCostUsd,
|
|
82
|
+
pricingMissing,
|
|
83
|
+
latencyMs,
|
|
84
|
+
failureClass,
|
|
85
|
+
...(req.keepOutputs
|
|
86
|
+
? { inputText: capture.variablesJson, outputText }
|
|
87
|
+
: {}),
|
|
88
|
+
});
|
|
89
|
+
if (failureClass === "budget_exhausted") {
|
|
90
|
+
break; // typed stop at the envelope; partial results stand
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bake-off review samples — Phase 4 addendum A3 (value-review-storage.md §11).
|
|
3
|
+
*
|
|
4
|
+
* A SEPARATE store from captures: rows exist only because a human ran
|
|
5
|
+
* `modelrig bakeoff --keep-outputs` intending a value review. They carry the
|
|
6
|
+
* rendered input + each arm's output so the human-in-the-loop diff (eval
|
|
7
|
+
* rung 1.5) happens in the console. Written by the CLI with the service-role
|
|
8
|
+
* key; read server-side only; deny-anon RLS. NEVER the anon staging posture.
|
|
9
|
+
*
|
|
10
|
+
* Transport is raw PostgREST over fetch (same pattern as the exporter).
|
|
11
|
+
*/
|
|
12
|
+
/** 256KB per output, per the architect's caps; TOAST compresses at rest. */
|
|
13
|
+
export declare const OUTPUT_MAX_BYTES: number;
|
|
14
|
+
export type KeepOutputsMode = "winner" | "all";
|
|
15
|
+
export interface BakeoffSample {
|
|
16
|
+
readonly bakeoffId: string;
|
|
17
|
+
readonly route: string;
|
|
18
|
+
readonly arm: string;
|
|
19
|
+
readonly armMeta: Record<string, unknown> | null;
|
|
20
|
+
readonly inputRef: string;
|
|
21
|
+
readonly inputText: string;
|
|
22
|
+
readonly outputText: string | null;
|
|
23
|
+
readonly conform: boolean;
|
|
24
|
+
readonly costUsd: number;
|
|
25
|
+
readonly latencyMs: number;
|
|
26
|
+
readonly expiresAt: string;
|
|
27
|
+
}
|
|
28
|
+
/** One arm's per-input samples, ready to persist. */
|
|
29
|
+
export interface ArmSamples {
|
|
30
|
+
readonly arm: string;
|
|
31
|
+
readonly armMeta: Record<string, unknown> | null;
|
|
32
|
+
readonly samples: ReadonlyArray<{
|
|
33
|
+
readonly inputRef: string;
|
|
34
|
+
readonly inputText: string;
|
|
35
|
+
readonly outputText: string | null;
|
|
36
|
+
readonly conform: boolean;
|
|
37
|
+
readonly costUsd: number;
|
|
38
|
+
readonly latencyMs: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/** Default persists incumbent + winner only; "all" persists every arm. */
|
|
42
|
+
export declare function selectArmsToKeep(mode: KeepOutputsMode, incumbentArm: string, winnerArm: string | null, allArms: readonly string[]): string[];
|
|
43
|
+
export declare function truncateOutput(text: string | null): string | null;
|
|
44
|
+
/** Default retention: 90 days from now (or pass a sooner stamp). Pure over
|
|
45
|
+
* an injected `now` so it stays testable and workflow-safe. */
|
|
46
|
+
export declare function retentionExpiry(nowMs: number, days?: number): string;
|
|
47
|
+
export declare function buildBakeoffSamples(bakeoffId: string, route: string, arms: readonly ArmSamples[], expiresAt: string): BakeoffSample[];
|
|
48
|
+
export interface BakeoffSamplesClientConfig {
|
|
49
|
+
readonly supabaseUrl: string;
|
|
50
|
+
readonly serviceRoleKey: string;
|
|
51
|
+
readonly fetchFn?: typeof fetch;
|
|
52
|
+
}
|
|
53
|
+
export interface BakeoffSamplesClient {
|
|
54
|
+
insert(samples: readonly BakeoffSample[]): Promise<void>;
|
|
55
|
+
/** Purge by bakeoff id, or everything past its expiry when no id given. */
|
|
56
|
+
purge(opts?: {
|
|
57
|
+
bakeoffId?: string;
|
|
58
|
+
nowIso?: string;
|
|
59
|
+
}): Promise<number>;
|
|
60
|
+
}
|
|
61
|
+
export declare function createBakeoffSamplesClient(config: BakeoffSamplesClientConfig): BakeoffSamplesClient;
|
|
62
|
+
//# sourceMappingURL=samples.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"samples.d.ts","sourceRoot":"","sources":["../../src/bakeoff/samples.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAE3C,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;QAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;QAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,CAAC,CAAC;CACJ;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,eAAe,EACrB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,MAAM,EAAE,CAKV;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAOjE;AAED;+DAC+D;AAC/D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,MAAM,CAEhE;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,SAAS,EAAE,MAAM,GAChB,aAAa,EAAE,CAoBjB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,2EAA2E;IAC3E,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACxE;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,0BAA0B,GAAG,oBAAoB,CAoEnG"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bake-off review samples — Phase 4 addendum A3 (value-review-storage.md §11).
|
|
4
|
+
*
|
|
5
|
+
* A SEPARATE store from captures: rows exist only because a human ran
|
|
6
|
+
* `modelrig bakeoff --keep-outputs` intending a value review. They carry the
|
|
7
|
+
* rendered input + each arm's output so the human-in-the-loop diff (eval
|
|
8
|
+
* rung 1.5) happens in the console. Written by the CLI with the service-role
|
|
9
|
+
* key; read server-side only; deny-anon RLS. NEVER the anon staging posture.
|
|
10
|
+
*
|
|
11
|
+
* Transport is raw PostgREST over fetch (same pattern as the exporter).
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.OUTPUT_MAX_BYTES = void 0;
|
|
15
|
+
exports.selectArmsToKeep = selectArmsToKeep;
|
|
16
|
+
exports.truncateOutput = truncateOutput;
|
|
17
|
+
exports.retentionExpiry = retentionExpiry;
|
|
18
|
+
exports.buildBakeoffSamples = buildBakeoffSamples;
|
|
19
|
+
exports.createBakeoffSamplesClient = createBakeoffSamplesClient;
|
|
20
|
+
/** 256KB per output, per the architect's caps; TOAST compresses at rest. */
|
|
21
|
+
exports.OUTPUT_MAX_BYTES = 256 * 1024;
|
|
22
|
+
/** Default persists incumbent + winner only; "all" persists every arm. */
|
|
23
|
+
function selectArmsToKeep(mode, incumbentArm, winnerArm, allArms) {
|
|
24
|
+
if (mode === "all")
|
|
25
|
+
return [...allArms];
|
|
26
|
+
const keep = [incumbentArm];
|
|
27
|
+
if (winnerArm !== null && winnerArm !== incumbentArm)
|
|
28
|
+
keep.push(winnerArm);
|
|
29
|
+
return keep;
|
|
30
|
+
}
|
|
31
|
+
function truncateOutput(text) {
|
|
32
|
+
if (text === null)
|
|
33
|
+
return null;
|
|
34
|
+
if (Buffer.byteLength(text, "utf8") <= exports.OUTPUT_MAX_BYTES)
|
|
35
|
+
return text;
|
|
36
|
+
// Cut on a byte budget, then tag it so the reviewer knows it's clipped.
|
|
37
|
+
const marker = "\n\n[truncated at 256KB for review storage]";
|
|
38
|
+
const budget = exports.OUTPUT_MAX_BYTES - Buffer.byteLength(marker, "utf8");
|
|
39
|
+
return Buffer.from(text, "utf8").subarray(0, budget).toString("utf8") + marker;
|
|
40
|
+
}
|
|
41
|
+
/** Default retention: 90 days from now (or pass a sooner stamp). Pure over
|
|
42
|
+
* an injected `now` so it stays testable and workflow-safe. */
|
|
43
|
+
function retentionExpiry(nowMs, days = 90) {
|
|
44
|
+
return new Date(nowMs + days * 24 * 3600 * 1000).toISOString();
|
|
45
|
+
}
|
|
46
|
+
function buildBakeoffSamples(bakeoffId, route, arms, expiresAt) {
|
|
47
|
+
const rows = [];
|
|
48
|
+
for (const arm of arms) {
|
|
49
|
+
for (const s of arm.samples) {
|
|
50
|
+
rows.push({
|
|
51
|
+
bakeoffId,
|
|
52
|
+
route,
|
|
53
|
+
arm: arm.arm,
|
|
54
|
+
armMeta: arm.armMeta,
|
|
55
|
+
inputRef: s.inputRef,
|
|
56
|
+
inputText: s.inputText,
|
|
57
|
+
outputText: truncateOutput(s.outputText),
|
|
58
|
+
conform: s.conform,
|
|
59
|
+
costUsd: s.costUsd,
|
|
60
|
+
latencyMs: s.latencyMs,
|
|
61
|
+
expiresAt,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rows;
|
|
66
|
+
}
|
|
67
|
+
function createBakeoffSamplesClient(config) {
|
|
68
|
+
const fetchFn = config.fetchFn ?? fetch;
|
|
69
|
+
const base = config.supabaseUrl.replace(/\/$/, "");
|
|
70
|
+
async function request(path, init) {
|
|
71
|
+
const response = await fetchFn(`${base}/rest/v1/${path}`, {
|
|
72
|
+
...init,
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
apikey: config.serviceRoleKey,
|
|
76
|
+
authorization: `Bearer ${config.serviceRoleKey}`,
|
|
77
|
+
...(init.headers ?? {}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`bakeoff_samples ${init.method ?? "GET"} ${path} → ${response.status} ` +
|
|
82
|
+
`${await response.text().catch(() => "")}`);
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async insert(samples) {
|
|
88
|
+
if (samples.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
// Chunk the POST: at the ≤50-inputs cap, large-input routes (e.g. the
|
|
91
|
+
// cutover steps pass ~600KB assembled prompts) would make a single body
|
|
92
|
+
// tens of MB and blow past PostgREST's limit. Small batches keep every
|
|
93
|
+
// body well under it, at any cap.
|
|
94
|
+
const CHUNK = 8;
|
|
95
|
+
for (let i = 0; i < samples.length; i += CHUNK) {
|
|
96
|
+
const batch = samples.slice(i, i + CHUNK);
|
|
97
|
+
await request("bakeoff_samples", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { prefer: "return=minimal" },
|
|
100
|
+
body: JSON.stringify(batch.map((s) => ({
|
|
101
|
+
bakeoff_id: s.bakeoffId,
|
|
102
|
+
route: s.route,
|
|
103
|
+
arm: s.arm,
|
|
104
|
+
arm_meta: s.armMeta,
|
|
105
|
+
input_ref: s.inputRef,
|
|
106
|
+
input_text: s.inputText,
|
|
107
|
+
output_text: s.outputText,
|
|
108
|
+
conform: s.conform,
|
|
109
|
+
cost_usd: s.costUsd,
|
|
110
|
+
latency_ms: s.latencyMs,
|
|
111
|
+
expires_at: s.expiresAt,
|
|
112
|
+
}))),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
async purge(opts) {
|
|
117
|
+
const filter = opts?.bakeoffId !== undefined
|
|
118
|
+
? `bakeoff_id=eq.${encodeURIComponent(opts.bakeoffId)}`
|
|
119
|
+
: `expires_at=lt.${encodeURIComponent(opts?.nowIso ?? new Date().toISOString())}`;
|
|
120
|
+
const response = await request(`bakeoff_samples?${filter}`, {
|
|
121
|
+
method: "DELETE",
|
|
122
|
+
headers: { prefer: "return=representation" },
|
|
123
|
+
});
|
|
124
|
+
const deleted = (await response.json().catch(() => []));
|
|
125
|
+
return Array.isArray(deleted) ? deleted.length : 0;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bake-off statistics — Phase 3 spec WS5. Wilson 95% intervals ported from
|
|
3
|
+
* the probe suite's summarize math (same constants — results comparable);
|
|
4
|
+
* no cross-package import (the probes package is standalone since P2).
|
|
5
|
+
*
|
|
6
|
+
* The headline: effective $ per 1K conformant outputs — TOTAL sample cost
|
|
7
|
+
* (base + retries + repair + search, F1-corrected) ÷ conformant count × 1000.
|
|
8
|
+
*/
|
|
9
|
+
import type { ReplaySampleResult } from "./replay";
|
|
10
|
+
import type { VariantMetrics } from "./types";
|
|
11
|
+
export declare function wilson95(successes: number, n: number): readonly [number, number];
|
|
12
|
+
export declare function aggregateVariantMetrics(variant: string, samples: readonly ReplaySampleResult[]): VariantMetrics;
|
|
13
|
+
//# sourceMappingURL=stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/bakeoff/stats.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAShF;AAMD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,kBAAkB,EAAE,GACrC,cAAc,CAuBhB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bake-off statistics — Phase 3 spec WS5. Wilson 95% intervals ported from
|
|
4
|
+
* the probe suite's summarize math (same constants — results comparable);
|
|
5
|
+
* no cross-package import (the probes package is standalone since P2).
|
|
6
|
+
*
|
|
7
|
+
* The headline: effective $ per 1K conformant outputs — TOTAL sample cost
|
|
8
|
+
* (base + retries + repair + search, F1-corrected) ÷ conformant count × 1000.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.wilson95 = wilson95;
|
|
12
|
+
exports.aggregateVariantMetrics = aggregateVariantMetrics;
|
|
13
|
+
function wilson95(successes, n) {
|
|
14
|
+
if (n === 0)
|
|
15
|
+
return [0, 1];
|
|
16
|
+
const z = 1.959963984540054;
|
|
17
|
+
const p = successes / n;
|
|
18
|
+
const z2 = z * z;
|
|
19
|
+
const denominator = 1 + z2 / n;
|
|
20
|
+
const center = (p + z2 / (2 * n)) / denominator;
|
|
21
|
+
const half = (z * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n))) / denominator;
|
|
22
|
+
return [Math.max(0, center - half), Math.min(1, center + half)];
|
|
23
|
+
}
|
|
24
|
+
function mean(values) {
|
|
25
|
+
return values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length;
|
|
26
|
+
}
|
|
27
|
+
function aggregateVariantMetrics(variant, samples) {
|
|
28
|
+
const n = samples.length;
|
|
29
|
+
const conformant = samples.filter((s) => s.conform);
|
|
30
|
+
const repaired = samples.filter((s) => s.repaired);
|
|
31
|
+
const accuracies = samples
|
|
32
|
+
.map((s) => s.valueAccuracy)
|
|
33
|
+
.filter((a) => a !== null);
|
|
34
|
+
const totalCostUsd = samples.reduce((a, s) => a + s.costUsd, 0);
|
|
35
|
+
return {
|
|
36
|
+
variant,
|
|
37
|
+
samples: n,
|
|
38
|
+
conformRate: n === 0 ? 0 : conformant.length / n,
|
|
39
|
+
conformCi95: wilson95(conformant.length, n),
|
|
40
|
+
repairRate: n === 0 ? 0 : repaired.length / n,
|
|
41
|
+
meanRepairCostUsd: mean(repaired.map((s) => s.repairCostUsd)),
|
|
42
|
+
valueAccuracyMean: accuracies.length === 0 ? null : mean(accuracies),
|
|
43
|
+
meanLatencyMs: mean(samples.map((s) => s.latencyMs)),
|
|
44
|
+
meanCostUsd: n === 0 ? 0 : totalCostUsd / n,
|
|
45
|
+
effectiveUsdPer1kConformant: conformant.length === 0 ? Number.POSITIVE_INFINITY : (totalCostUsd / conformant.length) * 1000,
|
|
46
|
+
pricingMissing: samples.some((s) => s.pricingMissing),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance bake-off contracts — Phase 3 spec WS5 (closed core).
|
|
3
|
+
* The headline metric: effective $ per 1K conformant outputs, with repair
|
|
4
|
+
* and search costs included (F1-corrected accounting).
|
|
5
|
+
*/
|
|
6
|
+
export interface VariantMetrics {
|
|
7
|
+
readonly variant: string;
|
|
8
|
+
readonly samples: number;
|
|
9
|
+
readonly conformRate: number;
|
|
10
|
+
readonly conformCi95: readonly [number, number];
|
|
11
|
+
readonly repairRate: number;
|
|
12
|
+
readonly meanRepairCostUsd: number;
|
|
13
|
+
/** Golden-set value accuracy where the route defines expectedValues; null otherwise. */
|
|
14
|
+
readonly valueAccuracyMean: number | null;
|
|
15
|
+
readonly meanLatencyMs: number;
|
|
16
|
+
readonly meanCostUsd: number;
|
|
17
|
+
/** (base + repair + search costs) / conformant outputs × 1000 — the number. */
|
|
18
|
+
readonly effectiveUsdPer1kConformant: number;
|
|
19
|
+
/** Addendum A1: any sample served by a model with no pricing entry —
|
|
20
|
+
* cost columns under-count; this variant is never ranked or proposed. */
|
|
21
|
+
readonly pricingMissing: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface BakeoffArtifact {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly route: string;
|
|
26
|
+
readonly routeVersion: number;
|
|
27
|
+
readonly ranAt: string;
|
|
28
|
+
readonly replayN: number;
|
|
29
|
+
readonly variants: readonly VariantMetrics[];
|
|
30
|
+
/** Winner by effectiveUsdPer1kConformant among variants meeting the gates;
|
|
31
|
+
* null when no variant beats the incumbent's gates. */
|
|
32
|
+
readonly proposal: {
|
|
33
|
+
readonly variant: string;
|
|
34
|
+
readonly vsIncumbentSavingsPct: number;
|
|
35
|
+
} | null;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/bakeoff/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,wFAAwF;IACxF,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,2BAA2B,EAAE,MAAM,CAAC;IAC7C;6EACyE;IACzE,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C;2DACuD;IACvD,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChG"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Conformance bake-off contracts — Phase 3 spec WS5 (closed core).
|
|
4
|
+
* The headline metric: effective $ per 1K conformant outputs, with repair
|
|
5
|
+
* and search costs included (F1-corrected accounting).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
// Implemented: engine.ts assembles this from replay results; stats.ts owns
|
|
9
|
+
// the math (Wilson CI — ported from the probes' summarize approach, no
|
|
10
|
+
// cross-package import).
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* modelrig CLI — Phase 3 plan §3 + Phase 4 plan §3:
|
|
4
|
+
* modelrig bakeoff --route X --variants a,b,c --replay-last N
|
|
5
|
+
* [--envelope usd] [--json] [--no-mirror]
|
|
6
|
+
* modelrig bakeoff --from-proposal <id> [--replay-last N] [--json]
|
|
7
|
+
* modelrig watch run [--events fixtures.json] [--json]
|
|
8
|
+
*
|
|
9
|
+
* Bake-offs replay locally captured traffic, write the artifact JSON next
|
|
10
|
+
* to the telemetry db (.modelrig/bakeoffs/<id>.json), upsert the `bakeoffs`
|
|
11
|
+
* mirror when the Supabase sink is configured, and print the matrix.
|
|
12
|
+
* Captures never travel — only aggregate metrics. Watch cycles spend $0;
|
|
13
|
+
* --from-proposal runs draw from the monthly watch envelope and attach
|
|
14
|
+
* their evidence to the proposal (suggested → evidenced). Nothing here
|
|
15
|
+
* executes a swap — that is `modelrig swap execute --approve`, a separate,
|
|
16
|
+
* human command.
|
|
17
|
+
*/
|
|
18
|
+
import type { BakeoffArtifact } from "./bakeoff/types";
|
|
19
|
+
export interface Flags {
|
|
20
|
+
readonly positional: readonly string[];
|
|
21
|
+
readonly named: ReadonlyMap<string, string>;
|
|
22
|
+
readonly bare: ReadonlySet<string>;
|
|
23
|
+
}
|
|
24
|
+
export declare function parseArgs(argv: readonly string[]): Flags;
|
|
25
|
+
export declare function printMatrix(artifact: BakeoffArtifact): void;
|
|
26
|
+
export declare function main(argv: readonly string[]): Promise<number>;
|
|
27
|
+
//# sourceMappingURL=cli.d.ts.map
|