modelrig 0.3.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 (111) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +79 -14
  3. package/dist/adapters/deepseek.d.ts.map +1 -1
  4. package/dist/adapters/deepseek.js +8 -1
  5. package/dist/adapters/gemini.d.ts +45 -0
  6. package/dist/adapters/gemini.d.ts.map +1 -1
  7. package/dist/adapters/gemini.js +67 -6
  8. package/dist/adapters/grok.d.ts +7 -0
  9. package/dist/adapters/grok.d.ts.map +1 -1
  10. package/dist/adapters/grok.js +23 -1
  11. package/dist/adapters/openai-compat.d.ts +7 -0
  12. package/dist/adapters/openai-compat.d.ts.map +1 -1
  13. package/dist/adapters/openai-compat.js +24 -1
  14. package/dist/adapters/openai.d.ts +7 -0
  15. package/dist/adapters/openai.d.ts.map +1 -1
  16. package/dist/adapters/openai.js +23 -1
  17. package/dist/artifacts/artifact.d.ts +3 -2
  18. package/dist/artifacts/artifact.d.ts.map +1 -1
  19. package/dist/artifacts/artifact.js +11 -4
  20. package/dist/artifacts/run-context.d.ts +21 -2
  21. package/dist/artifacts/run-context.d.ts.map +1 -1
  22. package/dist/artifacts/run-context.js +46 -1
  23. package/dist/bakeoff/replay.d.ts.map +1 -1
  24. package/dist/bakeoff/replay.js +17 -1
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +26 -5
  27. package/dist/config.d.ts +7 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/index.d.ts +21 -15
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +39 -26
  32. package/dist/init/init-cli.d.ts.map +1 -1
  33. package/dist/init/init-cli.js +2 -1
  34. package/dist/init/observe-cli.d.ts.map +1 -1
  35. package/dist/init/observe-cli.js +2 -1
  36. package/dist/registry/select.d.ts +59 -0
  37. package/dist/registry/select.d.ts.map +1 -1
  38. package/dist/registry/select.js +66 -0
  39. package/dist/routes/loader-warnings.d.ts +28 -0
  40. package/dist/routes/loader-warnings.d.ts.map +1 -0
  41. package/dist/routes/loader-warnings.js +56 -0
  42. package/dist/routes/loader.d.ts +1 -11
  43. package/dist/routes/loader.d.ts.map +1 -1
  44. package/dist/routes/loader.js +44 -26
  45. package/dist/routes/sampling-config.d.ts.map +1 -1
  46. package/dist/routes/sampling-config.js +11 -0
  47. package/dist/run/dispatch.d.ts +14 -2
  48. package/dist/run/dispatch.d.ts.map +1 -1
  49. package/dist/run/dispatch.js +31 -2
  50. package/dist/run/ladder-key.d.ts +40 -0
  51. package/dist/run/ladder-key.d.ts.map +1 -0
  52. package/dist/run/ladder-key.js +39 -0
  53. package/dist/run/raw-defaults.d.ts +52 -0
  54. package/dist/run/raw-defaults.d.ts.map +1 -0
  55. package/dist/run/raw-defaults.js +123 -0
  56. package/dist/run/raw-support.d.ts +123 -0
  57. package/dist/run/raw-support.d.ts.map +1 -0
  58. package/dist/run/raw-support.js +150 -0
  59. package/dist/run/raw.d.ts +53 -11
  60. package/dist/run/raw.d.ts.map +1 -1
  61. package/dist/run/raw.js +157 -31
  62. package/dist/run/rawonly.d.ts +60 -0
  63. package/dist/run/rawonly.d.ts.map +1 -0
  64. package/dist/run/rawonly.js +120 -0
  65. package/dist/run/record-attempt.d.ts +14 -1
  66. package/dist/run/record-attempt.d.ts.map +1 -1
  67. package/dist/run/record-attempt.js +43 -2
  68. package/dist/run/resolve.d.ts +21 -1
  69. package/dist/run/resolve.d.ts.map +1 -1
  70. package/dist/run/resolve.js +29 -6
  71. package/dist/run/routing-docs.d.ts +73 -0
  72. package/dist/run/routing-docs.d.ts.map +1 -0
  73. package/dist/run/routing-docs.js +222 -0
  74. package/dist/run/run.d.ts +7 -1
  75. package/dist/run/run.d.ts.map +1 -1
  76. package/dist/run/run.js +38 -55
  77. package/dist/run/variant.d.ts +43 -0
  78. package/dist/run/variant.d.ts.map +1 -0
  79. package/dist/run/variant.js +86 -0
  80. package/dist/savings/emit.d.ts +33 -0
  81. package/dist/savings/emit.d.ts.map +1 -1
  82. package/dist/savings/emit.js +75 -3
  83. package/dist/telemetry/exporter.d.ts.map +1 -1
  84. package/dist/telemetry/exporter.js +6 -0
  85. package/dist/telemetry/ingest-protocol.d.ts +7 -0
  86. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  87. package/dist/telemetry/load-error-report.d.ts.map +1 -1
  88. package/dist/telemetry/load-error-report.js +4 -1
  89. package/dist/telemetry/otel-map.d.ts.map +1 -1
  90. package/dist/telemetry/otel-map.js +8 -0
  91. package/dist/telemetry/store-migrations.d.ts +15 -0
  92. package/dist/telemetry/store-migrations.d.ts.map +1 -0
  93. package/dist/telemetry/store-migrations.js +119 -0
  94. package/dist/telemetry/store.d.ts.map +1 -1
  95. package/dist/telemetry/store.js +26 -93
  96. package/dist/types/failures-teaching.js +1 -1
  97. package/dist/types/provider.d.ts +13 -0
  98. package/dist/types/provider.d.ts.map +1 -1
  99. package/dist/types/route.d.ts +37 -1
  100. package/dist/types/route.d.ts.map +1 -1
  101. package/dist/types/telemetry.d.ts +31 -0
  102. package/dist/types/telemetry.d.ts.map +1 -1
  103. package/dist/validate-cli.d.ts.map +1 -1
  104. package/dist/validate-cli.js +17 -4
  105. package/dist/watch/evaluate.d.ts +27 -1
  106. package/dist/watch/evaluate.d.ts.map +1 -1
  107. package/dist/watch/evaluate.js +13 -0
  108. package/dist/watch/proposals.d.ts +28 -0
  109. package/dist/watch/proposals.d.ts.map +1 -1
  110. package/dist/watch/proposals.js +51 -0
  111. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,275 @@ 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
