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.
- package/CHANGELOG.md +129 -0
- package/README.md +13 -0
- package/dist/adapters/gemini.d.ts +39 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +53 -2
- package/dist/artifacts/artifact.d.ts +3 -2
- package/dist/artifacts/artifact.d.ts.map +1 -1
- package/dist/artifacts/artifact.js +11 -4
- package/dist/artifacts/run-context.d.ts +21 -2
- package/dist/artifacts/run-context.d.ts.map +1 -1
- package/dist/artifacts/run-context.js +46 -1
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +17 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/registry/select.d.ts +59 -0
- package/dist/registry/select.d.ts.map +1 -1
- package/dist/registry/select.js +66 -0
- package/dist/routes/loader-warnings.d.ts +28 -0
- package/dist/routes/loader-warnings.d.ts.map +1 -0
- package/dist/routes/loader-warnings.js +56 -0
- package/dist/routes/loader.d.ts +1 -11
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +44 -26
- package/dist/run/raw-defaults.d.ts +52 -0
- package/dist/run/raw-defaults.d.ts.map +1 -0
- package/dist/run/raw-defaults.js +123 -0
- package/dist/run/raw.d.ts +12 -0
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +90 -8
- package/dist/run/resolve.d.ts +17 -1
- package/dist/run/resolve.d.ts.map +1 -1
- package/dist/run/resolve.js +20 -4
- package/dist/run/routing-docs.js +1 -1
- package/dist/run/run.d.ts +7 -1
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +14 -33
- package/dist/run/variant.d.ts +43 -0
- package/dist/run/variant.d.ts.map +1 -0
- package/dist/run/variant.js +86 -0
- package/dist/savings/emit.d.ts +33 -0
- package/dist/savings/emit.d.ts.map +1 -1
- package/dist/savings/emit.js +75 -3
- package/dist/types/route.d.ts +31 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/watch/evaluate.d.ts +27 -1
- package/dist/watch/evaluate.d.ts.map +1 -1
- package/dist/watch/evaluate.js +13 -0
- package/dist/watch/proposals.d.ts +28 -0
- package/dist/watch/proposals.d.ts.map +1 -1
- package/dist/watch/proposals.js +51 -0
- 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+
|
|
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"}
|
package/dist/adapters/gemini.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
*
|
|
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":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAEnF;4DAC4D;AAC5D,MAAM,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEnG;8CAC8C;AAC9C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;CAC1C;AAYD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB
|
|
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
|
|
185
|
-
//
|
|
186
|
-
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGjF;;sEAEsE;AACtE,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;AAEzC,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;wEAEoE;IACpE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;oCAGgC;IAChC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;gFACgF;AAChF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;uFAEmF;IACnF,aAAa,IAAI,MAAM,EAAE,CAAC;IAC1B
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/bakeoff/replay.js
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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) {
|