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 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AAgBH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,iBAAiB,CAAC;AAcvE,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACpC;AAaD,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAuBxD;AAMD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CA6B3D;AAqfD,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCnE"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* modelrig CLI — Phase 3 plan §3 + Phase 4 plan §3:
|
|
5
|
+
* modelrig bakeoff --route X --variants a,b,c --replay-last N
|
|
6
|
+
* [--envelope usd] [--json] [--no-mirror]
|
|
7
|
+
* modelrig bakeoff --from-proposal <id> [--replay-last N] [--json]
|
|
8
|
+
* modelrig watch run [--events fixtures.json] [--json]
|
|
9
|
+
*
|
|
10
|
+
* Bake-offs replay locally captured traffic, write the artifact JSON next
|
|
11
|
+
* to the telemetry db (.modelrig/bakeoffs/<id>.json), upsert the `bakeoffs`
|
|
12
|
+
* mirror when the Supabase sink is configured, and print the matrix.
|
|
13
|
+
* Captures never travel — only aggregate metrics. Watch cycles spend $0;
|
|
14
|
+
* --from-proposal runs draw from the monthly watch envelope and attach
|
|
15
|
+
* their evidence to the proposal (suggested → evidenced). Nothing here
|
|
16
|
+
* executes a swap — that is `modelrig swap execute --approve`, a separate,
|
|
17
|
+
* human command.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.parseArgs = parseArgs;
|
|
21
|
+
exports.printMatrix = printMatrix;
|
|
22
|
+
exports.main = main;
|
|
23
|
+
const node_crypto_1 = require("node:crypto");
|
|
24
|
+
const node_fs_1 = require("node:fs");
|
|
25
|
+
const node_os_1 = require("node:os");
|
|
26
|
+
const node_path_1 = require("node:path");
|
|
27
|
+
const run_1 = require("./init/run");
|
|
28
|
+
const failures_1 = require("./types/failures");
|
|
29
|
+
const failures_teaching_1 = require("./types/failures-teaching");
|
|
30
|
+
const rows_1 = require("./registry/rows");
|
|
31
|
+
const config_1 = require("./config");
|
|
32
|
+
const index_1 = require("./index");
|
|
33
|
+
const engine_1 = require("./bakeoff/engine");
|
|
34
|
+
const samples_1 = require("./bakeoff/samples");
|
|
35
|
+
const loader_1 = require("./routes/loader");
|
|
36
|
+
const store_1 = require("./telemetry/store");
|
|
37
|
+
const budget_1 = require("./watch/budget");
|
|
38
|
+
const proposals_1 = require("./watch/proposals");
|
|
39
|
+
const run_reported_1 = require("./watch/run-reported");
|
|
40
|
+
const state_1 = require("./watch/state");
|
|
41
|
+
const state_store_1 = require("./watch/state-store");
|
|
42
|
+
const swaps_client_1 = require("./savings/swaps-client");
|
|
43
|
+
const swap_execute_1 = require("./savings/swap-execute");
|
|
44
|
+
const BARE_FLAGS = new Set([
|
|
45
|
+
"json",
|
|
46
|
+
"no-mirror",
|
|
47
|
+
"approve",
|
|
48
|
+
"keep-outputs",
|
|
49
|
+
"keep-outputs=all",
|
|
50
|
+
// modelrig init (Phase 5 WS2)
|
|
51
|
+
"dry-run",
|
|
52
|
+
"include-tests",
|
|
53
|
+
]);
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const positional = [];
|
|
56
|
+
const named = new Map();
|
|
57
|
+
const bare = new Set();
|
|
58
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
59
|
+
const arg = argv[i];
|
|
60
|
+
if (arg.startsWith("--")) {
|
|
61
|
+
const name = arg.slice(2);
|
|
62
|
+
if (BARE_FLAGS.has(name)) {
|
|
63
|
+
bare.add(name);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const value = argv[i + 1];
|
|
67
|
+
if (value === undefined || value.startsWith("--")) {
|
|
68
|
+
throw new Error(`flag ${arg} needs a value`);
|
|
69
|
+
}
|
|
70
|
+
named.set(name, value);
|
|
71
|
+
i += 1;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
positional.push(arg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { positional, named, bare };
|
|
78
|
+
}
|
|
79
|
+
function fmtEff(usd) {
|
|
80
|
+
return Number.isFinite(usd) ? `$${usd.toFixed(4)}` : "∞ (0 conformant)";
|
|
81
|
+
}
|
|
82
|
+
function printMatrix(artifact) {
|
|
83
|
+
console.log(`bakeoff ${artifact.id}\nroute ${artifact.route}@${artifact.routeVersion} · replayed ${artifact.replayN} captured inputs · ${artifact.ranAt}`);
|
|
84
|
+
const header = ["variant", "n", "conform", "ci95", "repair", "repair$", "lat(ms)", "eff-$/1K-conf"];
|
|
85
|
+
const rows = artifact.variants.map((m) => [
|
|
86
|
+
m.variant,
|
|
87
|
+
String(m.samples),
|
|
88
|
+
`${(m.conformRate * 100).toFixed(0)}%`,
|
|
89
|
+
`[${(m.conformCi95[0] * 100).toFixed(0)},${(m.conformCi95[1] * 100).toFixed(0)}]`,
|
|
90
|
+
`${(m.repairRate * 100).toFixed(0)}%`,
|
|
91
|
+
`$${m.meanRepairCostUsd.toFixed(4)}`,
|
|
92
|
+
m.meanLatencyMs.toFixed(0),
|
|
93
|
+
m.pricingMissing ? "UNPRICED (no ranking)" : fmtEff(m.effectiveUsdPer1kConformant),
|
|
94
|
+
]);
|
|
95
|
+
const widths = header.map((h, c) => Math.max(h.length, ...rows.map((r) => r[c].length)));
|
|
96
|
+
const line = (cells) => cells.map((cell, c) => cell.padEnd(widths[c])).join(" ");
|
|
97
|
+
console.log(line(header));
|
|
98
|
+
for (const row of rows)
|
|
99
|
+
console.log(line(row));
|
|
100
|
+
if (artifact.proposal !== null) {
|
|
101
|
+
console.log(`PROPOSAL: variant "${artifact.proposal.variant}" beats the incumbent by ` +
|
|
102
|
+
`${artifact.proposal.vsIncumbentSavingsPct.toFixed(1)}% effective cost — human decision required; ` +
|
|
103
|
+
"nothing is executed.");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log("proposal: none (no variant beats the incumbent's gates)");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Control-plane client from config.export — the watch/proposal surfaces
|
|
110
|
+
* need it; a clear error beats a silent no-op. */
|
|
111
|
+
function proposalsClientFrom(config) {
|
|
112
|
+
const exp = (0, config_1.directExportOf)(config, "this command");
|
|
113
|
+
return (0, proposals_1.createProposalsClient)({
|
|
114
|
+
supabaseUrl: exp.supabaseUrl,
|
|
115
|
+
serviceRoleKey: exp.serviceRoleKey,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** A3: --keep-outputs (winner) | --keep-outputs=all | off. */
|
|
119
|
+
function keepOutputsMode(flags) {
|
|
120
|
+
if (flags.bare.has("keep-outputs=all"))
|
|
121
|
+
return "all";
|
|
122
|
+
if (flags.bare.has("keep-outputs"))
|
|
123
|
+
return "winner";
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
async function runAndReportBakeoff(config, flags, route, variants, replayLastN, envelope) {
|
|
127
|
+
const keep = keepOutputsMode(flags);
|
|
128
|
+
if (keep !== undefined)
|
|
129
|
+
(0, config_1.directExportOf)(config, "--keep-outputs (bakeoff_samples lives there)");
|
|
130
|
+
const mirrorTarget = config.export?.mode === "direct" ? config.export : undefined;
|
|
131
|
+
const rig = (0, index_1.createRig)(config);
|
|
132
|
+
try {
|
|
133
|
+
const artifact = await rig.bakeoff(route, {
|
|
134
|
+
variants,
|
|
135
|
+
replayLastN,
|
|
136
|
+
...(envelope !== undefined ? { envelopeUsdPerVariant: envelope } : {}),
|
|
137
|
+
...(keep !== undefined ? { keepOutputs: keep } : {}),
|
|
138
|
+
});
|
|
139
|
+
const outDir = (0, node_path_1.join)((0, node_path_1.dirname)(config.telemetryDbPath), "bakeoffs");
|
|
140
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
141
|
+
const artifactPath = (0, node_path_1.join)(outDir, `${artifact.id}.json`);
|
|
142
|
+
(0, node_fs_1.writeFileSync)(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
143
|
+
let mirrored = false;
|
|
144
|
+
if (mirrorTarget !== undefined && !flags.bare.has("no-mirror")) {
|
|
145
|
+
await (0, engine_1.mirrorBakeoff)(artifact, {
|
|
146
|
+
supabaseUrl: mirrorTarget.supabaseUrl,
|
|
147
|
+
serviceRoleKey: mirrorTarget.serviceRoleKey,
|
|
148
|
+
sourceId: mirrorTarget.sourceId ?? (0, node_os_1.hostname)(),
|
|
149
|
+
});
|
|
150
|
+
mirrored = true;
|
|
151
|
+
}
|
|
152
|
+
if (flags.bare.has("json")) {
|
|
153
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
printMatrix(artifact);
|
|
157
|
+
}
|
|
158
|
+
console.log(`artifact → ${artifactPath}${mirrored ? " · mirrored to bakeoffs" : ""}` +
|
|
159
|
+
(keep !== undefined
|
|
160
|
+
? ` · review samples kept (${keep}) → console /samples?bakeoff=${artifact.id}`
|
|
161
|
+
: ""));
|
|
162
|
+
return artifact;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
rig.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function cmdBakeoff(flags) {
|
|
169
|
+
const config = (0, config_1.loadConfigFromEnv)();
|
|
170
|
+
const proposalId = flags.named.get("from-proposal");
|
|
171
|
+
if (proposalId !== undefined) {
|
|
172
|
+
// Evidence run for a watch proposal — budget-enveloped by the monthly
|
|
173
|
+
// watch cap, evidence attached on completion (suggested → evidenced).
|
|
174
|
+
const client = proposalsClientFrom(config);
|
|
175
|
+
const proposal = await client.get(proposalId);
|
|
176
|
+
if (proposal === null)
|
|
177
|
+
throw new Error(`no proposal "${proposalId}" on the control plane`);
|
|
178
|
+
if (proposal.status !== "suggested" && proposal.status !== "evidenced") {
|
|
179
|
+
throw new Error(`proposal ${proposalId} is ${proposal.status} — evidence runs apply to ` +
|
|
180
|
+
"suggested/evidenced proposals only");
|
|
181
|
+
}
|
|
182
|
+
const routes = (0, loader_1.loadRoutes)(config.routesDir);
|
|
183
|
+
const bundle = routes.get(proposal.route);
|
|
184
|
+
const variants = ["default", ...bundle.variants.map((v) => v.name)];
|
|
185
|
+
const lastRaw = flags.named.get("replay-last");
|
|
186
|
+
const replayLastN = lastRaw !== undefined ? Number(lastRaw) : 25;
|
|
187
|
+
if (!Number.isInteger(replayLastN) || replayLastN < 1) {
|
|
188
|
+
throw new Error("--replay-last must be a positive integer");
|
|
189
|
+
}
|
|
190
|
+
// Monthly watch budget: reserve headroom, split it across variants,
|
|
191
|
+
// accrue what the run actually spent afterwards.
|
|
192
|
+
const budgetStore = (0, store_1.openTelemetryStore)(config.telemetryDbPath);
|
|
193
|
+
try {
|
|
194
|
+
const monthlyUsd = config.watch?.monthlyUsd ?? budget_1.DEFAULT_WATCH_MONTHLY_USD;
|
|
195
|
+
const reservation = (0, budget_1.reserveWatchBudget)(budgetStore, monthlyUsd, new Date());
|
|
196
|
+
const perVariant = reservation.remainingUsd / variants.length;
|
|
197
|
+
console.log(`watch budget ${reservation.key}: $${reservation.remainingUsd.toFixed(2)} remaining → ` +
|
|
198
|
+
`$${perVariant.toFixed(2)}/variant cap`);
|
|
199
|
+
const artifact = await runAndReportBakeoff(config, flags, proposal.route, variants, replayLastN, perVariant);
|
|
200
|
+
const spentUsd = artifact.variants.reduce((a, m) => a + m.samples * m.meanCostUsd, 0);
|
|
201
|
+
budgetStore.accrue(reservation.key, spentUsd);
|
|
202
|
+
await client.attachEvidence(proposal.id, {
|
|
203
|
+
bakeoff_id: artifact.id,
|
|
204
|
+
replay_n: artifact.replayN,
|
|
205
|
+
ran_at: artifact.ranAt,
|
|
206
|
+
spent_usd: spentUsd,
|
|
207
|
+
proposal: artifact.proposal,
|
|
208
|
+
matrix: artifact.variants.map((m) => ({
|
|
209
|
+
variant: m.variant,
|
|
210
|
+
samples: m.samples,
|
|
211
|
+
conform_rate: m.conformRate,
|
|
212
|
+
conform_ci95: m.conformCi95,
|
|
213
|
+
effective_usd_per_1k_conformant: Number.isFinite(m.effectiveUsdPer1kConformant)
|
|
214
|
+
? m.effectiveUsdPer1kConformant
|
|
215
|
+
: null,
|
|
216
|
+
pricing_missing: m.pricingMissing,
|
|
217
|
+
})),
|
|
218
|
+
});
|
|
219
|
+
console.log(`evidence attached → proposal ${proposal.id} is now evidenced ` +
|
|
220
|
+
`($${spentUsd.toFixed(2)} accrued to ${reservation.key}). ` +
|
|
221
|
+
"Approval stays human: modelrig swap execute <proposalId> --approve");
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
budgetStore.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const route = flags.named.get("route");
|
|
229
|
+
const variantsRaw = flags.named.get("variants");
|
|
230
|
+
const lastRaw = flags.named.get("replay-last");
|
|
231
|
+
if (route === undefined || variantsRaw === undefined || lastRaw === undefined) {
|
|
232
|
+
throw new Error("bakeoff requires --route, --variants a,b,c and --replay-last N (or --from-proposal <id>)");
|
|
233
|
+
}
|
|
234
|
+
const variants = variantsRaw.split(",").map((v) => v.trim()).filter((v) => v !== "");
|
|
235
|
+
const replayLastN = Number(lastRaw);
|
|
236
|
+
if (!Number.isInteger(replayLastN) || replayLastN < 1) {
|
|
237
|
+
throw new Error("--replay-last must be a positive integer");
|
|
238
|
+
}
|
|
239
|
+
const envelopeRaw = flags.named.get("envelope");
|
|
240
|
+
const envelope = envelopeRaw !== undefined ? Number(envelopeRaw) : undefined;
|
|
241
|
+
if (envelope !== undefined && (!Number.isFinite(envelope) || envelope <= 0)) {
|
|
242
|
+
throw new Error("--envelope must be positive");
|
|
243
|
+
}
|
|
244
|
+
await runAndReportBakeoff(config, flags, route, variants, replayLastN, envelope);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
const DEFAULT_WATCH_THRESHOLD_PCT = 10;
|
|
248
|
+
async function cmdWatch(flags) {
|
|
249
|
+
const sub = flags.positional[0];
|
|
250
|
+
if (sub !== "run") {
|
|
251
|
+
throw new Error("usage: modelrig watch run [--events fixtures.json] [--json]");
|
|
252
|
+
}
|
|
253
|
+
const config = (0, config_1.loadConfigFromEnv)();
|
|
254
|
+
const client = proposalsClientFrom(config);
|
|
255
|
+
const routes = (0, loader_1.loadRoutes)(config.routesDir);
|
|
256
|
+
const routeEconomics = routes.list().map((b) => ({
|
|
257
|
+
route: b.route,
|
|
258
|
+
candidates: b.candidates.map((c) => ({ provider: c.provider, model: c.model })),
|
|
259
|
+
}));
|
|
260
|
+
const providers = new Set(routeEconomics.flatMap((r) => r.candidates.map((c) => c.provider)));
|
|
261
|
+
const eventsPath = flags.named.get("events");
|
|
262
|
+
const injectedEvents = eventsPath !== undefined
|
|
263
|
+
? JSON.parse((0, node_fs_1.readFileSync)(eventsPath, "utf8"))
|
|
264
|
+
: undefined;
|
|
265
|
+
// A4: on a cron container the pricing snapshot MUST outlive the process.
|
|
266
|
+
// A file-backed store there re-seeds every run and can never fire an event.
|
|
267
|
+
// Durable watch state needs the database itself, so api mode gets the local
|
|
268
|
+
// file (and the warning below) exactly as an unconfigured rig does.
|
|
269
|
+
const watchExport = config.export?.mode === "direct" ? config.export : undefined;
|
|
270
|
+
const store = (0, state_store_1.createWatchStateStore)({
|
|
271
|
+
filePath: (0, node_path_1.join)((0, node_path_1.dirname)(config.telemetryDbPath), "watch-state.json"),
|
|
272
|
+
export: watchExport,
|
|
273
|
+
});
|
|
274
|
+
if (watchExport === undefined) {
|
|
275
|
+
console.warn("[modelrig] watch state is local-file only — fine on your machine, but on an ephemeral " +
|
|
276
|
+
"container every cycle re-seeds and no price change can ever fire. Configure " +
|
|
277
|
+
"MODELRIG_SUPABASE_URL + MODELRIG_SUPABASE_SERVICE_ROLE_KEY for durable state.");
|
|
278
|
+
}
|
|
279
|
+
// The cycle reports its own outcome to the control plane. A4 gave this job
|
|
280
|
+
// durable state but no proof of life: `control_state` sat empty for a week
|
|
281
|
+
// and "never scheduled", "running and failing" and "running and finding
|
|
282
|
+
// nothing" were indistinguishable from outside, readable only through the
|
|
283
|
+
// hosting platform's log viewer. Now the job answers for itself.
|
|
284
|
+
const result = await (0, run_reported_1.runWatchCycleReported)({
|
|
285
|
+
routes: routeEconomics,
|
|
286
|
+
store,
|
|
287
|
+
universe: (0, state_1.buildPriceUniverse)(providers),
|
|
288
|
+
client,
|
|
289
|
+
thresholdPct: config.watch?.thresholdPct ?? DEFAULT_WATCH_THRESHOLD_PCT,
|
|
290
|
+
...(injectedEvents !== undefined ? { injectedEvents } : {}),
|
|
291
|
+
}, {
|
|
292
|
+
supabaseUrl: watchExport?.supabaseUrl ?? "",
|
|
293
|
+
serviceRoleKey: watchExport?.serviceRoleKey ?? "",
|
|
294
|
+
});
|
|
295
|
+
if (flags.bare.has("json")) {
|
|
296
|
+
console.log(JSON.stringify(result, null, 2));
|
|
297
|
+
}
|
|
298
|
+
else if (result.seeded) {
|
|
299
|
+
console.log("watch: seeded pricing state (first run watches silently — events fire from the next cycle)\n" +
|
|
300
|
+
` state → ${store.description}`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log(`watch: ${result.events} event(s) → ${result.drafts} draft(s) → ` +
|
|
304
|
+
`${result.inserted} new proposal(s) (duplicates deduped; run \`modelrig bakeoff ` +
|
|
305
|
+
"--from-proposal <id>` to attach evidence — nothing executes automatically)");
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
function swapsClientFrom(config) {
|
|
310
|
+
const exp = (0, config_1.directExportOf)(config, "this command");
|
|
311
|
+
return (0, swaps_client_1.createSwapsClient)({
|
|
312
|
+
supabaseUrl: exp.supabaseUrl,
|
|
313
|
+
serviceRoleKey: exp.serviceRoleKey,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function cmdSwapExecute(flags) {
|
|
317
|
+
const proposalId = flags.positional[1];
|
|
318
|
+
if (proposalId === undefined) {
|
|
319
|
+
throw new Error("usage: modelrig swap execute <proposalId> --approve [--approved-by name]");
|
|
320
|
+
}
|
|
321
|
+
// THE gate (spec hard rule): no --approve, no execution — full stop,
|
|
322
|
+
// before any config or network access.
|
|
323
|
+
if (!flags.bare.has("approve")) {
|
|
324
|
+
throw new Error("swap execution requires explicit --approve — proposals never auto-execute. " +
|
|
325
|
+
`Re-run: modelrig swap execute ${proposalId} --approve`);
|
|
326
|
+
}
|
|
327
|
+
const config = (0, config_1.loadConfigFromEnv)();
|
|
328
|
+
const proposals = proposalsClientFrom(config);
|
|
329
|
+
const swaps = swapsClientFrom(config);
|
|
330
|
+
const routes = (0, loader_1.loadRoutes)(config.routesDir);
|
|
331
|
+
// The orchestration (guards, ordering, baseline snapshot) lives in the
|
|
332
|
+
// hermetically-tested executeSwap; the CLI only injects the real config
|
|
333
|
+
// deps and prints. The --approve gate is enforced here (fast-fail before
|
|
334
|
+
// config) AND inside executeSwap (defense in depth).
|
|
335
|
+
const result = await (0, swap_execute_1.executeSwap)({
|
|
336
|
+
proposalId,
|
|
337
|
+
approve: flags.bare.has("approve"),
|
|
338
|
+
approvedBy: flags.named.get("approved-by") ?? (0, node_os_1.userInfo)().username,
|
|
339
|
+
proposals,
|
|
340
|
+
swaps,
|
|
341
|
+
loadBundle: (route) => routes.get(route),
|
|
342
|
+
newId: () => (0, node_crypto_1.randomUUID)(),
|
|
343
|
+
now: () => new Date().toISOString(),
|
|
344
|
+
});
|
|
345
|
+
console.log(`swap ${result.swapId} recorded (approved by ${result.approvedBy}) — ` +
|
|
346
|
+
`${result.change.fromModel} → ${result.change.toModel}\n`);
|
|
347
|
+
console.log("APPLY THIS CHANGE (the executor never edits route files — git does):\n");
|
|
348
|
+
console.log(result.change.yaml);
|
|
349
|
+
console.log(`REVERT COMMAND (any time): modelrig swap revert ${result.swapId}\n` +
|
|
350
|
+
`The odometer starts measuring ${result.route} savings from the moment the change deploys.`);
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
async function cmdSwapRevert(flags) {
|
|
354
|
+
const swapId = flags.positional[1];
|
|
355
|
+
if (swapId === undefined)
|
|
356
|
+
throw new Error("usage: modelrig swap revert <swapId>");
|
|
357
|
+
const config = (0, config_1.loadConfigFromEnv)();
|
|
358
|
+
const swaps = swapsClientFrom(config);
|
|
359
|
+
const swap = await swaps.get(swapId);
|
|
360
|
+
if (swap === null)
|
|
361
|
+
throw new Error(`no swap "${swapId}" on the control plane`);
|
|
362
|
+
if (swap.revertedAt !== null) {
|
|
363
|
+
console.log(`swap ${swapId} was already reverted at ${swap.revertedAt} — nothing to do`);
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
const revertedAt = new Date().toISOString();
|
|
367
|
+
await swaps.markReverted(swapId, revertedAt);
|
|
368
|
+
const incumbents = swap.baseline.incumbent_candidates ?? [];
|
|
369
|
+
console.log(`swap ${swapId} marked reverted — the odometer stops accruing it at ${revertedAt}\n`);
|
|
370
|
+
console.log("APPLY THIS CHANGE (git is the actuator, as always):\n");
|
|
371
|
+
const routes = (0, loader_1.loadRoutes)(config.routesDir);
|
|
372
|
+
let version = null;
|
|
373
|
+
try {
|
|
374
|
+
version = routes.get(swap.route).version + 1;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Route no longer loads locally — emit without the version line.
|
|
378
|
+
}
|
|
379
|
+
console.log([
|
|
380
|
+
`# revert swap ${swapId}: restore ${swap.route} to its pre-swap candidate order`,
|
|
381
|
+
`# apply to modelrig/routes/${swap.route}.yaml`,
|
|
382
|
+
...(version !== null ? [`version: ${version}`] : ["# bump the version field by 1"]),
|
|
383
|
+
"candidates:",
|
|
384
|
+
...incumbents.flatMap((c) => [` - provider: ${c.provider}`, ` model: ${c.model}`]),
|
|
385
|
+
"",
|
|
386
|
+
].join("\n"));
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
async function cmdSwap(flags) {
|
|
390
|
+
const sub = flags.positional[0];
|
|
391
|
+
if (sub === "execute")
|
|
392
|
+
return cmdSwapExecute(flags);
|
|
393
|
+
if (sub === "revert")
|
|
394
|
+
return cmdSwapRevert(flags);
|
|
395
|
+
throw new Error("usage: modelrig swap execute <proposalId> --approve [--approved-by name] | modelrig swap revert <swapId>");
|
|
396
|
+
}
|
|
397
|
+
async function cmdSamples(flags) {
|
|
398
|
+
const sub = flags.positional[0];
|
|
399
|
+
if (sub !== "purge") {
|
|
400
|
+
throw new Error("usage: modelrig samples purge [--bakeoff <id>]");
|
|
401
|
+
}
|
|
402
|
+
const config = (0, config_1.loadConfigFromEnv)();
|
|
403
|
+
const samplesExport = (0, config_1.directExportOf)(config, "samples purge");
|
|
404
|
+
const client = (0, samples_1.createBakeoffSamplesClient)({
|
|
405
|
+
supabaseUrl: samplesExport.supabaseUrl,
|
|
406
|
+
serviceRoleKey: samplesExport.serviceRoleKey,
|
|
407
|
+
});
|
|
408
|
+
const bakeoffId = flags.named.get("bakeoff");
|
|
409
|
+
const deleted = await client.purge(bakeoffId !== undefined ? { bakeoffId } : { nowIso: new Date().toISOString() });
|
|
410
|
+
console.log(bakeoffId !== undefined
|
|
411
|
+
? `purged ${deleted} review sample(s) for bakeoff ${bakeoffId}`
|
|
412
|
+
: `purged ${deleted} expired review sample(s)`);
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* `modelrig init [path]` — Phase 5 WS2. Scans a repo for provider call
|
|
417
|
+
* sites and drafts a route bundle per site, plus a MIGRATION.md map.
|
|
418
|
+
*
|
|
419
|
+
* It writes ONLY under <routes-dir>/drafts/. Your source is never edited;
|
|
420
|
+
* applying the migration is a human's diff to make.
|
|
421
|
+
*/
|
|
422
|
+
async function cmdInit(flags) {
|
|
423
|
+
const root = (0, node_path_1.resolve)(flags.positional[0] ?? ".");
|
|
424
|
+
const config = (0, config_1.loadConfigFromEnv)();
|
|
425
|
+
const draftsDir = flags.named.get("out") ?? (0, node_path_1.join)(config.routesDir, "drafts");
|
|
426
|
+
// Registry rows seed the challenger candidates. A missing/unbuilt registry
|
|
427
|
+
// is not fatal — drafts then carry the incumbent alone, and say so.
|
|
428
|
+
let registryRows = [];
|
|
429
|
+
try {
|
|
430
|
+
registryRows = (0, rows_1.loadRegistryRows)(config.registryPath);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
registryRows = [];
|
|
434
|
+
}
|
|
435
|
+
const result = (0, run_1.runInit)({
|
|
436
|
+
root,
|
|
437
|
+
draftsDir,
|
|
438
|
+
fs: { readdirSync: node_fs_1.readdirSync, readFileSync: node_fs_1.readFileSync, statSync: node_fs_1.statSync, existsSync: node_fs_1.existsSync, mkdirSync: node_fs_1.mkdirSync, writeFileSync: node_fs_1.writeFileSync },
|
|
439
|
+
path: { join: node_path_1.join, relative: node_path_1.relative, dirname: node_path_1.dirname, basename: node_path_1.basename },
|
|
440
|
+
}, {
|
|
441
|
+
registryRows,
|
|
442
|
+
...(flags.bare.has("dry-run") ? { dryRun: true } : {}),
|
|
443
|
+
...(flags.bare.has("include-tests") ? { includeTests: true } : {}),
|
|
444
|
+
});
|
|
445
|
+
if (flags.bare.has("json")) {
|
|
446
|
+
console.log(JSON.stringify({
|
|
447
|
+
sitesFound: result.sitesFound,
|
|
448
|
+
filesScanned: result.filesScanned,
|
|
449
|
+
filesSkipped: result.filesSkipped,
|
|
450
|
+
routes: result.drafts.map((d) => ({
|
|
451
|
+
route: d.route,
|
|
452
|
+
file: d.site.file,
|
|
453
|
+
line: d.site.line,
|
|
454
|
+
provider: d.site.provider,
|
|
455
|
+
model: d.site.model,
|
|
456
|
+
notes: d.site.notes,
|
|
457
|
+
})),
|
|
458
|
+
written: result.written,
|
|
459
|
+
skipped: result.skipped,
|
|
460
|
+
}, null, 2));
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
console.log(`modelrig init — scanned ${result.filesScanned} file(s) under ${root}\n` +
|
|
464
|
+
`found ${result.sitesFound} provider call site(s)`);
|
|
465
|
+
if (result.sitesFound === 0) {
|
|
466
|
+
console.log("\nNothing to draft. If you know there are model calls here, they may sit behind a helper\n" +
|
|
467
|
+
"that hides the SDK — point init at the file that actually calls the provider.");
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
for (const draft of result.drafts) {
|
|
471
|
+
console.log(` ${draft.site.file}:${draft.site.line} (${draft.site.provider}` +
|
|
472
|
+
`${draft.site.model !== null ? ` ${draft.site.model}` : ""}) → ${draft.route}`);
|
|
473
|
+
}
|
|
474
|
+
if (result.filesSkipped.length > 0) {
|
|
475
|
+
console.log(`\n${result.filesSkipped.length} file(s) skipped (too large or unreadable).`);
|
|
476
|
+
}
|
|
477
|
+
if (flags.bare.has("dry-run")) {
|
|
478
|
+
console.log("\n--dry-run: nothing written.");
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
console.log(`\nwrote ${result.written.length} file(s) to ${draftsDir}` +
|
|
482
|
+
(result.skipped.length > 0 ? ` (${result.skipped.length} left alone — already there)` : "") +
|
|
483
|
+
`\nread ${(0, node_path_1.join)(draftsDir, "MIGRATION.md")} next. Your source was not touched.`);
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* `modelrig serve` — Phase 5 WS4's zero-config path. The HTTP surface lives
|
|
488
|
+
* in the separate `modelrig-server` package (it has its own auth and budget
|
|
489
|
+
* concerns); this is the one command name people will reach for, so it
|
|
490
|
+
* hands off rather than making them learn a second package name.
|
|
491
|
+
*
|
|
492
|
+
* The dependency stays one-way: the core package does not depend on the
|
|
493
|
+
* server, it just calls it when present and teaches when it isn't.
|
|
494
|
+
*/
|
|
495
|
+
async function cmdServe() {
|
|
496
|
+
let serverMain = null;
|
|
497
|
+
try {
|
|
498
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
499
|
+
const mod = require("modelrig-server");
|
|
500
|
+
serverMain = mod.main ?? null;
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
serverMain = null;
|
|
504
|
+
}
|
|
505
|
+
if (serverMain === null) {
|
|
506
|
+
console.error([
|
|
507
|
+
"`modelrig serve` needs the server package, which isn't installed here.",
|
|
508
|
+
"",
|
|
509
|
+
" pnpm add modelrig-server (or npm i / yarn add)",
|
|
510
|
+
"",
|
|
511
|
+
"It's a separate package because serving over HTTP brings its own concerns —",
|
|
512
|
+
"a bearer token and a daily spend cap — that a library shouldn't assume for you.",
|
|
513
|
+
"You do NOT need it to use ModelRig from your own code: `rig.run(...)` works",
|
|
514
|
+
"without any server at all. Docs: https://modelrig.dev/quickstart.html",
|
|
515
|
+
].join("\n"));
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
serverMain();
|
|
519
|
+
// The server owns the process from here (it holds the listener open).
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
async function main(argv) {
|
|
523
|
+
const [command, ...rest] = argv;
|
|
524
|
+
const flags = parseArgs(rest);
|
|
525
|
+
switch (command) {
|
|
526
|
+
case "bakeoff":
|
|
527
|
+
return cmdBakeoff(flags);
|
|
528
|
+
case "watch":
|
|
529
|
+
return cmdWatch(flags);
|
|
530
|
+
case "swap":
|
|
531
|
+
return cmdSwap(flags);
|
|
532
|
+
case "samples":
|
|
533
|
+
return cmdSamples(flags);
|
|
534
|
+
case "init":
|
|
535
|
+
return cmdInit(flags);
|
|
536
|
+
case "serve":
|
|
537
|
+
return cmdServe();
|
|
538
|
+
default:
|
|
539
|
+
console.error([
|
|
540
|
+
"usage:",
|
|
541
|
+
" modelrig init [path] [--out dir] [--dry-run] [--include-tests] [--json]",
|
|
542
|
+
" modelrig serve (needs the modelrig-server package)",
|
|
543
|
+
" modelrig bakeoff --route <route> --variants a,b,c --replay-last N [--envelope usd] [--keep-outputs[=all]] [--json] [--no-mirror]",
|
|
544
|
+
" modelrig bakeoff --from-proposal <id> [--replay-last N] [--keep-outputs[=all]] [--json]",
|
|
545
|
+
" modelrig watch run [--events fixtures.json] [--json]",
|
|
546
|
+
" modelrig swap execute <proposalId> --approve [--approved-by name]",
|
|
547
|
+
" modelrig swap revert <swapId>",
|
|
548
|
+
" modelrig samples purge [--bakeoff <id>]",
|
|
549
|
+
].join("\n"));
|
|
550
|
+
return 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (require.main === module) {
|
|
554
|
+
main(process.argv.slice(2))
|
|
555
|
+
.then((code) => {
|
|
556
|
+
process.exitCode = code;
|
|
557
|
+
})
|
|
558
|
+
.catch((err) => {
|
|
559
|
+
// Errors that teach (WS3): a typed failure prints what happened, what
|
|
560
|
+
// to do, and where to read — from the one table, not from here.
|
|
561
|
+
if (err instanceof failures_1.RigFailureError) {
|
|
562
|
+
console.error((0, failures_teaching_1.renderFailureForTerminal)(err.failure));
|
|
563
|
+
}
|
|
564
|
+
else if ((0, failures_1.isRigFailure)(err)) {
|
|
565
|
+
console.error((0, failures_teaching_1.renderFailureForTerminal)(err));
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
569
|
+
}
|
|
570
|
+
process.exitCode = 1;
|
|
571
|
+
});
|
|
572
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration — plan §3. THE ONLY FILE that reads process.env in this
|
|
3
|
+
* package (adapters receive keys via constructors; enforced by
|
|
4
|
+
* __tests__/package-invariants.test.ts).
|
|
5
|
+
*/
|
|
6
|
+
import type { CapabilityFlag, ProviderId } from "./types/provider";
|
|
7
|
+
export interface RigConfig {
|
|
8
|
+
readonly routesDir: string;
|
|
9
|
+
readonly telemetryDbPath: string;
|
|
10
|
+
readonly keys: {
|
|
11
|
+
readonly gemini?: string;
|
|
12
|
+
readonly openai?: string;
|
|
13
|
+
readonly deepseek?: string;
|
|
14
|
+
readonly anthropic?: string;
|
|
15
|
+
/** xAI — env: XAI_API_KEY (xAI's own convention). */
|
|
16
|
+
readonly grok?: string;
|
|
17
|
+
/** A1 aggregator hosts (generic openai-compatible adapter). */
|
|
18
|
+
readonly deepinfra?: string;
|
|
19
|
+
readonly fireworks?: string;
|
|
20
|
+
};
|
|
21
|
+
/** Budget for envelopes first opened by this rig (default: envelope.ts's $25). */
|
|
22
|
+
readonly envelopeBudgetUsd?: number;
|
|
23
|
+
/** Addendum A1: USD/MTok charged to an active envelope for models with no
|
|
24
|
+
* pricing entry (default $100 — generous-high; envelopes fail closed). */
|
|
25
|
+
readonly unpricedRateUsdPerMTok?: number;
|
|
26
|
+
/** Capability overrides — the escape hatch ABOVE the registry (precedence:
|
|
27
|
+
* overrides > probed > declared). When set for a provider, REPLACES the
|
|
28
|
+
* whole resolved flag set for every model of that provider — any misprobed
|
|
29
|
+
* fact is one config line from fixed. */
|
|
30
|
+
readonly capabilityOverrides?: Partial<Record<ProviderId, readonly CapabilityFlag[]>>;
|
|
31
|
+
/** Registry facts file (Phase 3 WS6) — default: the packaged
|
|
32
|
+
* registry/registry.json. Env: MODELRIG_REGISTRY_PATH. */
|
|
33
|
+
readonly registryPath?: string;
|
|
34
|
+
/** Search provider for the grounding-inject rung (Phase 3 WS3) — omitted
|
|
35
|
+
* means `require: grounded` routes resolve natively-grounded candidates
|
|
36
|
+
* only. */
|
|
37
|
+
readonly search?: {
|
|
38
|
+
readonly tavilyApiKey: string;
|
|
39
|
+
readonly costPerSearchUsd?: number;
|
|
40
|
+
readonly searchDepth?: "basic" | "advanced";
|
|
41
|
+
};
|
|
42
|
+
/** Watch loop (Phase 4 WS1): monthly paid-run cap + material-shift
|
|
43
|
+
* threshold. Defaults apply when unset; the loop itself is opt-in (a cron
|
|
44
|
+
* running `modelrig watch run`). */
|
|
45
|
+
readonly watch?: {
|
|
46
|
+
readonly monthlyUsd?: number;
|
|
47
|
+
readonly thresholdPct?: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Cloud telemetry sink — omitted => local SQLite only.
|
|
51
|
+
*
|
|
52
|
+
* telemetry-ingestion-boundary.md §2.4's three modes, as a discriminated
|
|
53
|
+
* union rather than a bag of optional fields: `api` and `direct` need
|
|
54
|
+
* different credentials and it should be impossible to half-configure one.
|
|
55
|
+
*
|
|
56
|
+
* local-only pure-router users, ZDR-strict no credentials
|
|
57
|
+
* api every hosted customer their rig_sk_ key only
|
|
58
|
+
* direct self-host + our own deployments their own database
|
|
59
|
+
*/
|
|
60
|
+
readonly export?: ApiExportConfig | DirectExportConfig;
|
|
61
|
+
}
|
|
62
|
+
interface ExportCommon {
|
|
63
|
+
readonly sourceId?: string;
|
|
64
|
+
readonly batchSize?: number;
|
|
65
|
+
readonly intervalMs?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ApiExportConfig extends ExportCommon {
|
|
68
|
+
readonly mode: "api";
|
|
69
|
+
/** `rig_sk_…` with the `ingest` scope. The ONLY credential a hosted
|
|
70
|
+
* customer holds — database credentials never leave our infrastructure. */
|
|
71
|
+
readonly apiKey: string;
|
|
72
|
+
readonly ingestUrl: string;
|
|
73
|
+
}
|
|
74
|
+
export interface DirectExportConfig extends ExportCommon {
|
|
75
|
+
readonly mode: "direct";
|
|
76
|
+
readonly supabaseUrl: string;
|
|
77
|
+
readonly serviceRoleKey: string;
|
|
78
|
+
/** The tenant this deployment stamps. Defaults to the house org, which is
|
|
79
|
+
* the right answer for a self-hosted control plane: there is one org there
|
|
80
|
+
* and everything belongs to it. */
|
|
81
|
+
readonly orgId?: string;
|
|
82
|
+
readonly projectId?: string | null;
|
|
83
|
+
}
|
|
84
|
+
/** Provider → the env var that keys it. One table so error surfaces can say
|
|
85
|
+
* WHICH variable to set rather than "no key configured" (F1 / WS3: errors
|
|
86
|
+
* that teach). `loadConfigFromEnv` below reads the same names. */
|
|
87
|
+
export declare const PROVIDER_KEY_ENV: Readonly<Record<string, string>>;
|
|
88
|
+
export declare function loadConfigFromEnv(overrides?: Partial<RigConfig>): RigConfig;
|
|
89
|
+
/**
|
|
90
|
+
* Narrow `config.export` to DIRECT mode, or explain why the command cannot run.
|
|
91
|
+
*
|
|
92
|
+
* Phase 6 WS2 gave the export config two modes. Every command below needs a
|
|
93
|
+
* service-role control-plane client — proposals, swap events, bake-off
|
|
94
|
+
* mirrors, sample purges — and none of those has an ingest endpoint. A rig
|
|
95
|
+
* configured in api mode therefore genuinely cannot run them, and saying so is
|
|
96
|
+
* better than the `undefined` deref this used to be one refactor away from.
|
|
97
|
+
*/
|
|
98
|
+
export declare function directExportOf(config: RigConfig, what: string): {
|
|
99
|
+
readonly supabaseUrl: string;
|
|
100
|
+
readonly serviceRoleKey: string;
|
|
101
|
+
readonly sourceId?: string;
|
|
102
|
+
};
|
|
103
|
+
export {};
|
|
104
|
+
//# sourceMappingURL=config.d.ts.map
|