modelrig 0.9.0 → 0.10.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 (223) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +51 -27
  3. package/data/PROVENANCE.md +14 -0
  4. package/data/pricing-map.json +26 -0
  5. package/dist/adapters/attempt-meta-map.d.ts +8 -1
  6. package/dist/adapters/attempt-meta-map.d.ts.map +1 -1
  7. package/dist/adapters/attempt-meta-map.js +4 -0
  8. package/dist/adapters/build-adapters.js +2 -2
  9. package/dist/adapters/gemini.d.ts +28 -1
  10. package/dist/adapters/gemini.d.ts.map +1 -1
  11. package/dist/adapters/gemini.js +38 -9
  12. package/dist/adapters/grok.d.ts.map +1 -1
  13. package/dist/adapters/grok.js +53 -4
  14. package/dist/adapters/openai-compat.d.ts +8 -2
  15. package/dist/adapters/openai-compat.d.ts.map +1 -1
  16. package/dist/adapters/openai-compat.js +8 -1
  17. package/dist/adapters/openai-stream.d.ts +10 -2
  18. package/dist/adapters/openai-stream.d.ts.map +1 -1
  19. package/dist/adapters/openai-stream.js +5 -1
  20. package/dist/adapters/openai.d.ts +10 -1
  21. package/dist/adapters/openai.d.ts.map +1 -1
  22. package/dist/adapters/openai.js +57 -7
  23. package/dist/adapters/responses-api.d.ts +61 -0
  24. package/dist/adapters/responses-api.d.ts.map +1 -0
  25. package/dist/adapters/responses-api.js +124 -0
  26. package/dist/artifacts/artifact.d.ts +20 -0
  27. package/dist/artifacts/artifact.d.ts.map +1 -1
  28. package/dist/artifacts/artifact.js +207 -171
  29. package/dist/artifacts/index.d.ts +3 -1
  30. package/dist/artifacts/index.d.ts.map +1 -1
  31. package/dist/artifacts/index.js +8 -1
  32. package/dist/bakeoff/groundedness.d.ts +58 -0
  33. package/dist/bakeoff/groundedness.d.ts.map +1 -0
  34. package/dist/bakeoff/groundedness.js +165 -0
  35. package/dist/bakeoff/quality.d.ts +14 -1
  36. package/dist/bakeoff/quality.d.ts.map +1 -1
  37. package/dist/bakeoff/quality.js +10 -0
  38. package/dist/bakeoff/replay.d.ts.map +1 -1
  39. package/dist/bakeoff/replay.js +4 -0
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +8 -0
  42. package/dist/config.d.ts +31 -0
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +23 -0
  45. package/dist/doctor-cli.d.ts +7 -0
  46. package/dist/doctor-cli.d.ts.map +1 -0
  47. package/dist/doctor-cli.js +67 -0
  48. package/dist/doctor-preflight.d.ts +52 -0
  49. package/dist/doctor-preflight.d.ts.map +1 -0
  50. package/dist/doctor-preflight.js +112 -0
  51. package/dist/feedback/grade.d.ts +8 -1
  52. package/dist/feedback/grade.d.ts.map +1 -1
  53. package/dist/feedback/grade.js +4 -0
  54. package/dist/gateway/http-protocol.d.ts +19 -0
  55. package/dist/gateway/http-protocol.d.ts.map +1 -1
  56. package/dist/gateway/serve-client.d.ts +99 -0
  57. package/dist/gateway/serve-client.d.ts.map +1 -0
  58. package/dist/gateway/serve-client.js +360 -0
  59. package/dist/index.d.ts +25 -3
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +62 -8
  62. package/dist/prompts/manifest.d.ts +128 -0
  63. package/dist/prompts/manifest.d.ts.map +1 -0
  64. package/dist/prompts/manifest.js +214 -0
  65. package/dist/prompts/prompt-fetch.d.ts +84 -0
  66. package/dist/prompts/prompt-fetch.d.ts.map +1 -0
  67. package/dist/prompts/prompt-fetch.js +120 -0
  68. package/dist/prompts/prompt-resolve.d.ts +76 -0
  69. package/dist/prompts/prompt-resolve.d.ts.map +1 -0
  70. package/dist/prompts/prompt-resolve.js +93 -0
  71. package/dist/prompts/slot-resolver.d.ts +91 -0
  72. package/dist/prompts/slot-resolver.d.ts.map +1 -0
  73. package/dist/prompts/slot-resolver.js +135 -0
  74. package/dist/prompts/tools-register.d.ts +60 -0
  75. package/dist/prompts/tools-register.d.ts.map +1 -0
  76. package/dist/prompts/tools-register.js +48 -0
  77. package/dist/prompts/version-hash.d.ts +29 -0
  78. package/dist/prompts/version-hash.d.ts.map +1 -0
  79. package/dist/prompts/version-hash.js +54 -0
  80. package/dist/prompts-export-cli.d.ts +21 -0
  81. package/dist/prompts-export-cli.d.ts.map +1 -0
  82. package/dist/prompts-export-cli.js +130 -0
  83. package/dist/registry/cache-policy.d.ts.map +1 -1
  84. package/dist/registry/cache-policy.js +22 -0
  85. package/dist/registry/pricing-shape.d.ts +38 -8
  86. package/dist/registry/pricing-shape.d.ts.map +1 -1
  87. package/dist/registry/pricing-shape.js +40 -12
  88. package/dist/registry/pricing.d.ts +6 -5
  89. package/dist/registry/pricing.d.ts.map +1 -1
  90. package/dist/registry/pricing.js +9 -5
  91. package/dist/routes/destination-loader.d.ts +16 -0
  92. package/dist/routes/destination-loader.d.ts.map +1 -0
  93. package/dist/routes/destination-loader.js +29 -0
  94. package/dist/routes/draft.d.ts +7 -0
  95. package/dist/routes/draft.d.ts.map +1 -1
  96. package/dist/routes/draft.js +12 -0
  97. package/dist/routes/loader.d.ts.map +1 -1
  98. package/dist/routes/loader.js +27 -54
  99. package/dist/routes/output-loader.d.ts +42 -0
  100. package/dist/routes/output-loader.d.ts.map +1 -0
  101. package/dist/routes/output-loader.js +112 -0
  102. package/dist/routes/prompt-loader.d.ts +49 -0
  103. package/dist/routes/prompt-loader.d.ts.map +1 -0
  104. package/dist/routes/prompt-loader.js +113 -0
  105. package/dist/routes/resolved-bundle.d.ts +19 -0
  106. package/dist/routes/resolved-bundle.d.ts.map +1 -1
  107. package/dist/routes/resolved-bundle.js +24 -0
  108. package/dist/routes/screen-loader.d.ts +1 -1
  109. package/dist/routes/screen-loader.d.ts.map +1 -1
  110. package/dist/routes/screen-loader.js +8 -1
  111. package/dist/run/accept.d.ts +3 -0
  112. package/dist/run/accept.d.ts.map +1 -1
  113. package/dist/run/accept.js +1 -0
  114. package/dist/run/cost.d.ts +31 -2
  115. package/dist/run/cost.d.ts.map +1 -1
  116. package/dist/run/cost.js +57 -2
  117. package/dist/run/destination.d.ts +39 -0
  118. package/dist/run/destination.d.ts.map +1 -0
  119. package/dist/run/destination.js +45 -0
  120. package/dist/run/dispatch.d.ts +25 -3
  121. package/dist/run/dispatch.d.ts.map +1 -1
  122. package/dist/run/dispatch.js +26 -4
  123. package/dist/run/raw-defaults.d.ts.map +1 -1
  124. package/dist/run/raw-defaults.js +6 -1
  125. package/dist/run/raw-finalize.d.ts +50 -0
  126. package/dist/run/raw-finalize.d.ts.map +1 -0
  127. package/dist/run/raw-finalize.js +266 -0
  128. package/dist/run/raw-knobs.d.ts +36 -0
  129. package/dist/run/raw-knobs.d.ts.map +1 -0
  130. package/dist/run/raw-knobs.js +99 -0
  131. package/dist/run/raw-request.d.ts +24 -0
  132. package/dist/run/raw-request.d.ts.map +1 -0
  133. package/dist/run/raw-request.js +69 -0
  134. package/dist/run/raw-schema.d.ts +76 -0
  135. package/dist/run/raw-schema.d.ts.map +1 -0
  136. package/dist/run/raw-schema.js +166 -0
  137. package/dist/run/raw-search-cost.d.ts +48 -0
  138. package/dist/run/raw-search-cost.d.ts.map +1 -0
  139. package/dist/run/raw-search-cost.js +78 -0
  140. package/dist/run/raw-support.d.ts +64 -6
  141. package/dist/run/raw-support.d.ts.map +1 -1
  142. package/dist/run/raw-support.js +84 -11
  143. package/dist/run/raw.d.ts +25 -26
  144. package/dist/run/raw.d.ts.map +1 -1
  145. package/dist/run/raw.js +30 -351
  146. package/dist/run/record-attempt.d.ts +8 -0
  147. package/dist/run/record-attempt.d.ts.map +1 -1
  148. package/dist/run/record-attempt.js +26 -12
  149. package/dist/run/repair-attempt.d.ts +68 -0
  150. package/dist/run/repair-attempt.d.ts.map +1 -0
  151. package/dist/run/repair-attempt.js +180 -0
  152. package/dist/run/resolve-destination.d.ts +49 -0
  153. package/dist/run/resolve-destination.d.ts.map +1 -0
  154. package/dist/run/resolve-destination.js +76 -0
  155. package/dist/run/routing-docs.d.ts.map +1 -1
  156. package/dist/run/routing-docs.js +9 -4
  157. package/dist/run/run.d.ts +30 -1
  158. package/dist/run/run.d.ts.map +1 -1
  159. package/dist/run/run.js +75 -148
  160. package/dist/run/telemetry-row.d.ts +92 -0
  161. package/dist/run/telemetry-row.d.ts.map +1 -0
  162. package/dist/run/telemetry-row.js +81 -0
  163. package/dist/run/validate.d.ts +12 -1
  164. package/dist/run/validate.d.ts.map +1 -1
  165. package/dist/run/validate.js +31 -0
  166. package/dist/run/web-search-pricing.d.ts +43 -0
  167. package/dist/run/web-search-pricing.d.ts.map +1 -0
  168. package/dist/run/web-search-pricing.js +56 -0
  169. package/dist/settings/grading-guide.d.ts +27 -1
  170. package/dist/settings/grading-guide.d.ts.map +1 -1
  171. package/dist/status.d.ts +6 -0
  172. package/dist/status.d.ts.map +1 -1
  173. package/dist/status.js +4 -1
  174. package/dist/telemetry/artifact-store.d.ts.map +1 -1
  175. package/dist/telemetry/artifact-store.js +11 -0
  176. package/dist/telemetry/attempt-meta.d.ts +55 -1
  177. package/dist/telemetry/attempt-meta.d.ts.map +1 -1
  178. package/dist/telemetry/attempt-meta.js +24 -1
  179. package/dist/telemetry/exporter-setup.d.ts +2 -0
  180. package/dist/telemetry/exporter-setup.d.ts.map +1 -1
  181. package/dist/telemetry/exporter-setup.js +6 -1
  182. package/dist/telemetry/grade-mappers.d.ts +5 -2
  183. package/dist/telemetry/grade-mappers.d.ts.map +1 -1
  184. package/dist/telemetry/grade-mappers.js +8 -2
  185. package/dist/telemetry/grades-sql.d.ts +4 -1
  186. package/dist/telemetry/grades-sql.d.ts.map +1 -1
  187. package/dist/telemetry/grades-sql.js +26 -3
  188. package/dist/telemetry/ingest-protocol.d.ts +26 -2
  189. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  190. package/dist/telemetry/ingest-protocol.js +32 -29
  191. package/dist/telemetry/org-identity.d.ts +44 -0
  192. package/dist/telemetry/org-identity.d.ts.map +1 -0
  193. package/dist/telemetry/org-identity.js +98 -0
  194. package/dist/telemetry/route-registration.d.ts +48 -0
  195. package/dist/telemetry/route-registration.d.ts.map +1 -0
  196. package/dist/telemetry/route-registration.js +77 -0
  197. package/dist/telemetry/sinks.d.ts +10 -1
  198. package/dist/telemetry/sinks.d.ts.map +1 -1
  199. package/dist/telemetry/sinks.js +52 -6
  200. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  201. package/dist/telemetry/store-migrations.js +13 -0
  202. package/dist/tenancy/api-key-format.d.ts +16 -1
  203. package/dist/tenancy/api-key-format.d.ts.map +1 -1
  204. package/dist/tenancy/api-key-format.js +17 -0
  205. package/dist/types/provider.d.ts +20 -3
  206. package/dist/types/provider.d.ts.map +1 -1
  207. package/dist/types/provider.js +1 -0
  208. package/dist/types/route.d.ts +76 -0
  209. package/dist/types/route.d.ts.map +1 -1
  210. package/dist/types/telemetry.d.ts +30 -2
  211. package/dist/types/telemetry.d.ts.map +1 -1
  212. package/docs/agent-skill/SKILL.md +73 -5
  213. package/docs/instrumentation.md +81 -36
  214. package/docs/llms.txt +95 -16
  215. package/docs/managed-serving.md +150 -0
  216. package/docs/mcp-oracle.md +75 -2
  217. package/docs/migration-playbook.md +5 -4
  218. package/docs/quickstart.md +169 -83
  219. package/package.json +3 -2
  220. package/registry/registry.json +82 -0
  221. package/dist/feedback/feedback.d.ts +0 -35
  222. package/dist/feedback/feedback.d.ts.map +0 -1
  223. package/dist/feedback/feedback.js +0 -39
