modelrig 0.4.0 → 0.5.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +13 -0
  3. package/dist/adapters/gemini.d.ts +39 -0
  4. package/dist/adapters/gemini.d.ts.map +1 -1
  5. package/dist/adapters/gemini.js +53 -2
  6. package/dist/artifacts/artifact.d.ts +3 -2
  7. package/dist/artifacts/artifact.d.ts.map +1 -1
  8. package/dist/artifacts/artifact.js +11 -4
  9. package/dist/artifacts/run-context.d.ts +21 -2
  10. package/dist/artifacts/run-context.d.ts.map +1 -1
  11. package/dist/artifacts/run-context.js +46 -1
  12. package/dist/bakeoff/replay.d.ts.map +1 -1
  13. package/dist/bakeoff/replay.js +17 -1
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +21 -1
  16. package/dist/index.d.ts +15 -9
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +15 -5
  19. package/dist/registry/select.d.ts +59 -0
  20. package/dist/registry/select.d.ts.map +1 -1
  21. package/dist/registry/select.js +66 -0
  22. package/dist/routes/loader-warnings.d.ts +28 -0
  23. package/dist/routes/loader-warnings.d.ts.map +1 -0
  24. package/dist/routes/loader-warnings.js +56 -0
  25. package/dist/routes/loader.d.ts +1 -11
  26. package/dist/routes/loader.d.ts.map +1 -1
  27. package/dist/routes/loader.js +44 -26
  28. package/dist/run/raw-defaults.d.ts +52 -0
  29. package/dist/run/raw-defaults.d.ts.map +1 -0
  30. package/dist/run/raw-defaults.js +123 -0
  31. package/dist/run/raw.d.ts +12 -0
  32. package/dist/run/raw.d.ts.map +1 -1
  33. package/dist/run/raw.js +90 -8
  34. package/dist/run/resolve.d.ts +17 -1
  35. package/dist/run/resolve.d.ts.map +1 -1
  36. package/dist/run/resolve.js +20 -4
  37. package/dist/run/routing-docs.js +1 -1
  38. package/dist/run/run.d.ts +7 -1
  39. package/dist/run/run.d.ts.map +1 -1
  40. package/dist/run/run.js +14 -33
  41. package/dist/run/variant.d.ts +43 -0
  42. package/dist/run/variant.d.ts.map +1 -0
  43. package/dist/run/variant.js +86 -0
  44. package/dist/savings/emit.d.ts +33 -0
  45. package/dist/savings/emit.d.ts.map +1 -1
  46. package/dist/savings/emit.js +75 -3
  47. package/dist/types/route.d.ts +31 -0
  48. package/dist/types/route.d.ts.map +1 -1
  49. package/dist/watch/evaluate.d.ts +27 -1
  50. package/dist/watch/evaluate.d.ts.map +1 -1
  51. package/dist/watch/evaluate.js +13 -0
  52. package/dist/watch/proposals.d.ts +28 -0
  53. package/dist/watch/proposals.d.ts.map +1 -1
  54. package/dist/watch/proposals.js +51 -0
  55. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,135 @@ published package diverge.
