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,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Candidate resolution — plan §4. THE ONLY constructor of CandidateRef in the
|
|
4
|
+
* codebase (the candidate-set invariant is enforced here structurally).
|
|
5
|
+
* Phase 1 selection: first candidate whose static capability flags satisfy
|
|
6
|
+
* route.require, ordered by prefer:[cost] via the pricing snapshot; on a
|
|
7
|
+
* retryable failure the run loop asks for the next candidate.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.resolveCandidates = resolveCandidates;
|
|
11
|
+
exports.resolveDeclaredCandidate = resolveDeclaredCandidate;
|
|
12
|
+
function satisfiesRequirements(route, flags, searchAvailable) {
|
|
13
|
+
for (const requirement of route.require) {
|
|
14
|
+
switch (requirement) {
|
|
15
|
+
case "schema_conformant":
|
|
16
|
+
// The native rung demands native strict enforcement; the json_mode
|
|
17
|
+
// rung accepts either mechanism (a native-capable model still serves
|
|
18
|
+
// a json_mode route).
|
|
19
|
+
if (route.policy.json === "native") {
|
|
20
|
+
if (!flags.has("structured_native"))
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
else if (!flags.has("structured_native") && !flags.has("json_mode")) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
case "grounded":
|
|
28
|
+
// Phase 3 WS3: a configured search provider satisfies `grounded` for
|
|
29
|
+
// candidates without native grounding — the run loop orchestrates
|
|
30
|
+
// search-and-inject for them.
|
|
31
|
+
if (!flags.has("grounded_native") && !searchAvailable)
|
|
32
|
+
return false;
|
|
33
|
+
break;
|
|
34
|
+
case "trace_visible":
|
|
35
|
+
// Phase 3 WS6: reasoning-observability — the model's chain of thought
|
|
36
|
+
// is exposed to the caller (regulated/audit buyers hard-require this).
|
|
37
|
+
if (!flags.has("trace_visible"))
|
|
38
|
+
return false;
|
|
39
|
+
break;
|
|
40
|
+
case "zero_retention":
|
|
41
|
+
// Phase 1: validated by the loader, not solvable — no capability flag
|
|
42
|
+
// exists yet (registry work, Phase 2). Declared intent only.
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function resolveCandidates(route, capabilities, costUsdPerMTok, searchAvailable = false) {
|
|
49
|
+
const eligible = route.candidates.filter((c) => satisfiesRequirements(route, capabilities(c.provider, c.model), searchAvailable));
|
|
50
|
+
// prefer: [cost] — ascending, unknown cost sorts last, stable otherwise.
|
|
51
|
+
// "latency" is advisory in Phase 1 (no probe data): declared order stands.
|
|
52
|
+
const ordered = [...eligible];
|
|
53
|
+
if (route.prefer.includes("cost")) {
|
|
54
|
+
const cost = (c) => {
|
|
55
|
+
const usd = costUsdPerMTok(c.provider, c.model);
|
|
56
|
+
return usd === null ? Number.POSITIVE_INFINITY : usd;
|
|
57
|
+
};
|
|
58
|
+
ordered.sort((a, b) => cost(a) - cost(b));
|
|
59
|
+
}
|
|
60
|
+
// THE brand site — the only place in the codebase a CandidateRef may be
|
|
61
|
+
// minted. The cast below is the single sanctioned exception to the
|
|
62
|
+
// "no `as CandidateRef`" grep (scoped to run/resolve.ts in CI).
|
|
63
|
+
const refs = ordered.map((c) => Object.freeze({ provider: c.provider, model: c.model }));
|
|
64
|
+
let index = 0;
|
|
65
|
+
return {
|
|
66
|
+
next() {
|
|
67
|
+
return index < refs.length ? refs[index++] : null;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Mint a CandidateRef for a SPECIFIC declared candidate (the repair rung's
|
|
73
|
+
* designated repair model — loader-validated to be in the route's candidate
|
|
74
|
+
* list; re-checked here). Same brand site, same sanctioned-cast scope.
|
|
75
|
+
*/
|
|
76
|
+
function resolveDeclaredCandidate(route, spec) {
|
|
77
|
+
const declared = route.candidates.some((c) => c.provider === spec.provider && c.model === spec.model);
|
|
78
|
+
if (!declared)
|
|
79
|
+
return null;
|
|
80
|
+
return Object.freeze({ provider: spec.provider, model: spec.model });
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-class retry accounting — spec §2, plan §4. Budgets are NON-FUNGIBLE:
|
|
3
|
+
* exhausting one class never consumes another. Terminal classes
|
|
4
|
+
* (budget_exhausted, invariant_violation) are never retried.
|
|
5
|
+
*/
|
|
6
|
+
import type { FailureClass } from "../types/failures";
|
|
7
|
+
import type { RoutePolicy } from "../types/route";
|
|
8
|
+
export interface RetryLedger {
|
|
9
|
+
/** True if a retry is permitted for this class (and consumes one unit). */
|
|
10
|
+
consume(cls: FailureClass): boolean;
|
|
11
|
+
remaining(cls: FailureClass): number;
|
|
12
|
+
snapshot(): Partial<Record<FailureClass, number>>;
|
|
13
|
+
/** Backoff before the retry the most recent consume() granted — per-class:
|
|
14
|
+
* exponential for capacity_shed (30s base, mirrors the production flex
|
|
15
|
+
* backoff ladder) and network (1s base); immediate for content classes. */
|
|
16
|
+
delayMs(cls: FailureClass): number;
|
|
17
|
+
}
|
|
18
|
+
export declare function createRetryLedger(policy: RoutePolicy): RetryLedger;
|
|
19
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/run/retry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC;IACpC,SAAS,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC;IACrC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD;;+EAE2E;IAC3E,OAAO,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC;CACpC;AAMD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAsClE"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-class retry accounting — spec §2, plan §4. Budgets are NON-FUNGIBLE:
|
|
4
|
+
* exhausting one class never consumes another. Terminal classes
|
|
5
|
+
* (budget_exhausted, invariant_violation) are never retried.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createRetryLedger = createRetryLedger;
|
|
9
|
+
const failures_1 = require("../types/failures");
|
|
10
|
+
const CAPACITY_BACKOFF_MS = [30000, 60000, 120000, 240000];
|
|
11
|
+
const NETWORK_BACKOFF_BASE_MS = 1000;
|
|
12
|
+
const NETWORK_BACKOFF_CAP_MS = 30000;
|
|
13
|
+
function createRetryLedger(policy) {
|
|
14
|
+
const used = new Map();
|
|
15
|
+
function budget(cls) {
|
|
16
|
+
return policy.retries[cls] ?? 0;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
consume(cls) {
|
|
20
|
+
if (failures_1.TERMINAL_CLASSES.has(cls))
|
|
21
|
+
return false;
|
|
22
|
+
const attemptsUsed = used.get(cls) ?? 0;
|
|
23
|
+
if (attemptsUsed >= budget(cls))
|
|
24
|
+
return false;
|
|
25
|
+
used.set(cls, attemptsUsed + 1);
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
remaining(cls) {
|
|
29
|
+
if (failures_1.TERMINAL_CLASSES.has(cls))
|
|
30
|
+
return 0;
|
|
31
|
+
return Math.max(0, budget(cls) - (used.get(cls) ?? 0));
|
|
32
|
+
},
|
|
33
|
+
snapshot() {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [cls, n] of used)
|
|
36
|
+
out[cls] = n;
|
|
37
|
+
return out;
|
|
38
|
+
},
|
|
39
|
+
delayMs(cls) {
|
|
40
|
+
const attempt = Math.max(0, (used.get(cls) ?? 1) - 1);
|
|
41
|
+
if (cls === "capacity_shed") {
|
|
42
|
+
return CAPACITY_BACKOFF_MS[Math.min(attempt, CAPACITY_BACKOFF_MS.length - 1)];
|
|
43
|
+
}
|
|
44
|
+
if (cls === "network") {
|
|
45
|
+
return Math.min(NETWORK_BACKOFF_BASE_MS * 2 ** attempt, NETWORK_BACKOFF_CAP_MS);
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run loop — spec §4, plan §4/step 6. State machine:
|
|
3
|
+
* resolve → render → (envelope check) → dispatch → validate
|
|
4
|
+
* → retry per failure class | advance candidate | fail typed | succeed
|
|
5
|
+
* → record telemetry on every INFERENCE exit path (RouteConfigError exits
|
|
6
|
+
* record nothing by design — config errors precede any dispatch; C1
|
|
7
|
+
* ruling 2 / finding F3).
|
|
8
|
+
*/
|
|
9
|
+
import type { LoadedRoutes } from "../routes/loader";
|
|
10
|
+
import type { ProviderAdapter } from "../types/provider";
|
|
11
|
+
import type { RigManifest } from "../routes/manifest";
|
|
12
|
+
import type { RunOptions, RunResult } from "../types/route";
|
|
13
|
+
import type { TelemetryStore } from "../types/telemetry";
|
|
14
|
+
import type { CaptureStore } from "../telemetry/capture";
|
|
15
|
+
import type { CapabilityLookup } from "./resolve";
|
|
16
|
+
import type { SearchProvider } from "./grounding";
|
|
17
|
+
export interface RunDeps {
|
|
18
|
+
readonly routes: LoadedRoutes;
|
|
19
|
+
readonly adapters: ReadonlyMap<string, ProviderAdapter>;
|
|
20
|
+
readonly telemetry: TelemetryStore;
|
|
21
|
+
readonly capabilities: CapabilityLookup;
|
|
22
|
+
/** Budget for envelopes first opened by this rig (see envelope.ts default). */
|
|
23
|
+
readonly envelopeBudgetUsd?: number;
|
|
24
|
+
/** Addendum A1: USD per million tokens charged against an ACTIVE envelope
|
|
25
|
+
* when a model has no pricing entry — envelopes fail closed, never free.
|
|
26
|
+
* Deliberately generous-high by default. */
|
|
27
|
+
readonly unpricedRateUsdPerMTok?: number;
|
|
28
|
+
/** Rig manifest (declared dimensions) — null/omitted when no rig.yaml. */
|
|
29
|
+
readonly manifest?: RigManifest | null;
|
|
30
|
+
/** Local-only capture store (Phase 3 WS4) — writes gated per route by the
|
|
31
|
+
* bundle's `capture: true`; null/omitted disables capture entirely. */
|
|
32
|
+
readonly captures?: CaptureStore | null;
|
|
33
|
+
/** Search provider for the grounding-inject rung (Phase 3 WS3) —
|
|
34
|
+
* null/omitted means `require: grounded` resolves natively-grounded
|
|
35
|
+
* candidates only. */
|
|
36
|
+
readonly search?: SearchProvider | null;
|
|
37
|
+
/** Injectable backoff sleep — tests pass a no-op. */
|
|
38
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare function run(deps: RunDeps, routeName: string, opts: RunOptions): Promise<RunResult>;
|
|
41
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAMrD,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,mBAAmB,CAAC;AAGvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAsC,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AASlD,OAAO,KAAK,EAAE,cAAc,EAAoC,MAAM,aAAa,CAAC;AAGpF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;gDAE4C;IAC5C,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC;2EACuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACxC;;0BAEsB;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAwFD,wBAAsB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwiBhG"}
|
package/dist/run/run.js
ADDED
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output validation — plan §4. Ajv compiled once per (route, version), cached.
|
|
3
|
+
* Phase 1 checks schema conformance; value-accuracy gates arrive with the
|
|
4
|
+
* bake-off in Phase 3.
|
|
5
|
+
*/
|
|
6
|
+
import type { RouteBundle } from "../types/route";
|
|
7
|
+
export interface ValidationOutcome {
|
|
8
|
+
readonly valid: boolean;
|
|
9
|
+
/** ajv error summaries, capped, human- and agent-readable. */
|
|
10
|
+
readonly errors: readonly string[];
|
|
11
|
+
/** Parsed output when text was JSON-parseable (even if schema-invalid). */
|
|
12
|
+
readonly parsed: unknown;
|
|
13
|
+
}
|
|
14
|
+
export declare function validateOutput(route: RouteBundle, text: string): ValidationOutcome;
|
|
15
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/run/validate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAsBD,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,iBAAiB,CA6BlF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Output validation — plan §4. Ajv compiled once per (route, version), cached.
|
|
4
|
+
* Phase 1 checks schema conformance; value-accuracy gates arrive with the
|
|
5
|
+
* bake-off in Phase 3.
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.validateOutput = validateOutput;
|
|
12
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
13
|
+
// Mirrors the production probe harness: new Ajv({ strict: false, allErrors:
|
|
14
|
+
// true }) — the v3 schemas trip ajv strict-mode warnings by design.
|
|
15
|
+
const ajv = new ajv_1.default({ strict: false, allErrors: true, allowUnionTypes: true });
|
|
16
|
+
const MAX_ERRORS = 12;
|
|
17
|
+
/** Compiled validators cached per (route, version) — recompiling a 300-line
|
|
18
|
+
* schema per attempt is measurable. */
|
|
19
|
+
const validatorCache = new Map();
|
|
20
|
+
function validatorFor(route) {
|
|
21
|
+
const key = `${route.route}@${route.version}`;
|
|
22
|
+
let compiled = validatorCache.get(key);
|
|
23
|
+
if (compiled === undefined) {
|
|
24
|
+
compiled = ajv.compile(route.outputSchema);
|
|
25
|
+
validatorCache.set(key, compiled);
|
|
26
|
+
}
|
|
27
|
+
return compiled;
|
|
28
|
+
}
|
|
29
|
+
function validateOutput(route, text) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
errors: [`output is not valid JSON: ${err.message}`],
|
|
38
|
+
parsed: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (route.outputSchema === null) {
|
|
42
|
+
return { valid: true, errors: [], parsed };
|
|
43
|
+
}
|
|
44
|
+
const validate = validatorFor(route);
|
|
45
|
+
if (validate(parsed)) {
|
|
46
|
+
return { valid: true, errors: [], parsed };
|
|
47
|
+
}
|
|
48
|
+
const all = validate.errors ?? [];
|
|
49
|
+
const errors = all
|
|
50
|
+
.slice(0, MAX_ERRORS)
|
|
51
|
+
.map((e) => `${e.instancePath || "/"} ${e.message ?? "invalid"}${e.keyword === "enum" ? ` (${JSON.stringify(e.params)})` : ""}`);
|
|
52
|
+
if (all.length > MAX_ERRORS) {
|
|
53
|
+
errors.push(`… ${all.length - MAX_ERRORS} more errors`);
|
|
54
|
+
}
|
|
55
|
+
return { valid: false, errors, parsed };
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swap-change emission — Phase 4 WS4 (plan §4/§7). The executor NEVER edits
|
|
3
|
+
* route files: it renders the exact YAML the human applies via git, plus the
|
|
4
|
+
* revert block. This file deliberately imports no filesystem API — the
|
|
5
|
+
* "actions live in your code" invariant is structural, and emit.test.ts
|
|
6
|
+
* greps for it.
|
|
7
|
+
*/
|
|
8
|
+
import type { CandidateSpec, RouteBundle } from "../types/route";
|
|
9
|
+
export interface SwapChange {
|
|
10
|
+
readonly route: string;
|
|
11
|
+
readonly file: string;
|
|
12
|
+
readonly fromModel: string;
|
|
13
|
+
readonly toModel: string;
|
|
14
|
+
readonly newVersion: number;
|
|
15
|
+
/** Paste-ready YAML fragment applying the swap (complete, no elisions). */
|
|
16
|
+
readonly yaml: string;
|
|
17
|
+
/** Paste-ready YAML fragment reverting it (version bumps again — history
|
|
18
|
+
* moves forward, never rewrites). */
|
|
19
|
+
readonly revertYaml: string;
|
|
20
|
+
/** Original candidate order — snapshotted into swap_events.baseline so a
|
|
21
|
+
* revert can be emitted even without the pre-swap file. */
|
|
22
|
+
readonly incumbentCandidates: readonly CandidateSpec[];
|
|
23
|
+
}
|
|
24
|
+
export declare function buildSwapChange(bundle: RouteBundle, winningVariant: string): SwapChange;
|
|
25
|
+
//# sourceMappingURL=emit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/savings/emit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAE/E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;yCACqC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;+DAC2D;IAC3D,QAAQ,CAAC,mBAAmB,EAAE,SAAS,aAAa,EAAE,CAAC;CACxD;AAiCD,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,UAAU,CA6CvF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Swap-change emission — Phase 4 WS4 (plan §4/§7). The executor NEVER edits
|
|
4
|
+
* route files: it renders the exact YAML the human applies via git, plus the
|
|
5
|
+
* revert block. This file deliberately imports no filesystem API — the
|
|
6
|
+
* "actions live in your code" invariant is structural, and emit.test.ts
|
|
7
|
+
* greps for it.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.buildSwapChange = buildSwapChange;
|
|
11
|
+
function candidateLines(candidates) {
|
|
12
|
+
return candidates.flatMap((c) => [` - provider: ${c.provider}`, ` model: ${c.model}`]);
|
|
13
|
+
}
|
|
14
|
+
function overrideLines(variant) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (variant.json !== undefined)
|
|
17
|
+
lines.push(`json: ${variant.json}`);
|
|
18
|
+
if (variant.repair !== undefined) {
|
|
19
|
+
lines.push("repair:");
|
|
20
|
+
lines.push(` max_repairs: ${variant.repair.maxRepairs}`);
|
|
21
|
+
if (variant.repair.repairModel !== undefined) {
|
|
22
|
+
lines.push(` repair_model: ${variant.repair.repairModel.provider}/${variant.repair.repairModel.model}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
27
|
+
/** Winner-first candidate order: the variant's candidates, then every other
|
|
28
|
+
* declared candidate in original order. Nothing is deleted — fallbacks and
|
|
29
|
+
* bake-off challengers keep their standing. */
|
|
30
|
+
function promotedOrder(declared, winners) {
|
|
31
|
+
const key = (c) => `${c.provider}/${c.model}`;
|
|
32
|
+
const winnerKeys = new Set(winners.map(key));
|
|
33
|
+
return [...winners, ...declared.filter((c) => !winnerKeys.has(key(c)))];
|
|
34
|
+
}
|
|
35
|
+
function buildSwapChange(bundle, winningVariant) {
|
|
36
|
+
const variant = bundle.variants.find((v) => v.name === winningVariant);
|
|
37
|
+
if (variant === undefined) {
|
|
38
|
+
throw new Error(`route "${bundle.route}" has no variant "${winningVariant}" — cannot emit a swap for it`);
|
|
39
|
+
}
|
|
40
|
+
const winners = variant.candidates ?? bundle.candidates;
|
|
41
|
+
if (winners.length === 0) {
|
|
42
|
+
throw new Error(`variant "${winningVariant}" has no candidates to promote`);
|
|
43
|
+
}
|
|
44
|
+
const incumbent = bundle.candidates[0];
|
|
45
|
+
const promoted = promotedOrder(bundle.candidates, winners);
|
|
46
|
+
const file = `modelrig/routes/${bundle.route}.yaml`;
|
|
47
|
+
const newVersion = bundle.version + 1;
|
|
48
|
+
const yaml = [
|
|
49
|
+
`# swap: serve ${bundle.route} on ${winners[0].provider}/${winners[0].model}`,
|
|
50
|
+
`# apply to ${file} — replace the version and candidates fields; nothing else changes`,
|
|
51
|
+
`version: ${newVersion}`,
|
|
52
|
+
"candidates:",
|
|
53
|
+
...candidateLines(promoted),
|
|
54
|
+
...overrideLines(variant),
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
const revertYaml = [
|
|
58
|
+
`# revert: restore ${bundle.route} to ${incumbent.provider}/${incumbent.model}`,
|
|
59
|
+
`# apply to ${file} — replace the version and candidates fields; nothing else changes`,
|
|
60
|
+
`version: ${newVersion + 1}`,
|
|
61
|
+
"candidates:",
|
|
62
|
+
...candidateLines(bundle.candidates),
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
return {
|
|
66
|
+
route: bundle.route,
|
|
67
|
+
file,
|
|
68
|
+
fromModel: `${incumbent.provider}/${incumbent.model}`,
|
|
69
|
+
toModel: `${winners[0].provider}/${winners[0].model}`,
|
|
70
|
+
newVersion,
|
|
71
|
+
yaml,
|
|
72
|
+
revertYaml,
|
|
73
|
+
incumbentCandidates: bundle.candidates,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The savings odometer — Phase 4 WS4. Verified savings per executed swap:
|
|
3
|
+
*
|
|
4
|
+
* (baseline effective $/conformant − actual $/conformant since swap)
|
|
5
|
+
* × conformant volume since swap
|
|
6
|
+
*
|
|
7
|
+
* Rules (the odometer's credibility IS the brand — plan §10):
|
|
8
|
+
* - MEASURED only: computed from telemetry rows that actually flowed.
|
|
9
|
+
* Zero volume → $0, never a projection.
|
|
10
|
+
* - Honest costs: failed-attempt spend counts against the actual side;
|
|
11
|
+
* unpriced rows are excluded entirely (their $0 would inflate savings).
|
|
12
|
+
* - Negative results stay negative — a swap that made things worse shows it.
|
|
13
|
+
* - Reverted swaps stop accruing at reverted_at.
|
|
14
|
+
*/
|
|
15
|
+
import type { TelemetryRow } from "../types/telemetry";
|
|
16
|
+
import type { SwapEventRecord } from "./swaps-client";
|
|
17
|
+
export interface SwapSavings {
|
|
18
|
+
readonly swapId: string;
|
|
19
|
+
readonly route: string;
|
|
20
|
+
readonly toVariant: string;
|
|
21
|
+
readonly executedAt: string;
|
|
22
|
+
readonly conformantVolume: number;
|
|
23
|
+
readonly actualUsdPerConformant: number | null;
|
|
24
|
+
readonly baselineUsdPerConformant: number;
|
|
25
|
+
readonly verifiedUsd: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Odometer {
|
|
28
|
+
readonly totalVerifiedUsd: number;
|
|
29
|
+
readonly perSwap: readonly SwapSavings[];
|
|
30
|
+
}
|
|
31
|
+
export declare function computeOdometer(swaps: readonly SwapEventRecord[], rows: readonly TelemetryRow[]): Odometer;
|
|
32
|
+
//# sourceMappingURL=odometer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"odometer.d.ts","sourceRoot":"","sources":["../../src/savings/odometer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,IAAI,EAAE,SAAS,YAAY,EAAE,GAC5B,QAAQ,CAoDV"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The savings odometer — Phase 4 WS4. Verified savings per executed swap:
|
|
4
|
+
*
|
|
5
|
+
* (baseline effective $/conformant − actual $/conformant since swap)
|
|
6
|
+
* × conformant volume since swap
|
|
7
|
+
*
|
|
8
|
+
* Rules (the odometer's credibility IS the brand — plan §10):
|
|
9
|
+
* - MEASURED only: computed from telemetry rows that actually flowed.
|
|
10
|
+
* Zero volume → $0, never a projection.
|
|
11
|
+
* - Honest costs: failed-attempt spend counts against the actual side;
|
|
12
|
+
* unpriced rows are excluded entirely (their $0 would inflate savings).
|
|
13
|
+
* - Negative results stay negative — a swap that made things worse shows it.
|
|
14
|
+
* - Reverted swaps stop accruing at reverted_at.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.computeOdometer = computeOdometer;
|
|
18
|
+
function computeOdometer(swaps, rows) {
|
|
19
|
+
const perSwap = swaps.map((swap) => {
|
|
20
|
+
const from = Date.parse(swap.executedAt);
|
|
21
|
+
const until = swap.revertedAt !== null ? Date.parse(swap.revertedAt) : Number.POSITIVE_INFINITY;
|
|
22
|
+
const window = rows.filter((r) => {
|
|
23
|
+
if (r.route !== swap.route || r.pricingMissing)
|
|
24
|
+
return false;
|
|
25
|
+
// Replay/shadow traffic is not production serving — never let a
|
|
26
|
+
// bake-off run on the swapped route inflate measured savings.
|
|
27
|
+
try {
|
|
28
|
+
if (JSON.parse(r.tagsJson).replay !== undefined)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Unparseable tags: treat as production (the conservative default
|
|
33
|
+
// here is to keep the row; a malformed tag never hides real spend).
|
|
34
|
+
}
|
|
35
|
+
const t = Date.parse(r.ts);
|
|
36
|
+
return t >= from && t < until;
|
|
37
|
+
});
|
|
38
|
+
const conformant = window.filter((r) => r.failureClass === null).length;
|
|
39
|
+
const spentUsd = window.reduce((a, r) => a + r.costEstimateUsd, 0);
|
|
40
|
+
const baselinePerConformant = swap.baseline.effective_usd_per_1k_conformant / 1000;
|
|
41
|
+
if (conformant === 0) {
|
|
42
|
+
// No conformant volume → nothing verified. $0, not a projection.
|
|
43
|
+
return {
|
|
44
|
+
swapId: swap.id,
|
|
45
|
+
route: swap.route,
|
|
46
|
+
toVariant: swap.toVariant,
|
|
47
|
+
executedAt: swap.executedAt,
|
|
48
|
+
conformantVolume: 0,
|
|
49
|
+
actualUsdPerConformant: null,
|
|
50
|
+
baselineUsdPerConformant: baselinePerConformant,
|
|
51
|
+
verifiedUsd: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const actualPerConformant = spentUsd / conformant;
|
|
55
|
+
return {
|
|
56
|
+
swapId: swap.id,
|
|
57
|
+
route: swap.route,
|
|
58
|
+
toVariant: swap.toVariant,
|
|
59
|
+
executedAt: swap.executedAt,
|
|
60
|
+
conformantVolume: conformant,
|
|
61
|
+
actualUsdPerConformant: actualPerConformant,
|
|
62
|
+
baselineUsdPerConformant: baselinePerConformant,
|
|
63
|
+
verifiedUsd: (baselinePerConformant - actualPerConformant) * conformant,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
totalVerifiedUsd: perSwap.reduce((a, s) => a + s.verifiedUsd, 0),
|
|
68
|
+
perSwap,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swap execution orchestration — Phase 4 WS4, extracted from the CLI so the
|
|
3
|
+
* safety-critical path (the one that must NEVER auto-execute and must record
|
|
4
|
+
* exactly once) is hermetically testable with injected clients. The CLI is
|
|
5
|
+
* thin wiring over this; time, ids, and the approver are injected.
|
|
6
|
+
*
|
|
7
|
+
* Invariants enforced here (each has a test):
|
|
8
|
+
* - `approve` MUST be true — no --approve, no execution, no side effects.
|
|
9
|
+
* - Only an evidenced/approved proposal with a WINNING variant executes.
|
|
10
|
+
* - The incumbent baseline is snapshotted into swap_events (odometer seed),
|
|
11
|
+
* and it MUST be an explicit `default` evidence row — never a fallback.
|
|
12
|
+
* - Control-plane writes happen BEFORE the YAML is emitted (a process death
|
|
13
|
+
* after apply must not leave an unrecorded swap): approved → record →
|
|
14
|
+
* executed. Applying the YAML stays a human/git action.
|
|
15
|
+
*/
|
|
16
|
+
import type { SwapChange } from "./emit";
|
|
17
|
+
import type { SwapEventRecord } from "./swaps-client";
|
|
18
|
+
import type { ProposalRecord, ProposalStatus } from "../watch/proposals";
|
|
19
|
+
import type { RouteBundle } from "../types/route";
|
|
20
|
+
export interface SwapExecuteDeps {
|
|
21
|
+
readonly proposalId: string;
|
|
22
|
+
/** THE gate — false throws before any read or write. */
|
|
23
|
+
readonly approve: boolean;
|
|
24
|
+
readonly approvedBy: string;
|
|
25
|
+
readonly proposals: {
|
|
26
|
+
get(id: string): Promise<ProposalRecord | null>;
|
|
27
|
+
setStatus(id: string, from: ProposalStatus, to: ProposalStatus): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
readonly swaps: {
|
|
30
|
+
record(event: SwapEventRecord): Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
readonly loadBundle: (route: string) => RouteBundle;
|
|
33
|
+
readonly newId: () => string;
|
|
34
|
+
readonly now: () => string;
|
|
35
|
+
}
|
|
36
|
+
export interface SwapExecuteResult {
|
|
37
|
+
readonly swapId: string;
|
|
38
|
+
readonly route: string;
|
|
39
|
+
readonly change: SwapChange;
|
|
40
|
+
readonly approvedBy: string;
|
|
41
|
+
readonly executedAt: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function executeSwap(deps: SwapExecuteDeps): Promise<SwapExecuteResult>;
|
|
44
|
+
//# sourceMappingURL=swap-execute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swap-execute.d.ts","sourceRoot":"","sources":["../../src/savings/swap-execute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AASlD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE;QAClB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;QAChD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAChF,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE;QAAE,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAClE,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,WAAW,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuFnF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Swap execution orchestration — Phase 4 WS4, extracted from the CLI so the
|
|
4
|
+
* safety-critical path (the one that must NEVER auto-execute and must record
|
|
5
|
+
* exactly once) is hermetically testable with injected clients. The CLI is
|
|
6
|
+
* thin wiring over this; time, ids, and the approver are injected.
|
|
7
|
+
*
|
|
8
|
+
* Invariants enforced here (each has a test):
|
|
9
|
+
* - `approve` MUST be true — no --approve, no execution, no side effects.
|
|
10
|
+
* - Only an evidenced/approved proposal with a WINNING variant executes.
|
|
11
|
+
* - The incumbent baseline is snapshotted into swap_events (odometer seed),
|
|
12
|
+
* and it MUST be an explicit `default` evidence row — never a fallback.
|
|
13
|
+
* - Control-plane writes happen BEFORE the YAML is emitted (a process death
|
|
14
|
+
* after apply must not leave an unrecorded swap): approved → record →
|
|
15
|
+
* executed. Applying the YAML stays a human/git action.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.executeSwap = executeSwap;
|
|
19
|
+
const emit_1 = require("./emit");
|
|
20
|
+
async function executeSwap(deps) {
|
|
21
|
+
// The gate first — before any client call, so a non-approved invocation
|
|
22
|
+
// has zero side effects (defense in depth with the CLI's fast-fail check).
|
|
23
|
+
if (!deps.approve) {
|
|
24
|
+
throw new Error("swap execution requires explicit --approve — proposals never auto-execute. " +
|
|
25
|
+
`Re-run: modelrig swap execute ${deps.proposalId} --approve`);
|
|
26
|
+
}
|
|
27
|
+
const proposal = await deps.proposals.get(deps.proposalId);
|
|
28
|
+
if (proposal === null)
|
|
29
|
+
throw new Error(`no proposal "${deps.proposalId}" on the control plane`);
|
|
30
|
+
if (proposal.status !== "evidenced" && proposal.status !== "approved") {
|
|
31
|
+
throw new Error(`proposal ${deps.proposalId} is ${proposal.status} — only evidenced proposals can be approved ` +
|
|
32
|
+
"(run `modelrig bakeoff --from-proposal` first; approval without evidence is not a thing)");
|
|
33
|
+
}
|
|
34
|
+
const winner = proposal.evidence?.proposal;
|
|
35
|
+
if (winner === null || winner === undefined) {
|
|
36
|
+
throw new Error(`proposal ${deps.proposalId} has no winning variant in its evidence — the incumbent stood; ` +
|
|
37
|
+
"nothing to execute");
|
|
38
|
+
}
|
|
39
|
+
// The odometer baseline MUST come from the incumbent — the `default` variant
|
|
40
|
+
// — and nothing else. There is no "close enough" row: seeding from a
|
|
41
|
+
// challenger would report savings against another challenger and quietly
|
|
42
|
+
// inflate every number the odometer ever shows. If in doubt, refuse.
|
|
43
|
+
const matrix = proposal.evidence?.matrix;
|
|
44
|
+
const incumbentRow = matrix?.find((m) => m.variant === "default");
|
|
45
|
+
if (incumbentRow === undefined) {
|
|
46
|
+
const seen = (matrix ?? []).map((m) => m.variant).join(", ") || "none";
|
|
47
|
+
throw new Error(`proposal ${deps.proposalId} evidence has no incumbent (variant "default") row — ` +
|
|
48
|
+
`cannot seed the odometer from a challenger. Variants present: ${seen}. ` +
|
|
49
|
+
"Re-run the bake-off so the incumbent is measured alongside the challengers " +
|
|
50
|
+
`(modelrig bakeoff --from-proposal ${deps.proposalId}).`);
|
|
51
|
+
}
|
|
52
|
+
if (incumbentRow.effective_usd_per_1k_conformant === null) {
|
|
53
|
+
throw new Error(`proposal ${deps.proposalId} evidence has no usable incumbent baseline — cannot seed the odometer`);
|
|
54
|
+
}
|
|
55
|
+
const bundle = deps.loadBundle(proposal.route);
|
|
56
|
+
const change = (0, emit_1.buildSwapChange)(bundle, winner.variant);
|
|
57
|
+
const baseline = {
|
|
58
|
+
effective_usd_per_1k_conformant: incumbentRow.effective_usd_per_1k_conformant,
|
|
59
|
+
conform_rate: incumbentRow.conform_rate,
|
|
60
|
+
samples: incumbentRow.samples,
|
|
61
|
+
bakeoff_id: String(proposal.evidence?.bakeoff_id ?? ""),
|
|
62
|
+
incumbent_candidates: change.incumbentCandidates.map((c) => ({
|
|
63
|
+
provider: c.provider,
|
|
64
|
+
model: c.model,
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
const swapId = deps.newId();
|
|
68
|
+
const executedAt = deps.now();
|
|
69
|
+
// Record on the control plane FIRST — the emitted YAML is worthless as
|
|
70
|
+
// history if the process dies after the operator applies it.
|
|
71
|
+
if (proposal.status === "evidenced") {
|
|
72
|
+
await deps.proposals.setStatus(proposal.id, "evidenced", "approved");
|
|
73
|
+
}
|
|
74
|
+
await deps.swaps.record({
|
|
75
|
+
id: swapId,
|
|
76
|
+
proposalId: proposal.id,
|
|
77
|
+
route: proposal.route,
|
|
78
|
+
fromVariant: "default",
|
|
79
|
+
toVariant: winner.variant,
|
|
80
|
+
baseline,
|
|
81
|
+
executedAt,
|
|
82
|
+
approvedBy: deps.approvedBy,
|
|
83
|
+
revertedAt: null,
|
|
84
|
+
});
|
|
85
|
+
await deps.proposals.setStatus(proposal.id, "approved", "executed");
|
|
86
|
+
return { swapId, route: proposal.route, change, approvedBy: deps.approvedBy, executedAt };
|
|
87
|
+
}
|