package/CHANGELOG.md CHANGED
@@ -10,8 +10,187 @@ published package diverge.
10
10
  This project adheres to [Semantic Versioning](https://semver.org/). Dates are
11
11
  UTC.
12
12
 
13
+ Every `[Unreleased]` entry adds its symbols to `release-manifest.json` (rule P17
14
+ in `docs/ModelRig/development-rules.md`; enforced by `release-parity.test.ts`).
15
+
13
16
  ## [Unreleased]
14
17
 
18
+ ## [0.10.0] — 2026-10-19
19
+
20
+ **Behaviour change (raw lane) — migration note.** A `rig.runRaw` call on Gemini
21
+ that passed a non-empty `systemPrompt` alongside `cache.key` used to have the
22
+ system prompt silently DROPPED (the adapter could not place it beside
23
+ `cachedContent`). It is now delivered in the user turn — ahead of `userPrompt`,
24
+ separated by a blank line — and is therefore billed as input tokens on every
25
+ such call. If the cache resource already holds that text, pass
26
+ `systemPrompt: ""` to keep the request byte-identical to before (apps/api's raw
27
+ lane already does). A call with an empty `systemPrompt`, or without a handle, is
28
+ unchanged.
29
+
30
+ **Per-call user turn (`prompt.user`) + the Gemini cache-handle delivery rule**
31
+ (GitLab issue #5, R1). A route may now declare `prompt.user: <path>` — a second
32
+ template rendered with the same `variables`, capability conditionals and
33
+ `x-trust` rules as `prompt.system` — whose rendered text **is** the user turn of
34
+ every primary attempt (replacing the fixed trigger sentence) and rides ahead of
35
+ the directive on a same-model repair. `prompt.system` may be omitted when
36
+ `prompt.user` is declared. Absent ⇒ the fixed trigger ⇒ byte-identical
37
+ behaviour and an unchanged `bundle_sha`; declaring it changes the `bundle_sha`,
38
+ and `userTemplate` rides the resolved (mirror) bundle. **Cache rule:** a Gemini
39
+ dispatch carrying a cache handle (`cachedContent`) may not carry
40
+ `systemInstruction`; before this release the adapter silently DROPPED the
41
+ rendered template on such a dispatch (the `caching-lifecycle.md` "you do not
42
+ need to empty your template" reassurance was a trap). Now **with a handle,
43
+ everything rendered is delivered as the user turn** — the system prompt (if
44
+ any), a blank line, the user turn — on both lanes, and nothing is dropped. Every
45
+ Gemini attempt that reached the provider stamps `inferences.meta.prompt_delivery`
46
+ (`system` | `user` | `user+cache`, exported as `PromptDelivery`) so the control
47
+ plane can prove what was sent; a transport-error row (no response) carries none.
48
+ The loader warns on a `cache:` route with a Gemini candidate, a non-empty
49
+ `prompt.system` and no `prompt.user`. Docs: `caching-lifecycle.md` → *A cache
50
+ handle changes where the template is delivered*; `route-bundles.md` → *Per-call
51
+ user turn*.
52
+
53
+ **Prose on the route lane — `output: text`** (GitLab issue #1; incident
54
+ 2026-10-11). A route can now declare that its output is prose/markdown rather
55
+ than JSON: `output: text` in the bundle YAML (or `output: { kind: text,
56
+ min_chars: N }` for the one declared length check). The response is returned
57
+ **verbatim** as `result.output` (a string): no `JSON.parse`, no schema, no
58
+ json-mode coaching, and the JSON repair rung is never entered — so a prose route
59
+ is never rejected `content_invalid` for not being JSON. The default is unchanged
60
+ (`json`; `output: json` may be written explicitly and is identical to absent):
61
+ every existing route behaves byte-identically and keeps its `bundle_sha`. A
62
+ text route's `bundle_sha` differs from its JSON twin, and `outputKind` rides the
63
+ resolved bundle, so the hosted mirror lane and call-config pins honour the prose
64
+ contract too. The loader refuses `output: text` combined with `schema:`,
65
+ `require: schema_conformant`, or `repair:` with a teaching error. A JSON route
66
+ that receives prose keeps today's `content_invalid` and now carries a
67
+ `fixHint` pointing at `output: text`. Every dispatched route-lane attempt stamps
68
+ `inferences.meta.output_kind` (`json` | `text`, the contract it was held to);
69
+ raw-lane rows carry no such key. `OutputKind` is exported. Docs:
70
+ `route-bundles.md` → *Prose and markdown output*.
71
+
72
+ **`responseSchema` on the raw lane (R2, GitLab #6).** `rig.runRaw` accepts a
73
+ JSON Schema object and forwards it as the provider's *native* structured-output
74
+ directive — gemini `responseJsonSchema` + JSON mime, openai/grok strict
75
+ `json_schema` (a non-object root is warned pre-dispatch), anthropic a forced
76
+ tool call — then validates the returned text client-side with the same Ajv the
77
+ routed lane uses. A miss is a **billed, terminal `content_invalid`**: one
78
+ `lane: "raw"` row with that class and a thrown `RigFailureError` whose `fixHint`
79
+ names the first errors; nothing is repaired, retried, or fallen through, so
80
+ `meta.validated` stays `false` (boundary B2 is narrowed from "no structure" to
81
+ "no repair"). Providers without a native schema path on this lane — deepseek and
82
+ the OpenAI-compat hosts (deepinfra / fireworks / zai) — fail closed
83
+ pre-dispatch, as does a schema that is not a plain object, does not compile, or
84
+ is declared alongside `responseFormat`. The row's `meta.schema_sha` is
85
+ `sha256:` of the canonical (sorted-key) schema JSON, so declared-vs-served drift
86
+ is queryable; the schema joins `input_hash` exactly as a route schema does. The
87
+ knob is one more cell of `RAW_KNOB_SUPPORT` — the guard, the published matrix,
88
+ `RAW_DEFAULTS`, and `llms.txt` all derive from it. Zero DDL (`meta` is jsonb).
89
+
90
+ **The generated tables now ship in the committed docs.** `route-bundles.md`
91
+ (raw-lane defaults, provider × knob matrix) and `routing-reliability.md`
92
+ (failure classes, the boundary) — and the nine use-case template pages — used
93
+ to carry a bare HTML-comment marker that only the modelrig.dev build filled in,
94
+ so GitLab, the OSS mirror and any offline agent reading the repo saw an empty
95
+ line where the table belonged. Every marker is now a fence around its current
96
+ render; the docs build refreshes the fenced body from the same renderer on every
97
+ run (the `docs/llms.txt` rule applied to the docs) and fails on a bare
98
+ placeholder, and a tripwire test asserts each committed body is byte-identical
99
+ to its renderer. Regenerate with `tsx scripts/build-docs-site.ts` and commit.
100
+
101
+ **Cost rows are priced at the SERVED tier, and Gemini has a `flex` tier
102
+ (GitLab #2).** `attemptCost` / `costComponents` now read `usage.servedTier`
103
+ (both lanes, every attempt — serving, failed, repair) and price `flex` /
104
+ `priority` from the model's published `pricing_v2` tiers instead of always
105
+ billing the standard base. `ServingTiers` gains `flex`, and a tier multiplier
106
+ may now be **per component** (`{ input: 0.5, output: 0.5, cache_read: 1.0 }`
107
+ beside the scalar form; an absent component is exempt, i.e. the standard
108
+ rate) — Google's flex/batch halve input + output but leave cache read "Same as
109
+ Standard", which one scalar cannot say. `gemini/gemini-3.1-pro-preview` (and
110
+ `-customtools`) carry `tiers.flex`, `tiers.priority: 1.8` and the >200k
111
+ context band, verified against the Gemini pricing page 2026-10-11. On the
112
+ InferWealth flex batch this recorded $1.11 where Google's list is ≈$0.68
113
+ (1,357,697 in / 1,289,855 cached / 59,625 out: in $0.068 + cache $0.258 + out
114
+ $0.358). A model that publishes no rate for a served tier still bills at base
115
+ (fail-closed, never a guessed multiplier); `standard` rows are byte-identical.
116
+ Known gap, stated: the >200k context band is **listed, not applied** — rows
117
+ whose prompt exceeds 200k tokens still price at the ≤200k rates and under-state
118
+ Google's list (follow-up issue). `meta.served_tier_source` (`provider` |
119
+ `requested_fallback`) now says whether the served tier came from the provider's
120
+ response or fell back to the requested tier, so a fallback-priced row is
121
+ identifiable. **Scope of the claim:** attempt cost rows (`cost_estimate_usd`)
122
+ and `meta.cost` price at the served tier; the server's managed re-pricing
123
+ (`modelrig-server` `compute-fee.ts` `listPriceCents`, the `/costs` rollup and
124
+ settlement draw) still prices at base — that money path is untouched here and
125
+ tracked for the next money-path MR. Zero DDL.
126
+
127
+ **Managed serving is released at the SDK/capability layer** (`destination:
128
+ modelrig-served`). The serving cells of the capability table and the published
129
+ `llms.txt` matrix drop their UNRELEASED tags; `config.managedServing` is
130
+ unchanged (set in code, default `false`, so an absent/`direct` route stays
131
+ byte-identical). New customer page `docs/managed-serving.md` — declare the
132
+ destination, enable it in code, mint a `serve`-scoped key, opt the organization
133
+ in (Account settings → Managed serving, or `PATCH /v1/orgs/:org/settings
134
+ {managed_serving_enabled}`), fund the prepaid balance, and what each 401/402/
135
+ 403/502/503 refusal teaches; `route-bundles.md` documents the `destination`
136
+ field. The SDK side of the hosted keyless transport (K1/K1b) is in this package
137
+ (it is not in 0.9.0, where a served run uses the local seam). Hosted serving
138
+ also needs the org opt-in, a `serve`-scoped key and a funded prepaid balance.
139
+
140
+ Routes are visible — and pinnable — before their first call (RV-1). A rig
141
+ created with a `routesDir` and a control-plane export now registers each route
142
+ the moment `createRig()` runs: its metadata (route, version, candidates,
143
+ capture flag) plus its declared resolved bundle and `bundle_sha`, through the
144
+ same `/v1/ingest` call telemetry uses (the `ingest` scope is enough). The
145
+ hosted ingest now stores that bundle (it used to drop it in api mode), so a
146
+ call-config pin passes the eligibility gate instead of failing with "no
147
+ servable bundle". The bundle is route configuration (the authored prompt
148
+ template, never per-call values); it is stored even for a posture-`off` org.
149
+ A prompt version promoted in ModelRig keeps precedence over the YAML template.
150
+ Registration is fire-and-forget and fail-open: never awaited, never throws,
151
+ warns once on failure, and never changes what this process serves. The
152
+ exporter's first-flush routes mirror (which now also lands its bundle in api
153
+ mode) remains the retry path. Pair it with `GET /v1/routes/:route/call-config?include=effective`
154
+ (or MCP `get_call_config`) to see what a route will serve and why — pin vs your
155
+ YAML default — and whether recent calls drifted from a pin. A bundle that
156
+ arrives over `/v1/ingest` is stored as route configuration only: pins, the
157
+ effective view and bake-offs use it, but the hosted gateway keeps serving only
158
+ bundles from a trusted sync, so an `ingest` key cannot change what the gateway
159
+ serves. `stampBatch` now writes routes with and without a bundle as two
160
+ separate writes (a bulk insert needs every row to carry the same columns).
161
+
162
+ Silent telemetry drops are now loud — **no configuration required**. When an
163
+ ingest POST is rejected with `401`/`403` (a wrong, revoked, or wrong-org key),
164
+ the api sink emits ONE loud, legible warning — `[modelrig] AUTH FAILED —
165
+ telemetry is being DROPPED (401 at <host>, key …5WMM) …` — naming the host and
166
+ the redacted key and pointing at `app.modelrig.ai` and `modelrig doctor`. It
167
+ fires once per failure streak (a later successful send re-arms it), rides the
168
+ POST that is already failing (no extra request), and never prints the key or the
169
+ response body. This is the automatic safety net — the one key a customer already
170
+ sets is enough; there is no boot-preflight env var.
171
+
172
+ `modelrig doctor` — a new CLI command that confirms, in one line, which org your
173
+ `MODELRIG_API_KEY` resolves to (via the server's `GET /v1/whoami`): prints
174
+ `key …5WMM → org «YourOrg» ✓` on success, a loud `AUTH FAILED (401)` line on a
175
+ rejected key, and exits non-zero when the key is bad — so CI / a deploy step /
176
+ setup can gate on it before any traffic runs. Never prints the key (last-four
177
+ only).
178
+
179
+ **Which org am I exporting to? — the wrong-org signal (REL-1, M5).** A valid key
180
+ minted in the wrong org used to fail silently: every ingest POST is a 200 and the
181
+ runs land in another org's console. Now, on the first successful flush in each
182
+ process, the api sink calls `GET /v1/whoami` once (the same helper `modelrig
183
+ doctor` uses) and logs `[modelrig] exporting to org «Name» (slug) as key «name»
184
+ rig_sk_…last4`. It runs in the background, never blocks or drops a batch, and is
185
+ silent if whoami fails (a rejected key already has its own `AUTH FAILED` line).
186
+ New code option `createRig({ expectOrg: "<slug>" })`: when the key resolves to a
187
+ different org it logs `[modelrig] ORG MISMATCH: key resolves to «a», expectOrg is
188
+ «b» — telemetry is landing in «a»` loudly on every flush. The whoami call is
189
+ aborted after 5 s, so it never holds a process open. It never throws and never drops telemetry. It is a code option, not an
190
+ env var (P15). `modelrig doctor --expect-org <slug>` exits 1 and prints both
191
+ slugs on a mismatch, so a deploy or CI step can gate on it. Docs:
192
+ `instrumentation.md` → *Which org am I exporting to?*
193
+
15
194
  ## [0.9.0] — 2026-09-01
16
195
 
17
196
  **Breaking** (the reason this is 0.9.0, not 0.8.2): the temporary `rig.feedback` /
package/README.md CHANGED
@@ -89,10 +89,11 @@ import { createRig, loadConfigFromEnv } from "modelrig";
89
89
  const rig = createRig(loadConfigFromEnv()); // keys from GEMINI_API_KEY etc.
90
90
  const result = await rig.run("example.support_summarize", {
91
91
  input: { ticket, product, priorContext }, // template variables
92
- tags: { run_id, step: "summarize" },
93
- budget: { envelope: run_id }, // hard-stop cost envelope
92
+ tags: { subject: customer, feature: "summarize" },
93
+ budget: { envelope: ticketId }, // hard-stop cost envelope
94
94
  });
95
- // result.output — schema-validated JSON (ajv), regardless of serving model
95
+ // result.output — schema-validated JSON (ajv), regardless of serving model;
96
+ // the verbatim string on an `output: text` (prose) route
96
97
  // result.meta — servedCandidate, tokens (in/out/cached), cost, tiers,
97
98
  // validation status, attempts by failure class
98
99
  rig.close();
@@ -123,15 +124,23 @@ without it. ModelRig carries the handle and prices the hits — it never creates
123
124
  refreshes, or deletes the resource, so a long job needs your own TTL heartbeat
124
125
  (see the [caching lifecycle](https://modelrig.dev/caching-lifecycle.html) doc).
125
126
  The Lane-B `rig.runRaw({ provider, model, apiKey, …, cache: { key } })` seam takes
126
- the same field.
127
+ the same field. A Gemini handle forbids a system instruction on the request, so
128
+ **with a handle everything the route rendered is delivered as the user turn**
129
+ (never dropped); declare the per-call delta in a `prompt.user` template and let
130
+ the cache hold the shared prefix — every attempt stamps `meta.prompt_delivery`
131
+ (`system` | `user` | `user+cache`) so you can prove what was sent.
127
132
 
128
133
  `rig.runRaw` also accepts
129
134
  provider knobs, each additive — absent ⇒ byte-identical dispatch:
130
135
  `grounding: { mode: "native" }` (native provider web grounding —
131
136
  gemini; grok fail-closed until CE-9b), `serviceTier: "standard" | "flex" | "priority"`,
132
137
  `reasoning: { level: "minimal" | "low" | "medium" | "high" }` (reasoning effort¹),
133
- and `responseFormat: "json_object"` (syntactic-JSON forcing¹ — MIME-shaping, NOT
134
- schema enforcement, so `meta.validated` stays `false`). A declared directive a
138
+ `responseFormat: "json_object"` (syntactic-JSON forcing¹ — MIME-shaping, NOT
139
+ schema enforcement, so `meta.validated` stays `false`), and `responseSchema`
140
+ (a JSON Schema object² — forwarded as the provider's native structured-output
141
+ directive AND Ajv-checked client-side; a miss is a billed, terminal
142
+ `content_invalid` — never repaired or retried — and the row carries
143
+ `meta.schema_sha`). A declared directive a
135
144
  provider cannot honor FAILS CLOSED — a thrown `RigFailureError` with class
136
145
  `invariant_violation`, pre-dispatch (no meter, no provider call) — never a silent
137
146
  drop. **Which provider honors which knob is one table** — the runtime guard and
@@ -144,8 +153,10 @@ you don't duplicate the thinking-model set). A grounded step that also passes a
144
153
  cache resource at creation (ModelRig sends `cachedContent` without tools, which
145
154
  the provider requires).
146
155
 
147
- _¹ Multi-provider `reasoning` and `responseFormat` ship in
148
- 0.4.0; on the published package `reasoning`/`grounding` are gemini-only._
156
+ _¹ Which provider honors `reasoning`, `grounding` and `responseFormat` is the
157
+ knob matrix — generated from the same table the runtime guard reads._
158
+ _² `responseSchema` needs 0.10.0 or newer (gemini / openai / grok / anthropic;
159
+ deepseek and the compat hosts fail closed) — see the knob matrix._
149
160
 
150
161
  **runRaw — a BYOK passthrough (Lane B).** When you don't need a route — no
151
162
  schema, no validation, no retries — call a `provider/model` directly with your
@@ -248,6 +259,10 @@ lines up with the console's built-ins:
248
259
  | `session` | the conversation/session the call belongs to |
249
260
  | `cost_center` | the team/budget to attribute spend to |
250
261
 
262
+ These five are the keys **you** set. `run_id` and `step` are stamped by the SDK
263
+ inside a run (don't set `run_id` yourself), and ModelRig owns a few more; the one
264
+ complete list is in the [instrumentation guide](./docs/instrumentation.md#reserved-tag-keys-the-one-list).
265
+
251
266
  `modelrig validate` warns on near-misses — a declared `rig.yaml` dimension named
252
267
  `customer`, `tenant`, `user_id`, `team`, … is nudged toward the reserved key it
253
268
  resembles. They stay ordinary tags; nothing is required.
@@ -260,33 +275,40 @@ one **step** per model-call family, and **save the work products** as artifacts;
260
275
  the console's **Runs** tab then shows the run → step → artifact chain you click
261
276
  through, and `/projects` groups it by your tags.
262
277
 
278
+ <!-- BLOCK:RUN_WRAP -->
279
+ **Wrap one pipeline execution in a run.** Every pipeline execution is a run. Wrap each execution in `run.scope` (or `run.start` / `run.end`), name one step per model call with `stepKey`, `artifact.save` the work products, and pass `environment` so the Env column on /runs is filled. Inside the run the SDK stamps `run_id` and `step` on every call. `run_id` is reserved: don't set it yourself.
280
+
263
281
  ```ts
264
282
  import { createRig, loadConfigFromEnv } from "modelrig";
265
283
 
266
284
  const rig = createRig(loadConfigFromEnv());
267
285
 
268
- // One run per pipeline execution. run.scope isolates concurrent runs; use
269
- // run.start()/run.end() for one run at a time on a request path.
286
+ // Every pipeline execution is a run. Inside the scope the SDK records each
287
+ // rig.run / rig.runRaw call as a step and stamps run_id and step on every row.
288
+ // run_id is reserved: don't set it yourself (put your own attempt id in a tag of
289
+ // your own, such as pipeline_attempt_id).
270
290
  await rig.artifacts.run.scope(
271
- { pipeline: "support", episodeKey: ticketId },
291
+ { pipeline: "support", episodeKey: ticketId, environment: process.env.NODE_ENV ?? "development" },
272
292
  async () => {
273
293
  const result = await rig.run("example.support_summarize", {
274
294
  input: { ticket, product, priorContext },
275
- tags: { subject: customer, feature: "summarize" }, // run_id auto-stamped
276
- stepKey: "summarize", // names this step on /runs
295
+ tags: { subject: customer, feature: "summarize" },
296
+ stepKey: "summarize", // names this step on /runs
277
297
  });
278
- // Save the work product — prompt / raw / parsed all save the same way.
279
- rig.artifacts.artifact.save(result.output, { name: "summary", type: "step_output" });
298
+ rig.artifacts.artifact.save(result.output, { name: "summary", type: "step_output", stepKey: "summarize" });
280
299
  },
281
300
  );
282
- rig.close();
301
+
302
+ await rig.close(); // flushes the exporter — call once at shutdown
283
303
  ```
304
+ <!-- /BLOCK:RUN_WRAP -->
284
305
 
285
306
  - **Tags:** carry `subject` (what the run is *about* — an opaque id) and
286
307
  `feature`. Inside a run context the SDK stamps `tags.run_id` (the run's
287
308
  `episodeKey`, else its id) and `tags.step` (the current step key) on every
288
- attempt row of both lanes — a value you pass always wins, and outside a run
289
- context nothing is stamped, so the row is byte-identical.
309
+ attempt row of both lanes; outside a run context nothing is stamped, so the row
310
+ is byte-identical. `run_id` is reserved: don't set it yourself — the full
311
+ reserved-key list is in the [instrumentation guide](./docs/instrumentation.md#reserved-tag-keys-the-one-list).
290
312
  - **Raw lane too:** a `rig.runRaw` call inside a run context records a
291
313
  ground-truth step (since 0.5.0), so grounded/cached BYOK pipelines fill the
292
314
  same graph.
@@ -297,8 +319,7 @@ rig.close();
297
319
 
298
320
  **On by default under a control plane.** The run/artifact namespace is **on by
299
321
  default once a control plane is configured** — set `MODELRIG_API_KEY=rig_sk_…`
300
- (or the self-host Supabase pair) and runs record automatically, metadata and
301
- hashes only. A **pure local-only rig** (no sink to ship to) stays off; force it
322
+ (or the self-host Supabase pair) and runs record automatically. A **pure local-only rig** (no sink to ship to) stays off; force it
302
323
  on there with `MODELRIG_ARTIFACTS=1`. To **opt out of capture**, set posture
303
324
  `off` in your account settings (the console or the `set_org_settings` MCP tool) —
304
325
  the account posture is the customer control, not a deployment env var. With the
@@ -306,11 +327,12 @@ namespace off (a local-only rig) it is inert — `run.start` is a
306
327
  no-op, `artifact.save` returns null, zero rows are written, and your pipeline
307
328
  behaves byte-for-byte as before. `modelrig status` prints the current posture.
308
329
 
309
- > **Metadata and hashes only, this release.** Content custody (holding the
310
- > serialized bytes) is early access; today the value you save is hashed (sha256)
311
- > and the bytes are discarded — the metadata row and its integrity hash are what
312
- > persist. A `zeroRetention` run refuses every artifact fail-closed at the SDK
313
- > gate.
330
+ > **What is kept.** Every saved artifact records a metadata row with a sha256
331
+ > integrity hash. Whether the serialized bytes are kept as well is your
332
+ > organization's [content custody](https://modelrig.dev/content-custody.html)
333
+ > posture: new organizations are created `managed` (bytes kept in your managed
334
+ > store); `metadata` keeps hashes only and `off` captures nothing. A
335
+ > `zeroRetention` run refuses every artifact fail-closed at the SDK gate.
314
336
 
315
337
  The full artifact API — lineage (`link`), evaluations (`evaluate`), the reusable
316
338
  per-step seam, and `run.scope` vs `run.start` — is the
@@ -345,8 +367,10 @@ on the quickstart:
345
367
 
346
368
  ## Honest limits
347
369
 
348
- - **Prepaid balances and managed billing are not built yet.** This release is
349
- the gateway and the self-host lane; the hosted control plane has one customer.
370
+ - **Managed serving is opt-in and prepaid.** It draws from a prepaid balance your
371
+ organization funds in the console ([Managed serving](https://modelrig.dev/managed-serving.html));
372
+ with no balance, every call runs on your own provider keys (`destination: direct`,
373
+ the default).
350
374
  - **`0.x` means the shape may still move.** Route and config contracts are
351
375
  stable enough to build on, not frozen.
352
376
  - **`modelrig serve` needs the `modelrig-server` package** — it is not bundled
@@ -13,6 +13,20 @@ this snapshot was copied by hand per implementation-plan §9. Replace this manua
13
13
  copy with the sync script's output when Phase 0 tooling lands; until then, any
14
14
  pin bump must re-copy this file and update this note.
15
15
 
16
+ HAND-ADDED `pricing_v2` BLOCKS (must survive a pin bump — re-apply after any
17
+ re-copy): `gemini/gemini-3.1-pro-preview` and `…-customtools` carry an additive
18
+ `pricing_v2` (GitLab issue #2, 2026-10-11, verified against
19
+ https://ai.google.dev/gemini-api/docs/pricing): `tiers.flex = { input: 0.5,
20
+ output: 0.5, cache_read: 1.0 }` (flex/batch halve input + output; cache read is
21
+ "Same as Standard"), `tiers.priority = 1.8` (every component), and the >200k
22
+ context band. The snapshot wins on shared keys and never carries `pricing_v2`
23
+ upstream, so the block lives on the snapshot entry itself; `getPricingV2` reads
24
+ it and the served-tier cost path (`src/run/cost.ts`) prices flex/priority rows
25
+ from it. Flat fields are untouched (base is byte-identical). `scripts/sync-upstream.ts
26
+ --apply` RE-GRAFTS every `pricing_v2` block from the current map onto the new
27
+ snapshot (and refuses to apply if upstream dropped an entry that carries one), so a
28
+ pin bump cannot silently drop them; the manual re-copy path above must do the same.
29
+
16
30
  Key naming (verified against the snapshot):
17
31
  - Gemini API models: `gemini/<model>` (bare ids resolve to vertex_ai pricing — do not use)
18
32
  - OpenAI models: bare `<model>`
@@ -20338,6 +20338,19 @@
20338
20338
  "tpm": 800000
20339
20339
  },
20340
20340
  "gemini/gemini-3.1-pro-preview": {
20341
+ "pricing_v2": {
20342
+ "base": { "input": 2e-06, "output": 1.2e-05, "cache_read": 2e-07 },
20343
+ "tiers": {
20344
+ "priority": 1.8,
20345
+ "flex": { "input": 0.5, "output": 0.5, "cache_read": 1.0 }
20346
+ },
20347
+ "context_bands": [
20348
+ { "above_tokens": 200000, "rates": { "input": 4e-06, "output": 1.8e-05, "cache_read": 4e-07 } }
20349
+ ],
20350
+ "source": "https://ai.google.dev/gemini-api/docs/pricing (Gemini 3.1 Pro Preview, verified 2026-10-11: flex/batch 0.5x input+output, cache read 'Same as Standard'; priority 1.8x every component incl. cache read; >200k band doubles input, output and cache read)",
20351
+ "as_of": "2026-10-11",
20352
+ "basis": "base · standard tier · text · prompts ≤ 200k tokens (the >200k context band is listed; cost rows price at the SERVED tier)"
20353
+ },
20341
20354
  "cache_read_input_token_cost": 2e-07,
20342
20355
  "cache_read_input_token_cost_above_200k_tokens": 4e-07,
20343
20356
  "input_cost_per_token": 2e-06,
@@ -20395,6 +20408,19 @@
20395
20408
  "web_search_billing_unit": "per_query"
20396
20409
  },
20397
20410
  "gemini/gemini-3.1-pro-preview-customtools": {
20411
+ "pricing_v2": {
20412
+ "base": { "input": 2e-06, "output": 1.2e-05, "cache_read": 2e-07 },
20413
+ "tiers": {
20414
+ "priority": 1.8,
20415
+ "flex": { "input": 0.5, "output": 0.5, "cache_read": 1.0 }
20416
+ },
20417
+ "context_bands": [
20418
+ { "above_tokens": 200000, "rates": { "input": 4e-06, "output": 1.8e-05, "cache_read": 4e-07 } }
20419
+ ],
20420
+ "source": "https://ai.google.dev/gemini-api/docs/pricing (Gemini 3.1 Pro Preview, verified 2026-10-11: flex/batch 0.5x input+output, cache read 'Same as Standard'; priority 1.8x every component incl. cache read; >200k band doubles input, output and cache read)",
20421
+ "as_of": "2026-10-11",
20422
+ "basis": "base · standard tier · text · prompts ≤ 200k tokens (the >200k context band is listed; cost rows price at the SERVED tier)"
20423
+ },
20398
20424
  "cache_read_input_token_cost": 2e-07,
20399
20425
  "cache_read_input_token_cost_above_200k_tokens": 4e-07,
20400
20426
  "input_cost_per_token": 2e-06,
@@ -14,7 +14,7 @@
14
14
  * - gemini: `finishReason` is `SAFETY` / `PROHIBITED_CONTENT` / `RECITATION`.
15
15
  * The refusal TEXT itself is never carried (content) — only the boolean.
16
16
  */
17
- import type { AttemptMetaV1, FinishReason } from "../telemetry/attempt-meta";
17
+ import type { AttemptMetaV1, FinishReason, PromptDelivery, ServedTierSource } from "../telemetry/attempt-meta";
18
18
  import type { AdapterRequest, ProviderId, ToolCall } from "../types/provider";
19
19
  /** Map a provider's native finish/stop reason to the `FinishReason` enum
20
20
  * (spec §2). Unknown/absent ⇒ undefined (never inferred); an unrecognised
@@ -51,6 +51,13 @@ export interface AdapterMetaSignals {
51
51
  };
52
52
  /** true = the run loop streamed deltas (stream()); false/absent = buffered. */
53
53
  readonly streaming?: boolean;
54
+ /** Issue #5 (R1): how the rendered prompt reached the model on this call —
55
+ * reported by the adapter that shaped the request (today: gemini, where a
56
+ * cache handle moves delivery). Absent ⇒ no `prompt_delivery` key. */
57
+ readonly promptDelivery?: PromptDelivery;
58
+ /** Issue #2 (L6 F5): did the provider ECHO the served tier, or did the adapter
59
+ * fall back to the requested tier? Stamped as `meta.served_tier_source`. */
60
+ readonly servedTierSource?: ServedTierSource;
54
61
  }
55
62
  /** Build one adapter's NATIVE `meta` (spec §2). Only defined signals appear —
56
63
  * everything else is unknown and omitted. The producer takes it from here. */
@@ -1 +1 @@
1
- {"version":3,"file":"attempt-meta-map.d.ts","sourceRoot":"","sources":["../../src/adapters/attempt-meta-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE9E;;kCAEkC;AAClC,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,UAAU,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,YAAY,GAAG,SAAS,CA6C1B;AAED;;;oDAGoD;AACpD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,cAAc,GAClB;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAOlF;AAED;0EAC0E;AAC1E,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,GACzC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,SAAS,CAQ/C;AAED,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;8EAC8E;AAC9E,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,UAAU,EACpB,CAAC,EAAE,kBAAkB,GACpB,OAAO,CAAC,aAAa,CAAC,CAyBxB;AAED;;;uDAGuD;AACvD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CASzE"}
1
+ {"version":3,"file":"attempt-meta-map.d.ts","sourceRoot":"","sources":["../../src/adapters/attempt-meta-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC/G,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE9E;;kCAEkC;AAClC,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,UAAU,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC7B,YAAY,GAAG,SAAS,CA6C1B;AAED;;;oDAGoD;AACpD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,cAAc,GAClB;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAOlF;AAED;0EAC0E;AAC1E,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,GACzC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,SAAS,CAQ/C;AAED,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B;;0EAEsE;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;gFAC4E;IAC5E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CAC9C;AAED;8EAC8E;AAC9E,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,UAAU,EACpB,CAAC,EAAE,kBAAkB,GACpB,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED;;;uDAGuD;AACvD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CASzE"}
@@ -134,6 +134,10 @@ function buildAdapterMeta(provider, s) {
134
134
  }
135
135
  if (typeof s.streaming === "boolean")
136
136
  meta.streaming = s.streaming;
137
+ if (s.promptDelivery !== undefined)
138
+ meta.prompt_delivery = s.promptDelivery;
139
+ if (s.servedTierSource !== undefined)
140
+ meta.served_tier_source = s.servedTierSource;
137
141
  return meta;
138
142
  }
139
143
  /** A transport/SDK error's coarse envelope (spec §2 `http_status`/`error_code`).
@@ -27,8 +27,8 @@ function buildAdapters(config, registryFacts, compatStatic) {
27
27
  }
28
28
  if (config.keys.grok)
29
29
  adapters.set("grok", (0, grok_1.createGrokAdapter)({ apiKey: config.keys.grok }));
30
- // A1 aggregator hosts behind the generic openai-compatible adapter.
31
- for (const hostId of ["deepinfra", "fireworks"]) {
30
+ // A1 aggregator hosts + Z.ai (B4a) behind the generic openai-compatible adapter.
31
+ for (const hostId of ["deepinfra", "fireworks", "zai"]) {
32
32
  const apiKey = config.keys[hostId];
33
33
  if (!apiKey)
34
34
  continue;
@@ -13,8 +13,9 @@
13
13
  * Copyright (c) 2023 Berri AI, MIT License — retain this notice if LiteLLM's
14
14
  * gemini transformation logic is consulted during the port.
15
15
  */
16
- import type { AdapterRequest, CapabilityFlag, ProviderAdapter } from "../types/provider";
16
+ import type { AdapterRequest, CapabilityFlag, ProviderAdapter, ServiceTier } from "../types/provider";
17
17
  import type { FailureClass } from "../types/failures";
18
+ import type { PromptDelivery, ServedTierSource } from "../telemetry/attempt-meta";
18
19
  export interface GeminiAdapterConfig {
19
20
  readonly apiKey: string;
20
21
  }
@@ -53,9 +54,35 @@ export interface GeminiShapedRequest {
53
54
  readonly model: string;
54
55
  readonly contents: string;
55
56
  readonly config: Record<string, unknown>;
57
+ /** Issue #5 (R1): how the rendered prompt was delivered — stamped on the
58
+ * attempt's `meta.prompt_delivery` so the control plane can prove it. */
59
+ readonly promptDelivery: PromptDelivery;
56
60
  }
61
+ /**
62
+ * THE CACHE-HANDLE DELIVERY RULE (GitLab issue #5, R1). A request that sets
63
+ * `cachedContent` may not set `systemInstruction` (Google rejects both — the
64
+ * system instruction and tools are baked into the cache at creation). Before
65
+ * R1 the adapter silently DROPPED a non-empty system prompt here, so a route
66
+ * whose template held the per-call delta lost it whenever a handle was passed
67
+ * (caching-lifecycle.md's "you do not need to empty your template" was a trap).
68
+ * The rule now: **with a cache handle, everything rendered is delivered as the
69
+ * user turn** — the system prompt (when non-empty), a blank line, then the user
70
+ * turn — and nothing is dropped. The cache holds the shared prefix; the user
71
+ * turn carries what changes per call. Pure; exported for the adapter tests.
72
+ */
73
+ export declare function cachedUserTurn(systemPrompt: string, userPrompt: string): string;
57
74
  /** Pure request shaping — everything except the abort signal. */
58
75
  export declare function buildGeminiRequest(model: string, req: ShapingInput): GeminiShapedRequest;
76
+ /** Best-effort served-tier extraction (ported): response.serviceTier, then
77
+ * usageMetadata.trafficType (Vertex path), else assume the requested tier —
78
+ * production bills flex when a flex request was accepted without a tier echo.
79
+ * Issue #2 (L6 F5): now that cost rows PRICE at the served tier, the fallback
80
+ * is disclosed — `source` lands on `meta.served_tier_source`. Pure; exported
81
+ * for the unit test. No behaviour change in the tier itself. */
82
+ export declare function extractGeminiServedTier(rawServiceTier: string | undefined, trafficType: string | undefined, requested: ServiceTier): {
83
+ readonly tier: ServiceTier;
84
+ readonly source: ServedTierSource;
85
+ };
59
86
  /** A candidate's parts as we read them post-response — `functionCall`, `text`,
60
87
  * and (if `includeThoughts` were ever set) a `thought`-flagged text part may all
61
88
  * appear on the same candidate (a grounded / thinking turn). */
@@ -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;AAQlE,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,CA4MhF"}
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,EACf,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,mBAAmB,CAAC;AAGlE,OAAO,KAAK,EAAiB,cAAc,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKjG,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;IACzC;6EACyE;IACzE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,mBAAmB,CA4DxF;AAUD;;;;;gEAKgE;AAChE,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,WAAW,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAOnE;AAED;;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,CA8MhF"}
@@ -19,7 +19,9 @@ exports.GEMINI_THINKING_CAPABLE_MODELS = void 0;
19
19
  exports.geminiSupports = geminiSupports;
20
20
  exports.classifyGeminiFailure = classifyGeminiFailure;
21
21
  exports.geminiReasoningUnsupported = geminiReasoningUnsupported;
22
+ exports.cachedUserTurn = cachedUserTurn;
22
23
  exports.buildGeminiRequest = buildGeminiRequest;
24
+ exports.extractGeminiServedTier = extractGeminiServedTier;
23
25
  exports.extractGeminiCandidateText = extractGeminiCandidateText;
24
26
  exports.geminiCandidateHasFunctionCall = geminiCandidateHasFunctionCall;
25
27
  exports.resolveGeminiText = resolveGeminiText;
@@ -126,6 +128,21 @@ function geminiReasoningUnsupported(model, req) {
126
128
  }
127
129
  return null;
128
130
  }
131
+ /**
132
+ * THE CACHE-HANDLE DELIVERY RULE (GitLab issue #5, R1). A request that sets
133
+ * `cachedContent` may not set `systemInstruction` (Google rejects both — the
134
+ * system instruction and tools are baked into the cache at creation). Before
135
+ * R1 the adapter silently DROPPED a non-empty system prompt here, so a route
136
+ * whose template held the per-call delta lost it whenever a handle was passed
137
+ * (caching-lifecycle.md's "you do not need to empty your template" was a trap).
138
+ * The rule now: **with a cache handle, everything rendered is delivered as the
139
+ * user turn** — the system prompt (when non-empty), a blank line, then the user
140
+ * turn — and nothing is dropped. The cache holds the shared prefix; the user
141
+ * turn carries what changes per call. Pure; exported for the adapter tests.
142
+ */
143
+ function cachedUserTurn(systemPrompt, userPrompt) {
144
+ return systemPrompt === "" ? userPrompt : `${systemPrompt}\n\n${userPrompt}`;
145
+ }
129
146
  /** Pure request shaping — everything except the abort signal. */
130
147
  function buildGeminiRequest(model, req) {
131
148
  const usingCache = req.cache?.key !== undefined && req.cache.key !== "";
@@ -156,11 +173,18 @@ function buildGeminiRequest(model, req) {
156
173
  }
157
174
  // cachedContent excludes systemInstruction and tools — the API rejects
158
175
  // requests setting both (they are baked into the cache at creation time,
159
- // including the googleSearch tool for cached grounded steps).
176
+ // including the googleSearch tool for cached grounded steps). Issue #5 (R1):
177
+ // the rendered system prompt is therefore delivered AS the user turn (see
178
+ // cachedUserTurn) rather than dropped; `promptDelivery` records which.
179
+ let contents = req.userPrompt;
180
+ let promptDelivery;
160
181
  if (usingCache) {
161
182
  config.cachedContent = req.cache?.key;
183
+ contents = cachedUserTurn(req.systemPrompt, req.userPrompt);
184
+ promptDelivery = "user+cache";
162
185
  }
163
186
  else {
187
+ promptDelivery = req.systemPrompt !== "" ? "system" : "user";
164
188
  if (req.systemPrompt !== "") {
165
189
  config.systemInstruction = req.systemPrompt;
166
190
  }
@@ -179,20 +203,23 @@ function buildGeminiRequest(model, req) {
179
203
  if (req.requestedTier === "flex") {
180
204
  config.serviceTier = "flex";
181
205
  }
182
- return { model, contents: req.userPrompt, config };
206
+ return { model, contents, config, promptDelivery };
183
207
  }
184
208
  /** Best-effort served-tier extraction (ported): response.serviceTier, then
185
209
  * usageMetadata.trafficType (Vertex path), else assume the requested tier —
186
- * production bills flex when a flex request was accepted without a tier echo. */
187
- function extractServedTier(rawServiceTier, trafficType, requested) {
210
+ * production bills flex when a flex request was accepted without a tier echo.
211
+ * Issue #2 (L6 F5): now that cost rows PRICE at the served tier, the fallback
212
+ * is disclosed — `source` lands on `meta.served_tier_source`. Pure; exported
213
+ * for the unit test. No behaviour change in the tier itself. */
214
+ function extractGeminiServedTier(rawServiceTier, trafficType, requested) {
188
215
  if (rawServiceTier === "flex" || trafficType === "ON_DEMAND_FLEX")
189
- return "flex";
216
+ return { tier: "flex", source: "provider" };
190
217
  if (rawServiceTier === "standard" || rawServiceTier === "default" || trafficType === "ON_DEMAND") {
191
- return "standard";
218
+ return { tier: "standard", source: "provider" };
192
219
  }
193
220
  if (rawServiceTier === "priority" || trafficType === "ON_DEMAND_PRIORITY")
194
- return "priority";
195
- return requested;
221
+ return { tier: "priority", source: "provider" };
222
+ return { tier: requested, source: "requested_fallback" };
196
223
  }
197
224
  /**
198
225
  * #245 secondary — concatenate the answer `text` parts of a Gemini candidate's
@@ -277,7 +304,7 @@ function createGeminiAdapter(config) {
277
304
  const ttfbMs = Date.now() - dispatchedAt;
278
305
  const usageMetadata = (response.usageMetadata ?? {});
279
306
  const respWithTier = response;
280
- const servedTier = extractServedTier(respWithTier.serviceTier, usageMetadata.trafficType, req.requestedTier);
307
+ const { tier: servedTier, source: servedTierSource } = extractGeminiServedTier(respWithTier.serviceTier, usageMetadata.trafficType, req.requestedTier);
281
308
  const tokensIn = usageMetadata.promptTokenCount ?? 0;
282
309
  // Thinking tokens are billed as output and count against
283
310
  // maxOutputTokens — include them in tokensOut for honest accounting.
@@ -303,6 +330,8 @@ function createGeminiAdapter(config) {
303
330
  toolCalls,
304
331
  grounding: req.grounding === true ? { searchProvider: "google" } : undefined,
305
332
  streaming: false,
333
+ promptDelivery: shaped.promptDelivery, // issue #5 (R1): prove what was sent
334
+ servedTierSource,
306
335
  });
307
336
  // Tool-call turn (spec §4.4): collect functionCall parts. When present,
308
337
  // surface normalized calls and skip the text/empty/truncation guards.