10
10
  This project adheres to [Semantic Versioning](https://semver.org/). Dates are
11
11
  UTC.
12
12
 
13
+ ## [0.5.0] — 2026-08-22
14
+
15
+ Released with the docs/registry mirror in one R-A ceremony. Headline for router
16
+ codebases: `rig.runRaw` inside a run context now records a step (the `/runs`
17
+ tree fills in), raw-lane timeouts are tier-aware and teach, the `trial:`
18
+ candidate flag + console picker ship (deploy the server before any YAML uses
19
+ `trial:`), and the console rollups show cost and quality together by route or
20
+ tag. `modelrig-oracle` 0.2.0 ships alongside (`propose_candidates`).
21
+
22
+ Raw-lane defaults (#245) — the raw lane teaches its own timeouts. The `timeoutMs`
23
+ default becomes tier-aware, a `timeout` failure carries a teaching `fixHint`, an
24
+ empty no-tools gemini response carries a hint naming grounding, and the raw-lane
25
+ defaults are published as a generated table that cannot drift from the runtime.
26
+
27
+ Declare-first experiment ergonomics (routing-console R3, #239) — a `trial:`
28
+ candidate you declare but never serve live, a console challenger picker, and a
29
+ one-command bake-off that measures it on real captured traffic before you
30
+ promote it.
31
+
32
+ ### Changed
33
+
34
+ - **Raw-lane `timeoutMs` default is now tier-aware** (was a flat 120 s):
35
+ `standard`/`priority` default to 300 s and `flex` to 900 s (the 15-minute budget
36
+ ModelRig advertises) — a passthrough should not be shorter-fused than the tier it
37
+ advertises, and grounded/large prompts routinely exceed 120 s even at standard.
38
+ An explicit `timeoutMs` **always wins** (no pre-dispatch rejection — a short
39
+ deadline is a choice), and a `timeout` failure on the raw lane now carries a
40
+ `fixHint` naming the elapsed time, the tier, that tier's budget, and the two
41
+ fixes (raise `timeoutMs`, or drop to `serviceTier: "standard"`). The routed
42
+ lane's required `policy.timeout_ms` is unchanged. Callers who relied on a fast
43
+ 120 s failure should set `timeoutMs` explicitly.
44
+
45
+ ### Added — declare-first experiment (routing-console R3)
46
+
47
+ - **`candidates[].trial: true`** (route bundle) — a DECLARED candidate that is
48
+ NEVER served live. The loader parses and validates it, marks an all-trial
49
+ variant `trialOnly`, and warns on a trial candidate no variant references;
50
+ `resolveCandidates` drops trial candidates on every live path (`rig.run`,
51
+ `runStream`, `runBundle`, the hosted gateway slug lane) and keeps them only in
52
+ bake-off replay. Selecting a trial variant on a live call is a
53
+ `RouteConfigError` that names the bake-off. This lets a customer measure a new
54
+ model on real captured traffic before promoting it. Additive: a route with no
55
+ trial candidate is byte-identical to before.
56
+ - CAVEAT — **deploy the server before any customer YAML carries `trial:`.** An
57
+ SDK/server older than 0.5.0 IGNORES `trial:` (the loader has always been
58
+ tolerant of unknown candidate keys) and its resolver has no trial-mode filter,
59
+ so it would serve the candidate LIVE — including an older hosted-gateway build
60
+ reading the flag through the mirror. Roll the server first, then let routes
61
+ start declaring `trial:`. The console's declare-YAML says so inline.
62
+ - **`requirementPredicates(route)` / `evaluateChallengerEligibility(...)`** (new
63
+ exports) — decompose a route's `require` + json rung into capability-flag
64
+ requirements and answer "can this model be a trial challenger?", pinned in
65
+ agreement with the live resolver's `satisfiesRequirements`.
66
+ - **`buildDeclareTrialChange(bundle, challenger)` / `trialVariantName(...)`** and
67
+ a trial-winner branch of **`buildSwapChange`** (new exports) — the paste-ready
68
+ declare-YAML (challenger appended `trial: true`, a `trial-<provider>-<model>`
69
+ variant, `version: n+1`) and the promote-YAML that drops the winner's trial
70
+ flag, moves it to position 0, and removes the trial variant. No filesystem, no
71
+ git — the human applies the PR (F6).
72
+ - **`ManualChallengerTrigger` + `manualChallengerTriggerHash`**, and
73
+ **`proposalsClient.createManualChallenger`** — a console/oracle-initiated
74
+ declare-first proposal (`kind:"bakeoff"`, idempotent per `(route, challenger)`).
75
+ - **`RunMeta`/`Rig` additions:** `rig.capabilities` and `rig.searchAvailable` are
76
+ exposed so the challenger picker answers eligibility with the exact live flags;
77
+ `modelrig bakeoff --from-proposal <id>` reads the trial variant from a
78
+ manual_challenger proposal (default vs that one variant).
79
+ - **`resolveRouteBundle` / `hashResolvedBundle`** (new exports) — the canonical
80
+ `bundle_sha` helpers, used by the hosted-lane bake-off to prove the caller org's
81
+ mirrored bundle is the SAME bundle the deployment holds locally (a route-name
82
+ collision on a multi-tenant mirror otherwise baked off a foreign org's route).
83
+
84
+ ### Added
85
+
86
+ - **A grounding-naming `fixHint` on an empty no-tools gemini response.** When a
87
+ call that declared no tools comes back with no text parts (a grounding turn
88
+ that produced only `functionCall` parts, for example), the `content_invalid`
89
+ "empty response" failure now names grounding as the likely cause. Note: the
90
+ text extraction itself is unchanged in effect — the SDK already concatenated
91
+ mixed `functionCall` + text parts — so the customer-zero empty-response report
92
+ (#245 secondary) remains an OPEN investigation pending a captured response.
93
+
94
+ ### Docs
95
+
96
+ - **Raw-lane defaults table** — a generated table (`RAW_DEFAULTS`, rendered at the
97
+ `<!-- RAW_DEFAULTS -->` marker in `route-bundles.md` and in `llms.txt`) lists
98
+ every raw-lane knob and its default, with the tier-aware `timeoutMs` read from
99
+ the same constant the runtime resolves from — so the published defaults cannot
100
+ drift from what `rig.runRaw` sends. README / quickstart / migration-playbook
101
+ gain the "flex ⇒ set `timeoutMs` in minutes" one-liner.
102
+
103
+ Raw-lane artifact graph + tags parity (G1). A `rig.runRaw` / `rig.runRawStream`
104
+ call inside a `rig.artifacts.run.start` context now records a ground-truth step,
105
+ so a router or report pipeline that wraps raw (BYOK) calls in a run context gets
106
+ a real per-run tree at `/runs/[id]` instead of an empty one. ZERO DDL.
107
+
108
+ ### Added — raw-lane graph
109
+
110
+ - **Raw calls record a ground-truth step.** Inside a run context,
111
+ `runRaw`/`runRawStream` emit a step (on success AND failure, buffered AND
112
+ streaming) whose `inference_id` joins the metered `lane=raw` row — parity with
113
+ the routed lane's auto-step. Fail-open: a buffer error there never affects the
114
+ returned result. Outside a run context the dispatch is byte-identical to 0.4.0
115
+ (no step row). Raw steps are **metadata only** — no artifact content is written
116
+ on the raw lane.
117
+ - **`stepKey` on BOTH lanes.** `RunOptions.stepKey` (routed) and
118
+ `RawRunInput.stepKey` (raw) name the step. Resolution is `stepKey ?? tags.step
119
+ ?? auto` (`route#n` / `provider/model#n`), validated `[A-Za-z0-9_.:#-]{1,128}`;
120
+ an invalid value is warned once and dropped to the auto key (never thrown). A
121
+ raw row's step **task** is `tags.step ?? provider/model`.
122
+ - **Ambient artifact step.** `artifact.save` without a `stepKey` now attaches to
123
+ the ambient step (the most recent step in the run context), so the two grouping
124
+ mechanisms compose — a raw/routed call and the artifacts saved around it land in
125
+ one group.
126
+ - **Required-dimension hygiene on the raw lane.** `runRaw`/`runRawStream` run the
127
+ same once-per-process "required dimension missing from tags" warning the routed
128
+ lane does.
129
+
130
+ ### Added — docs
131
+
132
+ - **instrumentation.md** gains "Tags, dimensions, and the graph": tags = flat
133
+ filter/rollup (`/costs`), the graph = the per-run tree (`/runs/[id]`), how
134
+ `stepKey` + ambient artifacts compose, and the raw-lane behavior.
135
+ - **The per-service key rule** is now stated in the quickstart, the migration
136
+ playbook (F-6), and llms.txt: `MODELRIG_API_KEY` gates the exporter *per
137
+ process* — set it on EVERY service, worker, and cron, or a keyless process
138
+ buffers to its own local SQLite and ships nothing.
139
+ - **route-bundles.md** points at where `rig.yaml` `dimensions` are declared; the
140
+ **README** documents `stepKey` on the raw lane.
141
+
13
142
  ## [0.4.0] — 2026-08-22
14
143
 
15
144
  Released with the docs/registry mirror in one R-A ceremony. NOT in this tag:
package/README.md CHANGED
@@ -144,6 +144,19 @@ never stores it. It records one metered `lane=raw` telemetry row at the
144
144
  provider's list price (zero ModelRig margin) and throws a typed `RigFailureError`
145
145
  on failure, exactly like `rig.run`.
146
146
 
147
+ Called inside a `rig.artifacts.run.start` context, `runRaw`/`runRawStream` also
148
+ records a ground-truth **step** in the run's graph (`/runs/[id]`) — since
149
+ modelrig 0.5.0, on success and on failure. Name it with `stepKey` (the same field
150
+ `RunOptions` exposes on the routed lane; precedence `stepKey` > `tags.step` > an
151
+ auto `provider/model#n` key), so a raw call and the artifacts you `save` around it
152
+ group under one step.
153
+
154
+ The raw-lane `timeoutMs` default is **tier-aware** since 0.5.0 — `standard`/
155
+ `priority` 300 s, `flex` 900 s; an explicit `timeoutMs` always wins, and a
156
+ `timeout` failure teaches the tier + budget in its `fixHint`. **flex ⇒ set
157
+ `timeoutMs` in minutes** (default 900 s since 0.5.0). See the generated
158
+ [raw-lane defaults table](https://modelrig.dev/route-bundles.html#raw-lane-defaults-rigrunraw).
159
+
147
160
  **runRaw-only construction (`routesDir: null`) — 0.4.0+.** A
148
161
  Lane-B deployment that ONLY calls `runRaw`/`runRawStream` has no routes. Build it
149
162
  with `routesDir: null` and `createRig` skips route loading, the `rig.yaml`
@@ -56,5 +56,44 @@ export interface GeminiShapedRequest {
56
56
  }
57
57
  /** Pure request shaping — everything except the abort signal. */
58
58
  export declare function buildGeminiRequest(model: string, req: ShapingInput): GeminiShapedRequest;
59
+ /** A candidate's parts as we read them post-response — `functionCall`, `text`,
60
+ * and (if `includeThoughts` were ever set) a `thought`-flagged text part may all
61
+ * appear on the same candidate (a grounded / thinking turn). */
62
+ type GeminiCandidatePart = {
63
+ readonly functionCall?: unknown;
64
+ readonly text?: string;
65
+ readonly thought?: boolean;
66
+ };
67
+ type GeminiCandidateShape = {
68
+ readonly content?: {
69
+ readonly parts?: readonly GeminiCandidatePart[];
70
+ };
71
+ };
72
+ /**
73
+ * #245 secondary — concatenate the answer `text` parts of a Gemini candidate's
74
+ * content. On the raw / no-tools lane a grounding-originated `functionCall` part
75
+ * (googleSearch) is NOT a tool-call turn, and the SDK's `response.text` getter
76
+ * returns empty (and logs a warning) on a response that mixes `functionCall` +
77
+ * `text` parts — so read the text parts directly. `thought`-flagged parts are
78
+ * skipped to match `response.text` semantics (they are the reasoning summary, not
79
+ * the answer). Pure + exported for the fixture regression test (no network).
80
+ * (spec §2 Secondary — the SNPS bug.)
81
+ */
82
+ export declare function extractGeminiCandidateText(candidate: GeminiCandidateShape | undefined): string;
83
+ /** #245 secondary — whether a candidate carries a `functionCall` part. On the
84
+ * no-tools lane this means grounding produced only a googleSearch invocation with
85
+ * no text answer, which the empty-response `fixHint` should name. */
86
+ export declare function geminiCandidateHasFunctionCall(candidate: GeminiCandidateShape | undefined): boolean;
87
+ /**
88
+ * #245 secondary — pick the answer text from a Gemini response. The branch the
89
+ * `call()` path uses, extracted pure so it is unit-testable without a network:
90
+ * - tools declared: trust the SDK `response.text` getter, byte-identical to
91
+ * pre-#245 (a functionCall turn is handled earlier as a tool-call).
92
+ * - no tools declared: read the candidate's text parts directly, because
93
+ * `response.text` returns empty on a grounding-originated functionCall+text
94
+ * response; fall back to `response.text` only if there is no text part.
95
+ */
96
+ export declare function resolveGeminiText(candidate: GeminiCandidateShape | undefined, responseText: string | undefined, hasTools: boolean): string;
59
97
  export declare function createGeminiAdapter(config: GeminiAdapterConfig): ProviderAdapter;
98
+ export {};
60
99
  //# sourceMappingURL=gemini.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/adapters/gemini.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EACV,cAAc,EAGd,cAAc,EACd,eAAe,EAEhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,mBAAmB,CAAC;AAKlE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;mCAImC;AACnC,eAAO,MAAM,8BAA8B,EAAE,WAAW,CAAC,MAAM,CAI7D,CAAC;AA0BH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAyCxE;AAED;6EAC6E;AAC7E,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,iBAAiB,GAAG,IAAI,CAQrG;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,mBAAmB,CAqDxF;AA0BD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,eAAe,CA+JhF"}
1
+ {"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/adapters/gemini.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EACV,cAAc,EAGd,cAAc,EACd,eAAe,EAEhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,mBAAmB,CAAC;AAKlE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;mCAImC;AACnC,eAAO,MAAM,8BAA8B,EAAE,WAAW,CAAC,MAAM,CAI7D,CAAC;AA0BH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAyCxE;AAED;6EAC6E;AAC7E,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,iBAAiB,GAAG,IAAI,CAQrG;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,mBAAmB,CAqDxF;AA0BD;;gEAEgE;AAChE,KAAK,mBAAmB,GAAG;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AACF,KAAK,oBAAoB,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;KAAE,CAAA;CAAE,CAAC;AAEvG;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,oBAAoB,GAAG,SAAS,GAAG,MAAM,CAM9F;AAED;;qEAEqE;AACrE,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,oBAAoB,GAAG,SAAS,GAAG,OAAO,CAInG;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,oBAAoB,GAAG,SAAS,EAC3C,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,OAAO,GAChB,MAAM,CAGR;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,eAAe,CA+KhF"}
@@ -20,6 +20,9 @@ exports.geminiSupports = geminiSupports;
20
20
  exports.classifyGeminiFailure = classifyGeminiFailure;
21
21
  exports.geminiReasoningUnsupported = geminiReasoningUnsupported;
22
22
  exports.buildGeminiRequest = buildGeminiRequest;
23
+ exports.extractGeminiCandidateText = extractGeminiCandidateText;
24
+ exports.geminiCandidateHasFunctionCall = geminiCandidateHasFunctionCall;
25
+ exports.resolveGeminiText = resolveGeminiText;
23
26
  exports.createGeminiAdapter = createGeminiAdapter;
24
27
  const genai_1 = require("@google/genai");
25
28
  const tool_mapping_1 = require("./tool-mapping");
@@ -190,6 +193,43 @@ function extractServedTier(rawServiceTier, trafficType, requested) {
190
193
  return "priority";
191
194
  return requested;
192
195
  }
196
+ /**
197
+ * #245 secondary — concatenate the answer `text` parts of a Gemini candidate's
198
+ * content. On the raw / no-tools lane a grounding-originated `functionCall` part
199
+ * (googleSearch) is NOT a tool-call turn, and the SDK's `response.text` getter
200
+ * returns empty (and logs a warning) on a response that mixes `functionCall` +
201
+ * `text` parts — so read the text parts directly. `thought`-flagged parts are
202
+ * skipped to match `response.text` semantics (they are the reasoning summary, not
203
+ * the answer). Pure + exported for the fixture regression test (no network).
204
+ * (spec §2 Secondary — the SNPS bug.)
205
+ */
206
+ function extractGeminiCandidateText(candidate) {
207
+ return (candidate?.content?.parts ?? [])
208
+ .filter((p) => p.thought !== true)
209
+ .map((p) => p.text)
210
+ .filter((t) => typeof t === "string")
211
+ .join("");
212
+ }
213
+ /** #245 secondary — whether a candidate carries a `functionCall` part. On the
214
+ * no-tools lane this means grounding produced only a googleSearch invocation with
215
+ * no text answer, which the empty-response `fixHint` should name. */
216
+ function geminiCandidateHasFunctionCall(candidate) {
217
+ return (candidate?.content?.parts ?? []).some((p) => p.functionCall !== undefined && p.functionCall !== null);
218
+ }
219
+ /**
220
+ * #245 secondary — pick the answer text from a Gemini response. The branch the
221
+ * `call()` path uses, extracted pure so it is unit-testable without a network:
222
+ * - tools declared: trust the SDK `response.text` getter, byte-identical to
223
+ * pre-#245 (a functionCall turn is handled earlier as a tool-call).
224
+ * - no tools declared: read the candidate's text parts directly, because
225
+ * `response.text` returns empty on a grounding-originated functionCall+text
226
+ * response; fall back to `response.text` only if there is no text part.
227
+ */
228
+ function resolveGeminiText(candidate, responseText, hasTools) {
229
+ if (hasTools)
230
+ return responseText ?? "";
231
+ return extractGeminiCandidateText(candidate) || (responseText ?? "");
232
+ }
193
233
  function createGeminiAdapter(config) {
194
234
  const ai = new genai_1.GoogleGenAI({ apiKey: config.apiKey });
195
235
  return {
@@ -266,9 +306,20 @@ function createGeminiAdapter(config) {
266
306
  if (finishReason === "RECITATION") {
267
307
  return fail("refusal", "Gemini blocked output via recitation filter (finishReason=RECITATION)", undefined, usage);
268
308
  }
269
- const text = response.text ?? "";
309
+ // #245 secondary: when the caller declared NO tools, a `functionCall`
310
+ // part is grounding-originated (googleSearch), NOT a tool-call turn — and
311
+ // `response.text` returns empty on a mixed functionCall+text response, so
312
+ // read the text parts directly. When tools WERE declared this path is
313
+ // reached only for a no-functionCall response, so `response.text` is
314
+ // byte-identical to before.
315
+ const noTools = req.tools === undefined || req.tools.length === 0;
316
+ const text = resolveGeminiText(candidates?.[0], response.text, !noTools);
270
317
  if (text.trim().length === 0) {
271
- return fail("content_invalid", `empty response from Gemini (finishReason=${finishReason ?? "none"})`, undefined, usage);
318
+ const groundedNoText = noTools &&
319
+ geminiCandidateHasFunctionCall(candidates?.[0]);
320
+ return fail("content_invalid", `empty response from Gemini (finishReason=${finishReason ?? "none"})`, groundedNoText
321
+ ? "the grounded response returned only a googleSearch functionCall part with no text — refine the grounding query, or remove grounding if the model can answer directly"
322
+ : undefined, usage);
272
323
  }
273
324
  if (finishReason === "MAX_TOKENS" && req.outputSchema !== null) {
274
325
  // A truncated structured emission is mid-object and unparseable.
@@ -39,8 +39,9 @@ export interface ArtifactSaveOptions {
39
39
  /** The pipeline step this artifact belongs to — the semantic grouping key the
40
40
  * console's chain view orders on (E3 spec §2.1). E2 deferred step attribution
41
41
  * ("refined when @step scopes persist, E3"); an EXPLICIT key set by the caller
42
- * is the E3 refinement — safe under parallel step waves where an ambient
43
- * mutable current-step would cross-attribute. Null (absent) ⇒ ungrouped. */
42
+ * is the E3 refinement and stays the safe choice under parallel steps inside
43
+ * ONE run; absent one, the ambient current step (G1) is used — see
44
+ * run-context.ts currentStepKey(). Null (no key, no context) ⇒ ungrouped. */
44
45
  readonly stepKey?: string;
45
46
  /** The step attempt this artifact came from (mirrors steps.attempt_no). Only
46
47
  * meaningful alongside stepKey; null for artifacts saved outside a step. */
@@ -1 +1 @@
1
- {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../src/artifacts/artifact.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../src/artifacts/artifact.ts"],"names":[],"mappings":"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"}
@@ -166,6 +166,11 @@ function createArtifactApi(deps) {
166
166
  const schema = opts.schema ?? null;
167
167
  const id = (0, node_crypto_1.randomUUID)();
168
168
  const version = opts.supersedes !== undefined ? opts.supersedes.version + 1 : 1;
169
+ // G1 (spec §3): an explicit stepKey wins; absent, the artifact lands in the
170
+ // AMBIENT step — the most recent auto-step in this run context — so
171
+ // `artifact.save` and the run/@step graph compose. Null outside a step (or
172
+ // outside a run context), exactly as before.
173
+ const stepKey = opts.stepKey ?? ctx?.currentStepKey() ?? null;
169
174
  // Custody applies only when a rig content policy exists AND the caller left
170
175
  // the bytes to us (a caller-supplied contentRef = their own store; managed
171
176
  // custody must not double-home it).
@@ -181,9 +186,10 @@ function createArtifactApi(deps) {
181
186
  store.recordArtifact({
182
187
  id,
183
188
  runId: ctx?.runId ?? null,
184
- // E3: step attribution is now an explicit caller-supplied key (see
185
- // ArtifactSaveOptions.stepKey). Absent ⇒ null, exactly as before.
186
- stepKey: opts.stepKey ?? null,
189
+ // E3: step attribution is an explicit caller-supplied key; G1 adds the
190
+ // ambient-step fallback (resolved above) so a save with no stepKey joins
191
+ // the current run/@step. Absent both ⇒ null, exactly as before.
192
+ stepKey,
187
193
  attemptNo: opts.attemptNo ?? null,
188
194
  task: opts.task ?? null,
189
195
  name: opts.name,
@@ -243,7 +249,8 @@ function createArtifactApi(deps) {
243
249
  store.recordArtifact({
244
250
  id: redactedId,
245
251
  runId: ctx?.runId ?? null,
246
- stepKey: opts.stepKey ?? null,
252
+ // Same ambient-step resolution as the original row it supersedes.
253
+ stepKey,
247
254
  attemptNo: opts.attemptNo ?? null,
248
255
  task: opts.task ?? null,
249
256
  name: opts.name,
@@ -68,16 +68,35 @@ export interface ActiveRunContext {
68
68
  takeLoadedIds(): string[];
69
69
  /** Mint a unique step key for ONE rig.run()/@step invocation. Retries of that
70
70
  * invocation reuse the returned key (attempt_no distinguishes them); a later
71
- * invocation gets a fresh key, so two calls never collapse into one step. */
71
+ * invocation gets a fresh key, so two calls never collapse into one step. Also
72
+ * marks the minted key as the ambient step (see currentStepKey). */
72
73
  nextStepKey(label: string): string;
74
+ /** The most recent step key in this context — the ambient step an
75
+ * `artifact.save` without an explicit `stepKey` attaches to (G1 spec §3). Set
76
+ * by both `nextStepKey` (auto-steps, `@step` scopes) and `emitGroundTruthStep`
77
+ * (a raw-lane call with a caller-supplied key that never minted). Null before
78
+ * any step in this run. NOTE: a single mutable current-step cross-attributes
79
+ * under parallel step waves on ONE context — use `run.scope()` (each concurrent
80
+ * run gets its own context) or an explicit `stepKey` when steps overlap. */
81
+ currentStepKey(): string | null;
73
82
  /** Record one ground-truth step attempt (the rig.run auto-step). attempt_no is
74
83
  * assigned here, per step key. Fail-open — a buffer error is logged, dropped,
75
- * and never thrown into the run loop. */
84
+ * and never thrown into the run loop. Marks its key as the ambient step. */
76
85
  emitGroundTruthStep(input: GroundTruthStepInput): void;
77
86
  }
78
87
  /** The active run context, or null when none is set (namespace disabled, or no
79
88
  * run.start on this async path). Read by the run loop's auto-step seam. */
80
89
  export declare function getActiveRunContext(): ActiveRunContext | null;
90
+ /** True when `key` is a valid caller-supplied step key (spec §3). */
91
+ export declare function isValidStepKey(key: string): boolean;
92
+ /** Resolve a ground-truth step key for either lane (spec §3): an explicit
93
+ * caller-supplied key wins, then the `tags.step` convention, else a freshly
94
+ * minted auto key. A caller key is validated `[A-Za-z0-9_.:#-]{1,128}`; an
95
+ * invalid one is warned (once per bad value) and DROPPED to the auto key —
96
+ * instrumentation never throws a telemetry-label error into customer code. */
97
+ export declare function resolveStepKey(explicit: string | undefined, tagStep: string | undefined, auto: () => string): string;
98
+ /** Test-only: clear the once-per-process invalid-stepKey warning latch. */
99
+ export declare function resetStepKeyWarningsForTest(): void;
81
100
  /** A run-level evaluation the caller attaches (E5 chunk V.2) — the run-subject
82
101
  * mirror of ArtifactEvaluateOptions. The subject is the run, so there is no ref. */
83
102
  export interface RunEvaluateOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"run-context.d.ts","sourceRoot":"","sources":["../../src/artifacts/run-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run-context.d.ts","sourceRoot":"","sources":["../../src/artifacts/run-context.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,9 @@
21
21
  */
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.getActiveRunContext = getActiveRunContext;
24
+ exports.isValidStepKey = isValidStepKey;
25
+ exports.resolveStepKey = resolveStepKey;
26
+ exports.resetStepKeyWarningsForTest = resetStepKeyWarningsForTest;
24
27
  exports.createRunNamespace = createRunNamespace;
25
28
  const node_async_hooks_1 = require("node:async_hooks");
26
29
  const node_crypto_1 = require("node:crypto");
@@ -32,6 +35,38 @@ function getActiveRunContext() {
32
35
  const ctx = storage.getStore();
33
36
  return ctx !== undefined && !ctx.ended ? ctx : null;
34
37
  }
38
+ /** Valid ground-truth step key (G1 spec §3): a caller-supplied `stepKey` (either
39
+ * lane) or a `tags.step` used as one. Auto-minted keys (`label#seq`) may contain
40
+ * a `/` from a route slug or `provider/model` label and are NOT validated — the
41
+ * regex gates only what a customer types. */
42
+ const STEP_KEY_PATTERN = /^[A-Za-z0-9_.:#-]{1,128}$/;
43
+ /** True when `key` is a valid caller-supplied step key (spec §3). */
44
+ function isValidStepKey(key) {
45
+ return STEP_KEY_PATTERN.test(key);
46
+ }
47
+ const warnedStepKeys = new Set();
48
+ /** Resolve a ground-truth step key for either lane (spec §3): an explicit
49
+ * caller-supplied key wins, then the `tags.step` convention, else a freshly
50
+ * minted auto key. A caller key is validated `[A-Za-z0-9_.:#-]{1,128}`; an
51
+ * invalid one is warned (once per bad value) and DROPPED to the auto key —
52
+ * instrumentation never throws a telemetry-label error into customer code. */
53
+ function resolveStepKey(explicit, tagStep, auto) {
54
+ const candidate = explicit ?? tagStep;
55
+ if (candidate === undefined)
56
+ return auto();
57
+ if (isValidStepKey(candidate))
58
+ return candidate;
59
+ if (!warnedStepKeys.has(candidate)) {
60
+ warnedStepKeys.add(candidate);
61
+ console.warn(`[modelrig artifacts] invalid stepKey "${candidate}" (allowed [A-Za-z0-9_.:#-], 1-128 chars) — ` +
62
+ "grouped under an auto-generated key instead.");
63
+ }
64
+ return auto();
65
+ }
66
+ /** Test-only: clear the once-per-process invalid-stepKey warning latch. */
67
+ function resetStepKeyWarningsForTest() {
68
+ warnedStepKeys.clear();
69
+ }
35
70
  function warnOnce(scope, err) {
36
71
  console.warn(`[modelrig artifacts] ${scope} buffer write failed (dropped, run unaffected): ` +
37
72
  `${err instanceof Error ? err.message : String(err)}`);
@@ -67,6 +102,9 @@ function createRunNamespace(deps) {
67
102
  const attemptByStep = new Map();
68
103
  const loaded = new Set();
69
104
  let stepSeq = 0;
105
+ // The ambient step key (G1): the last step minted or emitted in this run,
106
+ // which an artifact.save without an explicit stepKey attaches to.
107
+ let lastStepKey = null;
70
108
  return {
71
109
  runId,
72
110
  traceId,
@@ -84,9 +122,16 @@ function createRunNamespace(deps) {
84
122
  },
85
123
  nextStepKey(label) {
86
124
  stepSeq += 1;
87
- return `${label}#${stepSeq}`;
125
+ lastStepKey = `${label}#${stepSeq}`;
126
+ return lastStepKey;
127
+ },
128
+ currentStepKey() {
129
+ return lastStepKey;
88
130
  },
89
131
  emitGroundTruthStep(input) {
132
+ // A caller-supplied key on the raw lane never minted through nextStepKey;
133
+ // record it as the ambient step so artifacts saved around this call join it.
134
+ lastStepKey = input.stepKey;
90
135
  const attemptNo = (attemptByStep.get(input.stepKey) ?? 0) + 1;
91
136
  attemptByStep.set(input.stepKey, attemptNo);
92
137
  try {
@@ -1 +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"}
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;AAI1C,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,CAwFxC"}
@@ -23,16 +23,29 @@ exports.replayVariant = replayVariant;
23
23
  const node_crypto_1 = require("node:crypto");
24
24
  const failures_1 = require("../types/failures");
25
25
  const run_1 = require("../run/run");
26
+ const variant_1 = require("../run/variant");
26
27
  async function replayVariant(deps, captures, req) {
27
28
  const rows = captures.readLast(req.route, req.lastN);
28
29
  const replayId = (0, node_crypto_1.randomUUID)();
29
30
  const envelopeKey = `replay:${replayId}`;
31
+ // Trial mode is scoped to the ARM being replayed, not the whole pass. A
32
+ // bake-off measures a declared trial candidate ONLY under a trial-only
33
+ // variant arm; the `default` arm (and any non-trial variant) must replay in
34
+ // LIVE mode so the incumbent never admits the cheaper trial candidate —
35
+ // otherwise `prefer:[cost]` would pick the trial model on BOTH arms and the
36
+ // comparison degenerates to trial-vs-trial (armMeta then misreports the
37
+ // incumbent). `mode: "trial"` keeps trial candidates and permits selecting a
38
+ // trial variant; `live` drops them, exactly as every production surface does.
39
+ const route = deps.routes.get(req.route);
40
+ const variant = req.variant === "default" ? undefined : route.variants.find((v) => v.name === req.variant);
41
+ const mode = variant !== undefined && (0, variant_1.isTrialOnlyVariant)(route, variant) ? "trial" : "live";
30
42
  // Zero production exposure knobs: no capture writes (no replay-of-replay),
31
43
  // and the whole pass shares one hard envelope.
32
44
  const replayDeps = {
33
45
  ...deps,
34
46
  captures: null,
35
47
  envelopeBudgetUsd: req.envelopeUsd,
48
+ mode,
36
49
  };
37
50
  const results = [];
38
51
  for (const capture of rows) {
@@ -48,8 +61,11 @@ async function replayVariant(deps, captures, req) {
48
61
  // text — plan §10 replay-validity risk), repair/grounding rungs and
49
62
  // retry budgets all behave exactly as production serving would.
50
63
  const result = await (0, run_1.run)(replayDeps, req.route, {
64
+ // `lane: "trial"` (spec §4.4) tags every replay attempt row so the R1
65
+ // route rollups and the savings odometer EXCLUDE trial spend — a
66
+ // bake-off's real dispatches must never move a live route's numbers.
67
+ tags: { replay: replayId, replay_capture: sampleTag, variant: req.variant, lane: "trial" },
51
68
  input: JSON.parse(capture.variablesJson),
52
- tags: { replay: replayId, replay_capture: sampleTag, variant: req.variant },
53
69
  ...(req.variant !== "default" ? { variant: req.variant } : {}),
54
70
  budget: { envelope: envelopeKey },
55
71
  });
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AAkCH,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;AAeD,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAuBxD;AA0gBD,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCnE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AAkCH,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;AAeD,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAuBxD;AA+hBD,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCnE"}
package/dist/cli.js CHANGED
@@ -159,7 +159,27 @@ async function cmdBakeoff(flags) {
159
159
  }
160
160
  const routes = (0, loader_1.loadRoutes)((0, rawonly_1.requireRoutesDir)(config, "modelrig bakeoff"));
161
161
  const bundle = routes.get(proposal.route);
162
- const variants = ["default", ...bundle.variants.map((v) => v.name)];
162
+ // Declare-first (routing-console spec §4.4, R3): a manual_challenger proposal
163
+ // names ONE trial variant — bake off default vs that variant only. The trial
164
+ // variant must be DECLARED in the local routes (the human merged the
165
+ // declare-YAML); until then it isn't loadable, so fail with a teaching error
166
+ // rather than a bake-off that silently omits the challenger.
167
+ let variants;
168
+ const trigger = proposal.trigger;
169
+ if (trigger.type === "manual_challenger") {
170
+ const trialVariant = trigger.variant;
171
+ if (typeof trialVariant !== "string" || trialVariant === "") {
172
+ throw new Error(`proposal ${proposalId} is a manual_challenger with no trial variant name`);
173
+ }
174
+ if (!bundle.variants.some((v) => v.name === trialVariant)) {
175
+ throw new Error(`route "${proposal.route}" does not declare the trial variant "${trialVariant}" yet — ` +
176
+ "merge the declare-YAML from the proposal first (modelrig ... declare-body), then re-run");
177
+ }
178
+ variants = ["default", trialVariant];
179
+ }
180
+ else {
181
+ variants = ["default", ...bundle.variants.map((v) => v.name)];
182
+ }
163
183
  const lastRaw = flags.named.get("replay-last");
164
184
  const replayLastN = lastRaw !== undefined ? Number(lastRaw) : 25;
165
185
  if (!Number.isInteger(replayLastN) || replayLastN < 1) {