+
142
+ ## [0.4.0] — 2026-08-22
143
+
144
+ Released with the docs/registry mirror in one R-A ceremony. NOT in this tag:
145
+ the R3 declare-first `trial:` candidate flag (#239) and raw-lane artifact
146
+ steps (G1) — both land in 0.5.0.
147
+
148
+ CE-9 raw-lane multi-provider parity — the `reasoning` and native-grounding knobs
149
+ generalize beyond gemini, a `responseFormat` knob joins the raw lane, and the
150
+ provider×knob support set becomes ONE table (`RAW_KNOB_SUPPORT`) that the runtime
151
+ guard AND the published docs matrix both read, so they can never disagree. Every
152
+ knob stays ADDITIVE: absent ⇒ byte-identical to the 0.3.0 request shape.
153
+
154
+ ### Added — docs
155
+
156
+ - **Routing & reliability page** (`https://modelrig.dev/routing-reliability.html`) —
157
+ the runtime model in one concept page: resolve -> candidate ladder -> validate ->
158
+ repair -> fall-through -> exhaust, what is stored per attempt, and a **code-anchored
159
+ boundary** (`ROUTING_BOUNDARY`, one tripwire test per entry) that lists what does not
160
+ trigger fall-through and what is not built — the build fails if a listed feature ships
161
+ without the entry being updated.
162
+
163
+ ### Fixed
164
+
165
+ - **Failure-teaching links resolve.** `RigFailureError.docsUrl` (and the CLI/console
166
+ error render) now point at `routing-reliability.html#<class>` — one stable heading per
167
+ failure class — instead of the never-built `/docs/failures` path, which 404'd on
168
+ every typed failure.
169
+
170
+ ### Added — `rig.runRaw` / `rig.runRawStream`
171
+
172
+ - **`reasoning` generalizes beyond gemini WITHOUT a per-model allowlist (R1).**
173
+ openai / deepinfra / fireworks forward `level` → `reasoning_effort` verbatim
174
+ and let the provider enforce — a model that does not accept it surfaces a
175
+ CLASSIFIED, teaching failure ("this model does not accept reasoning_effort —
176
+ remove reasoning or pick a reasoning model"), never a cryptic 400 or a silent
177
+ drop. grok forwards a DISCLOSED coarsening (`minimal,low → "low"`;
178
+ `medium,high → "high"`; xAI accepts `low|high` only). gemini stays
179
+ model-gated (thinking-capable models only). deepseek + anthropic FAIL CLOSED
180
+ pre-dispatch with a provider-specific fixHint (deepseek → select the
181
+ `deepseek-reasoner` model; anthropic → extended thinking unwired).
182
+ - **Grok native grounding (R2) — WITHDRAWN before release.** The CE-9 live gate hit xAI's `410 Live search is deprecated`; the replacement `web_search` tool is Responses-API-only, which the grok adapter does not speak yet. grok grounding FAILS CLOSED (matrix `supported: false`, teaching note) until CE-9b lands that path. Original entry, kept for the record: `grounding: { mode: "native" }` on grok →
183
+ xAI Live Search (`search_parameters: { mode: "on" }`). Citations are not
184
+ surfaced on `meta` in v1 (same documented limitation as gemini).
185
+ - **`responseFormat?: "json_object"` (R4).** MIME-shaping on the OpenAI-dialect
186
+ providers (openai/deepseek/grok/deepinfra/fireworks → `response_format: { type:
187
+ "json_object" }`) and gemini (`responseMimeType: "application/json"`, no
188
+ schema). This is syntactic-JSON forcing, NOT schema enforcement — F4 stands:
189
+ no validation, no repair, `meta.validated` stays false. anthropic has no
190
+ equivalent and FAILS CLOSED. DeepSeek's word-"JSON" prompt wrinkle is the
191
+ caller's responsibility on the raw lane (documented, never injected).
192
+ - **DeepInfra/Fireworks prompt-cache hint (R3).** `cache.key` now maps to
193
+ `prompt_cache_key` on the openai-compatible adapter, closing the DeepInfra cost
194
+ gap. (openai and grok already mapped `cache.key → prompt_cache_key` on 0.3.0 —
195
+ that was a docs gap, not a code gap; it is now documented as plain fact.)
196
+
197
+ ### Added — exports (R5/R6)
198
+
199
+ - **`RAW_KNOB_SUPPORT`** (the provider×knob matrix) + **`rawKnobSupport(provider)`**
200
+ (a pure lookup) — a multi-provider dispatcher ASKS which knobs a provider
201
+ honors instead of duplicating our lists. The guard (`assertRawKnobsSupported`)
202
+ and the docs generator both import this table.
203
+ - **`GEMINI_THINKING_CAPABLE_MODELS`** — the thinking-capable set (previously
204
+ un-exported), so customers stop duplicating it (the §3.1 drift complaint).
205
+ - **`coarsenGrokReasoningEffort`** + **`renderRawKnobMatrix`** — the disclosed
206
+ grok coarsening and the docs matrix renderer.
207
+
208
+ ### Docs
209
+
210
+ - A generated provider×knob matrix on the modelrig.dev route-bundles page and in
211
+ `llms.txt`, rendered FROM `RAW_KNOB_SUPPORT` (never a hand-written twin).
212
+ - `caching-lifecycle.md`: the cache directive is provider-neutral — a resource
213
+ HANDLE on gemini, a routing HINT (`prompt_cache_key`, retention on openai) on
214
+ the OpenAI-dialect providers.
215
+
216
+ CE-8 — first-class **runRaw-only construction**. A Lane-B customer who only calls
217
+ `rig.runRaw`/`runRawStream` can now build a rig with no routes at all, so a stray
218
+ route file it never serves cannot down construction — retiring the empty-temp-dir
219
+ workaround.
220
+
221
+ ### Added
222
+
223
+ - **`RigConfig.routesDir` now accepts `null`** (was `string`). With `null`,
224
+ `createRig` SKIPS route loading, the `rig.yaml` manifest, and the
225
+ serveable-candidate gate; `runRaw`/`runRawStream` work exactly as before. The
226
+ `string` path is byte-identical to 0.3.0 (the existing serveability hard-error
227
+ on a zero-serveable route is unchanged and regression-pinned).
228
+ - **`requireRoutesDir(config, surface)`** exported — narrows `routesDir` to a
229
+ string for route-required contexts, throwing the teaching failure on `null`.
230
+ Used by `modelrig-server` and `modelrig-oracle` (both always serve routes).
231
+
232
+ ### Behaviour
233
+
234
+ - On a `routesDir: null` rig, `run`/`runStream`/`runBundle`/`runBundleStream`/
235
+ `replay`/`bakeoff`/`bakeoffFromEvalCases`/`verifySwap` throw `RigFailureError`
236
+ class `invariant_violation` (no new failure class) with the fixHint
237
+ *"this rig was constructed runRaw-only (routesDir: null); give createRig a
238
+ routesDir to serve routes"*. `rig.serveability` is `[]`.
239
+ - `loadConfigFromEnv` stays string-only (no env var disables routes, by design);
240
+ opt out in code: `createRig({ ...loadConfigFromEnv(), routesDir: null })`.
241
+
242
+ ### Docs
243
+
244
+ - README, `docs/migration-playbook.md` (Lane-B seam + C4 checkpoint), and the
245
+ generated `llms.txt` gain the runRaw-only construction snippet, the `apiKey`
246
+ BYOK line on every `runRaw` example, the combined-prompt → `userPrompt` note,
247
+ and the note that the cache-ratio-vs-baseline metric is a REAL-run measurement
248
+ (deferred-to-first-run at the checkpoint), not a probe.
249
+
250
+ R0 — the **per-call telemetry ladder key** (routing-console spec §2.1). Every
251
+ inference row now records which call it belongs to and its position in the
252
+ routing ladder, so a dashboard can reconstruct the exact sequence one `rig.run`
253
+ walked and compute fallback / repair rates.
254
+
255
+ ### Added — telemetry ladder key
256
+
257
+ - **`RunMeta.callId`** — the `call_id` shared by every telemetry row a single
258
+ `run` / `runStream` / `runBundle` / `runRaw` invocation writes (primary, retry,
259
+ repair, grounding-search, tool-call), so a caller can correlate a `RunResult`
260
+ with its ladder in the console. Additive & OPTIONAL — a pre-0.4.0 caller's meta
261
+ omits it.
262
+ - Telemetry rows gain **`call_id`**, **`attempt_seq`** (1-based, monotonic across
263
+ every row of the call), and **`candidate_index`** (position of the dispatched
264
+ candidate in the call's EFFECTIVE declared list — after variant narrowing and
265
+ per-run filters; a repair row carries the repaired candidate's index; the raw
266
+ lane is seq 1 / index 0). All three are OPTIONAL on the wire and nullable in
267
+ storage: an older SDK OMITS them and the payload is byte-identical to before
268
+ R0; a reader treats null as *"recorded by an SDK older than 0.4.0."*
269
+ - OTLP spans gain `modelrig.call_id` / `modelrig.attempt_seq` /
270
+ `modelrig.candidate_index`, emitted only when present.
271
+ - **`CandidateIterator.index`** (read-only getter, public type) — the index of the
272
+ candidate the last `next()` returned (−1 before the first call). Additive for
273
+ callers; an external IMPLEMENTER of the interface must add the member.
274
+ - **`tags.rung`** on every telemetry row (R1 review, F1) — one of `primary` |
275
+ `retry` | `repair` | `search` | `tool_call` | `predispatch_fail`, so the routing
276
+ dashboard and ladder view can tell a real candidate dispatch from a tool-call
277
+ turn or a pre-dispatch failure without column heuristics. Additive (rides
278
+ `tags_json`, no schema change); a pre-0.4.0 row omits it and the rollup falls
279
+ back to `repaired_by` / `provider = "search"`. Public type `TelemetryRung` is
280
+ exported.
281
+
13
282
  ## [0.3.0] — 2026-08-21
14
283
 
15
284
  The `modelrig-oracle` MCP server ships to npm alongside this release (its own
package/README.md CHANGED
@@ -99,27 +99,88 @@ over the route's `cache: auto` stamp, and a non-matching fallback dispatches
99
99
  without it. ModelRig carries the handle and prices the hits — it never creates,
100
100
  refreshes, or deletes the resource, so a long job needs your own TTL heartbeat
101
101
  (see the [caching lifecycle](https://modelrig.dev/caching-lifecycle.html) doc).
102
- The Lane-B `rig.runRaw({ provider, model, …, cache: { key } })` seam takes the
103
- same field.
102
+ The Lane-B `rig.runRaw({ provider, model, apiKey, …, cache: { key } })` seam takes
103
+ the same field.
104
104
 
105
105
  `rig.runRaw` also accepts
106
- three provider knobs, each additive — absent ⇒ byte-identical dispatch:
107
- `grounding: { mode: "native" }` (native provider web grounding, gemini-only in
108
- v1), `serviceTier: "standard" | "flex" | "priority"`, and
109
- `reasoning: { level: "minimal" | "low" | "medium" | "high" }` (gemini-only in
110
- v1, mapped to the thinking level). A declared directive a provider cannot honor
111
- FAILS CLOSED — a thrown `RigFailureError` with class `invariant_violation`,
112
- pre-dispatch (no meter, no provider call) — never a silent drop. A grounded step
113
- that also passes a `cache.key` keeps its cached-token ratio: bake the
114
- `googleSearch` tool into the cache resource at creation (ModelRig sends
115
- `cachedContent` without tools, which the provider requires).
106
+ provider knobs, each additive — absent ⇒ byte-identical dispatch:
107
+ `grounding: { mode: "native" }` (native provider web grounding —
108
+ gemini; grok fail-closed until CE-9b), `serviceTier: "standard" | "flex" | "priority"`,
109
+ `reasoning: { level: "minimal" | "low" | "medium" | "high" }` (reasoning effort¹),
110
+ and `responseFormat: "json_object"` (syntactic-JSON forcing¹ — MIME-shaping, NOT
111
+ schema enforcement, so `meta.validated` stays `false`). A declared directive a
112
+ provider cannot honor FAILS CLOSED — a thrown `RigFailureError` with class
113
+ `invariant_violation`, pre-dispatch (no meter, no provider call) — never a silent
114
+ drop. **Which provider honors which knob is one table** — the runtime guard and
115
+ the published
116
+ [provider × knob matrix](https://modelrig.dev/route-bundles.html#raw-lane-provider-knob-support-rigrunraw)
117
+ are generated from the same `RAW_KNOB_SUPPORT`; ask it in code with
118
+ `rawKnobSupport(provider)` (and `GEMINI_THINKING_CAPABLE_MODELS` is exported so
119
+ you don't duplicate the thinking-model set). A grounded step that also passes a
120
+ `cache.key` keeps its cached-token ratio: bake the `googleSearch` tool into the
121
+ cache resource at creation (ModelRig sends `cachedContent` without tools, which
122
+ the provider requires).
123
+
124
+ _¹ Multi-provider `reasoning` and `responseFormat` ship in
125
+ 0.4.0; on the published package `reasoning`/`grounding` are gemini-only._
126
+
127
+ **runRaw — a BYOK passthrough (Lane B).** When you don't need a route — no
128
+ schema, no validation, no retries — call a `provider/model` directly with your
129
+ own provider key:
130
+
131
+ ```ts
132
+ const rig = createRig(loadConfigFromEnv());
133
+ const result = await rig.runRaw({
134
+ provider: "gemini",
135
+ model: "gemini-3.1-pro",
136
+ apiKey: process.env.GEMINI_API_KEY!, // runRaw is BYOK — pass your provider key
137
+ systemPrompt: "", // one combined prompt? put it ALL in userPrompt, leave systemPrompt empty
138
+ userPrompt: buildPrompt(input),
139
+ });
140
+ ```
141
+
142
+ `runRaw` is BYOK — pass your provider key; ModelRig holds it for the one call and
143
+ never stores it. It records one metered `lane=raw` telemetry row at the
144
+ provider's list price (zero ModelRig margin) and throws a typed `RigFailureError`
145
+ on failure, exactly like `rig.run`.
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
+
160
+ **runRaw-only construction (`routesDir: null`) — 0.4.0+.** A
161
+ Lane-B deployment that ONLY calls `runRaw`/`runRawStream` has no routes. Build it
162
+ with `routesDir: null` and `createRig` skips route loading, the `rig.yaml`
163
+ manifest, and the serveability gate entirely — so a stray route file it never
164
+ calls (or a route whose provider key is absent in this deployment) can no longer
165
+ down construction:
166
+
167
+ ```ts
168
+ const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
169
+ await rig.runRaw({ provider, model, apiKey, systemPrompt, userPrompt });
170
+ ```
171
+
172
+ `loadConfigFromEnv()` always yields a string `routesDir`, so the opt-out is a
173
+ deliberate code-level choice — override it as above; there is no env var that
174
+ disables routes, by design. On such a rig, `rig.run` and every other
175
+ route-dependent surface (`runStream`, `runBundle`, bake-offs, watch, replay,
176
+ `verifySwap`) throws a terminal `invariant_violation` naming the fix.
116
177
 
117
178
  Every attempt is recorded in a local SQLite file (`.modelrig/telemetry.db`)
118
179
  tagged with your `tags`. Failures are typed —
119
180
  `RigFailureError.failure.class` is one of `content_invalid`, `capacity_shed`,
120
- `network`, `refusal`, `cache_invalid`, `timeout`, `budget_exhausted`,
181
+ `network`, `refusal`, `cache_invalid`, `timeout`, `config_auth`, `budget_exhausted`,
121
182
  `invariant_violation` — with a separate retry budget per class, so a flaky
122
- network never spends your content-validation retries.
183
+ network never spends your content-validation retries. What each class means, whether it retries, and how it backs off is on [Routing & reliability](https://modelrig.dev/routing-reliability.html#5-failure-classes).
123
184
 
124
185
  ## CLI
125
186
 
@@ -155,6 +216,10 @@ on the quickstart:
155
216
  stable enough to build on, not frozen.
156
217
  - **`modelrig serve` needs the `modelrig-server` package** — it is not bundled
157
218
  here.
219
+ - **What the ladder does and does not do.** Fall-through is structural and
220
+ infrastructural, never a quality judgment; the full boundary (quality gate,
221
+ raw-lane structure, latency ordering…) is generated and tripwire-tested on
222
+ [Routing & reliability](https://modelrig.dev/routing-reliability.html#10-not-yet-implemented-the-boundary).
158
223
 
159
224
  ## License
160
225
 
@@ -1 +1 @@
1
- {"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../../src/adapters/deepseek.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAIV,cAAc,EACd,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAMhE,OAAO,KAAK,EAAuB,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAMhG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAKD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMhF;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAiB1E;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE;QAAE,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1D,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,oBAAoB,CAAC;CAC7C;AAOD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,oBAAoB,CA2B1F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CA8JpF"}
1
+ {"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../../src/adapters/deepseek.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAIV,cAAc,EACd,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAMhE,OAAO,KAAK,EAAuB,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAMhG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAKD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMhF;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAiB1E;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE;QAAE,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1D,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,oBAAoB,CAAC;CAC7C;AAOD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,oBAAoB,CAkC1F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CA8JpF"}
@@ -62,6 +62,13 @@ function classifyDeepSeekFailure(message) {
62
62
  const JSON_GUARD = "Respond with a single valid JSON object.";
63
63
  function buildDeepSeekParams(model, req) {
64
64
  const wantJson = req.outputSchema !== null;
65
+ // CE-9 R4: the raw lane's `responseFormat: "json_object"` also drives DeepSeek's
66
+ // json_object mode (the flag previously fired ONLY on the routed outputSchema
67
+ // path, which is always null on the raw lane). Absent both ⇒ no key
68
+ // (byte-identical). The JSON_GUARD below stays keyed on the ROUTED path only:
69
+ // on the raw lane the word-"JSON" wrinkle is the caller's prompt responsibility
70
+ // (spec R4), not silently intercepted here.
71
+ const jsonObject = wantJson || req.responseFormat === "json_object";
65
72
  let systemPrompt = req.systemPrompt;
66
73
  if (wantJson && !req.systemPrompt.includes("JSON") && !req.userPrompt.includes("JSON")) {
67
74
  systemPrompt = systemPrompt === "" ? JSON_GUARD : `${systemPrompt}\n\n${JSON_GUARD}`;
@@ -77,7 +84,7 @@ function buildDeepSeekParams(model, req) {
77
84
  stream: true,
78
85
  stream_options: { include_usage: true },
79
86
  ...(0, sampling_1.openAISamplingFields)(req),
80
- ...(wantJson ? { response_format: { type: "json_object" } } : {}),
87
+ ...(jsonObject ? { response_format: { type: "json_object" } } : {}),
81
88
  ...(req.tools !== undefined && req.tools.length > 0
82
89
  ? {
83
90
  tools: (0, tool_mapping_1.toOpenAITools)(req.tools),
@@ -18,6 +18,12 @@ import type { FailureClass } from "../types/failures";
18
18
  export interface GeminiAdapterConfig {
19
19
  readonly apiKey: string;
20
20
  }
21
+ /** Models that accept thinkingConfig — passing it to 2.x models 400s.
22
+ * Ported from the production THINKING_CAPABLE_MODELS set. EXPORTED (CE-9 R6) so
23
+ * a multi-provider customer's dispatcher reads THIS set instead of duplicating
24
+ * it (the §3.1 drift complaint) — the same fact the raw-lane guard uses to keep
25
+ * gemini reasoning model-gated. */
26
+ export declare const GEMINI_THINKING_CAPABLE_MODELS: ReadonlySet<string>;
21
27
  export declare function geminiSupports(feature: CapabilityFlag, model: string): boolean;
22
28
  export interface ClassifiedFailure {
23
29
  readonly cls: FailureClass;
@@ -50,5 +56,44 @@ export interface GeminiShapedRequest {
50
56
  }
51
57
  /** Pure request shaping — everything except the abort signal. */
52
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;
53
97
  export declare function createGeminiAdapter(config: GeminiAdapterConfig): ProviderAdapter;
98
+ export {};
54
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;AAkCD,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,CAgDxF;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"}
@@ -15,17 +15,24 @@
15
15
  * gemini transformation logic is consulted during the port.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.GEMINI_THINKING_CAPABLE_MODELS = void 0;
18
19
  exports.geminiSupports = geminiSupports;
19
20
  exports.classifyGeminiFailure = classifyGeminiFailure;
20
21
  exports.geminiReasoningUnsupported = geminiReasoningUnsupported;
21
22
  exports.buildGeminiRequest = buildGeminiRequest;
23
+ exports.extractGeminiCandidateText = extractGeminiCandidateText;
24
+ exports.geminiCandidateHasFunctionCall = geminiCandidateHasFunctionCall;
25
+ exports.resolveGeminiText = resolveGeminiText;
22
26
  exports.createGeminiAdapter = createGeminiAdapter;
23
27
  const genai_1 = require("@google/genai");
24
28
  const tool_mapping_1 = require("./tool-mapping");
25
29
  const sampling_1 = require("./sampling");
26
30
  /** Models that accept thinkingConfig — passing it to 2.x models 400s.
27
- * Ported from the production THINKING_CAPABLE_MODELS set. */
28
- const THINKING_CAPABLE_MODELS = new Set([
31
+ * Ported from the production THINKING_CAPABLE_MODELS set. EXPORTED (CE-9 R6) so
32
+ * a multi-provider customer's dispatcher reads THIS set instead of duplicating
33
+ * it (the §3.1 drift complaint) — the same fact the raw-lane guard uses to keep
34
+ * gemini reasoning model-gated. */
35
+ exports.GEMINI_THINKING_CAPABLE_MODELS = new Set([
29
36
  "gemini-3.1-pro-preview",
30
37
  "gemini-3.0-pro-preview",
31
38
  "gemini-3.1-flash-preview",
@@ -110,7 +117,7 @@ function classifyGeminiFailure(message) {
110
117
  * reasoning declared, or a thinking-capable model). Pure — unit-tested directly.
111
118
  */
112
119
  function geminiReasoningUnsupported(model, req) {
113
- if (req.reasoning !== undefined && !THINKING_CAPABLE_MODELS.has(model)) {
120
+ if (req.reasoning !== undefined && !exports.GEMINI_THINKING_CAPABLE_MODELS.has(model)) {
114
121
  return {
115
122
  cls: "invariant_violation",
116
123
  fixHint: `reasoning control requires a thinking-capable Gemini model; "${model}" is not one — remove reasoning or choose a thinking-capable model`,
@@ -134,12 +141,18 @@ function buildGeminiRequest(model, req) {
134
141
  maxOutputTokens: (0, sampling_1.effectiveMaxOutputTokens)(req, DEFAULT_MAX_OUTPUT_TOKENS),
135
142
  topP: req.sampling?.topP ?? TOP_P,
136
143
  topK: TOP_K,
137
- ...(THINKING_CAPABLE_MODELS.has(model) ? { thinkingConfig: { thinkingLevel } } : {}),
144
+ ...(exports.GEMINI_THINKING_CAPABLE_MODELS.has(model) ? { thinkingConfig: { thinkingLevel } } : {}),
138
145
  };
139
146
  if (req.outputSchema !== null) {
140
147
  config.responseMimeType = "application/json";
141
148
  config.responseJsonSchema = req.outputSchema;
142
149
  }
150
+ else if (req.responseFormat === "json_object") {
151
+ // CE-9 R4: mime-shaping ONLY — force a JSON mime type WITHOUT a schema. This
152
+ // is the raw lane, so F4 stands: no responseJsonSchema, no validation, no
153
+ // repair, `validated` stays false. Absent ⇒ no key (byte-identical).
154
+ config.responseMimeType = "application/json";
155
+ }
143
156
  // cachedContent excludes systemInstruction and tools — the API rejects
144
157
  // requests setting both (they are baked into the cache at creation time,
145
158
  // including the googleSearch tool for cached grounded steps).
@@ -180,6 +193,43 @@ function extractServedTier(rawServiceTier, trafficType, requested) {
180
193
  return "priority";
181
194
  return requested;
182
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
+ }
183
233
  function createGeminiAdapter(config) {
184
234
  const ai = new genai_1.GoogleGenAI({ apiKey: config.apiKey });
185
235
  return {
@@ -256,9 +306,20 @@ function createGeminiAdapter(config) {
256
306
  if (finishReason === "RECITATION") {
257
307
  return fail("refusal", "Gemini blocked output via recitation filter (finishReason=RECITATION)", undefined, usage);
258
308
  }
259
- 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);
260
317
  if (text.trim().length === 0) {
261
- 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);
262
323
  }
263
324
  if (finishReason === "MAX_TOKENS" && req.outputSchema !== null) {
264
325
  // A truncated structured emission is mid-object and unparseable.
@@ -36,7 +36,14 @@ export interface GrokShapedParams {
36
36
  /** Prompt-cache routing hint (WS-C) — the stable-prefix fingerprint the run
37
37
  * loop stamps; xAI keeps a warm cache keyed to it (writes free today). */
38
38
  readonly prompt_cache_key?: string;
39
+ /** CE-9 R1 — xAI accepts `reasoning_effort: "low" | "high"` ONLY, so the
40
+ * neutral four-level `level` is coarsened by `coarsenGrokReasoningEffort`
41
+ * (a DISCLOSED, lossy mapping: `minimal,low → "low"`; `medium,high → "high"`).
42
+ * Present only when the caller declared `reasoning`. */
43
+ readonly reasoning_effort?: "low" | "high";
39
44
  readonly response_format?: {
45
+ readonly type: "json_object";
46
+ } | {
40
47
  readonly type: "json_schema";
41
48
  readonly json_schema: {
42
49
  readonly name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"grok.d.ts","sourceRoot":"","sources":["../../src/adapters/grok.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAIV,cAAc,EACd,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAMhE,OAAO,KAAK,EAAuB,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAMhG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAKD,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAc7E;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAsBtE;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE;QAAE,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1D,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;8EAC0E;IAC1E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE;QACzB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;QAC7B,QAAQ,CAAC,WAAW,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;SAAE,CAAC;KACjG,CAAC;IACF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,oBAAoB,CAAC;CAC7C;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,gBAAgB,CAsClF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,eAAe,CAgK5E"}
1
+ {"version":3,"file":"grok.d.ts","sourceRoot":"","sources":["../../src/adapters/grok.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAIV,cAAc,EACd,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAMhE,OAAO,KAAK,EAAuB,cAAc,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAOhG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAKD,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAc7E;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CA+BtE;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE;QAAE,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAA;KAAE,CAAC;IAC1D,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;8EAC0E;IAC1E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;4DAGwD;IACxD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3C,QAAQ,CAAC,eAAe,CAAC,EACrB;QAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;KAAE,GAChC;QACE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;QAC7B,QAAQ,CAAC,WAAW,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;SAAE,CAAC;KACjG,CAAC;IACN,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,oBAAoB,CAAC;CAC7C;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,gBAAgB,CAmDlF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,eAAe,CAgK5E"}