modelrig 0.7.0 → 0.8.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 (128) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +63 -0
  3. package/dist/bakeoff/disagreement.d.ts +93 -0
  4. package/dist/bakeoff/disagreement.d.ts.map +1 -0
  5. package/dist/bakeoff/disagreement.js +140 -0
  6. package/dist/bakeoff/engine.d.ts.map +1 -1
  7. package/dist/bakeoff/engine.js +58 -2
  8. package/dist/bakeoff/pairwise.d.ts +102 -0
  9. package/dist/bakeoff/pairwise.d.ts.map +1 -0
  10. package/dist/bakeoff/pairwise.js +214 -0
  11. package/dist/bakeoff/replay.d.ts +6 -0
  12. package/dist/bakeoff/replay.d.ts.map +1 -1
  13. package/dist/bakeoff/replay.js +6 -2
  14. package/dist/bakeoff/report.d.ts +7 -0
  15. package/dist/bakeoff/report.d.ts.map +1 -1
  16. package/dist/bakeoff/report.js +46 -0
  17. package/dist/bakeoff/types.d.ts +24 -0
  18. package/dist/bakeoff/types.d.ts.map +1 -1
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +9 -9
  21. package/dist/mask/apply.d.ts +80 -0
  22. package/dist/mask/apply.d.ts.map +1 -0
  23. package/dist/mask/apply.js +228 -0
  24. package/dist/mask/detect.d.ts +38 -0
  25. package/dist/mask/detect.d.ts.map +1 -0
  26. package/dist/mask/detect.js +167 -0
  27. package/dist/mask/index.d.ts +21 -0
  28. package/dist/mask/index.d.ts.map +1 -0
  29. package/dist/mask/index.js +54 -0
  30. package/dist/mask/mask-policy-cli.d.ts +11 -0
  31. package/dist/mask/mask-policy-cli.d.ts.map +1 -0
  32. package/dist/mask/mask-policy-cli.js +33 -0
  33. package/dist/mask/policy.d.ts +38 -0
  34. package/dist/mask/policy.d.ts.map +1 -0
  35. package/dist/mask/policy.js +187 -0
  36. package/dist/mask/proof.d.ts +54 -0
  37. package/dist/mask/proof.d.ts.map +1 -0
  38. package/dist/mask/proof.js +120 -0
  39. package/dist/mask/receipt.d.ts +30 -0
  40. package/dist/mask/receipt.d.ts.map +1 -0
  41. package/dist/mask/receipt.js +49 -0
  42. package/dist/mask/seam.d.ts +53 -0
  43. package/dist/mask/seam.d.ts.map +1 -0
  44. package/dist/mask/seam.js +120 -0
  45. package/dist/mask/types.d.ts +81 -0
  46. package/dist/mask/types.d.ts.map +1 -0
  47. package/dist/mask/types.js +10 -0
  48. package/dist/mask/vault.d.ts +50 -0
  49. package/dist/mask/vault.d.ts.map +1 -0
  50. package/dist/mask/vault.js +81 -0
  51. package/dist/receipts/types.d.ts +65 -0
  52. package/dist/receipts/types.d.ts.map +1 -0
  53. package/dist/receipts/types.js +134 -0
  54. package/dist/routes/loader-warnings.d.ts +8 -0
  55. package/dist/routes/loader-warnings.d.ts.map +1 -1
  56. package/dist/routes/loader-warnings.js +23 -0
  57. package/dist/routes/loader.d.ts +1 -1
  58. package/dist/routes/loader.d.ts.map +1 -1
  59. package/dist/routes/loader.js +24 -25
  60. package/dist/routes/mask-loader.d.ts +18 -0
  61. package/dist/routes/mask-loader.d.ts.map +1 -0
  62. package/dist/routes/mask-loader.js +39 -0
  63. package/dist/routes/objective-loader.d.ts.map +1 -1
  64. package/dist/routes/objective-loader.js +51 -17
  65. package/dist/routes/screen-loader.d.ts +23 -0
  66. package/dist/routes/screen-loader.d.ts.map +1 -0
  67. package/dist/routes/screen-loader.js +79 -0
  68. package/dist/routes/template.d.ts +10 -0
  69. package/dist/routes/template.d.ts.map +1 -1
  70. package/dist/routes/template.js +10 -3
  71. package/dist/run/accept.d.ts +4 -0
  72. package/dist/run/accept.d.ts.map +1 -1
  73. package/dist/run/accept.js +2 -0
  74. package/dist/run/dispatch.d.ts +35 -0
  75. package/dist/run/dispatch.d.ts.map +1 -1
  76. package/dist/run/dispatch.js +33 -0
  77. package/dist/run/grounding.d.ts +47 -2
  78. package/dist/run/grounding.d.ts.map +1 -1
  79. package/dist/run/grounding.js +113 -6
  80. package/dist/run/raw.d.ts.map +1 -1
  81. package/dist/run/raw.js +6 -1
  82. package/dist/run/record-attempt.d.ts.map +1 -1
  83. package/dist/run/record-attempt.js +12 -1
  84. package/dist/run/run.d.ts.map +1 -1
  85. package/dist/run/run.js +19 -16
  86. package/dist/screen/provenance.d.ts +59 -0
  87. package/dist/screen/provenance.d.ts.map +1 -0
  88. package/dist/screen/provenance.js +104 -0
  89. package/dist/screen/types.d.ts +71 -0
  90. package/dist/screen/types.d.ts.map +1 -0
  91. package/dist/screen/types.js +9 -0
  92. package/dist/status-cli.d.ts +11 -0
  93. package/dist/status-cli.d.ts.map +1 -0
  94. package/dist/status-cli.js +21 -0
  95. package/dist/status-runs.d.ts +38 -0
  96. package/dist/status-runs.d.ts.map +1 -0
  97. package/dist/status-runs.js +80 -0
  98. package/dist/status.d.ts +7 -2
  99. package/dist/status.d.ts.map +1 -1
  100. package/dist/status.js +11 -2
  101. package/dist/telemetry/ingest-protocol.d.ts +8 -0
  102. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  103. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  104. package/dist/telemetry/store-migrations.js +11 -0
  105. package/dist/telemetry/store.d.ts.map +1 -1
  106. package/dist/telemetry/store.js +12 -3
  107. package/dist/telemetry/supabase-exporter.d.ts +64 -0
  108. package/dist/telemetry/supabase-exporter.d.ts.map +1 -0
  109. package/dist/telemetry/supabase-exporter.js +211 -0
  110. package/dist/types/provider.d.ts +11 -0
  111. package/dist/types/provider.d.ts.map +1 -1
  112. package/dist/types/provider.js +9 -0
  113. package/dist/types/route.d.ts +55 -0
  114. package/dist/types/route.d.ts.map +1 -1
  115. package/dist/types/telemetry.d.ts +7 -0
  116. package/dist/types/telemetry.d.ts.map +1 -1
  117. package/dist/validate-cli.d.ts.map +1 -1
  118. package/dist/validate-cli.js +13 -0
  119. package/dist/verify/types.d.ts +6 -0
  120. package/dist/verify/types.d.ts.map +1 -1
  121. package/docs/agent-skill/SKILL.md +131 -0
  122. package/docs/how-it-fits.md +34 -0
  123. package/docs/instrumentation.md +221 -0
  124. package/docs/llms.txt +276 -0
  125. package/docs/migration-playbook.md +512 -0
  126. package/docs/quickstart.md +333 -0
  127. package/package.json +18 -2
  128. package/receipts.d.ts +12 -0
@@ -0,0 +1,512 @@
1
+ # Migrate a codebase to ModelRig — the T0–T2 playbook
2
+
3
+ This is the canonical playbook the ModelRig skill follows when you import it into
4
+ your coding tool (Claude Code, Cursor, Codex, …) and ask it to onboard a
5
+ codebase. It is a **pointer skill**: the skill file itself holds almost no facts
6
+ and fetches this page, so one copy serves every ecosystem and a stale cached
7
+ answer never overrides the current one.
8
+
9
+ The skill performs the *full* migration, not just detection: it finds LLM call
10
+ sites, instruments work-product boundaries, and — one call site at a time —
11
+ replaces direct provider calls with ModelRig routes. It does this on a **four-tier
12
+ autonomy ladder** that treats behaviour-preserving work differently from
13
+ behaviour-affecting work, and it keeps model optimization permanently out of its
14
+ own hands.
15
+
16
+ > **The skill flags; it never forces.** Where a call site does not fit a tier,
17
+ > the skill says so and routes it to the correct, lower tier — a raw-lane
18
+ > outcome is a *correct* result, not a failure. See "Stated limits" below.
19
+
20
+ ## The autonomy ladder
21
+
22
+ | Tier | Action | Risk class | Agent autonomy |
23
+ |---|---|---|---|
24
+ | **0** | **Gateway pointing:** swap an OpenAI-compatible base URL to the raw lane | Transport-only | Apply after plan review — telemetry and cost accounting light up instantly |
25
+ | **1** | **Instrumentation:** steps, artifacts, lineage; detect existing OTel and extend, never duplicate | Behaviour-preserving (the SDK contract: return values never change) | Plan-before-edit, then apply; the validator warns on over-instrumentation |
26
+ | **2** | **Route extraction:** prompt → `systemTemplate` + variables, schema from Zod/TS types (or `schema: null`), call site → `rig.run()` — **always same-model** | Behaviour-affecting | Propose → verify (type-check + your tests + `modelrig verify-swap` shadow-equivalence) → **human merges**; one call site per PR |
27
+ | **3** | **Model optimization** | — | **Never the skill's job** — its output feeds bake-offs; swaps stay behind the existing approval gate |
28
+
29
+ The dividing line is risk class, not effort: **T0 and T1 preserve behaviour**, so
30
+ the agent applies them after a plan review; **T2 changes what serves a call**, so
31
+ the agent only *proposes* it, proves it, and a human merges it. **T3 is off the
32
+ ladder** — the skill's job ends at producing the evidence; a model swap is a
33
+ separate, human-gated bake-off decision (see Tier 3).
34
+
35
+ ## Before you start — the one human handoff (F-6)
36
+
37
+ Everything upstream of the console an agent can do now. The one step it cannot is
38
+ **minting an API key**, which needs a console login at <https://app.modelrig.ai>
39
+ (`/keys`) — account/password auth. Plan that handoff rather than stalling on it:
40
+
41
+ ```bash
42
+ export MODELRIG_API_KEY=<minted at app.modelrig.ai/keys by a human, once>
43
+ # The default ingest host (https://api.modelrig.ai) needs no other config.
44
+ ```
45
+
46
+ A key is **not** required to run routes locally — it only decides where telemetry
47
+ lands. But *observing your runs in the console* (Tier 1's payoff) does need it.
48
+
49
+ `MODELRIG_API_KEY` gates the exporter **per process**. Set it on EVERY service,
50
+ worker, and cron that constructs a rig — a process without it buffers to its own
51
+ local SQLite and ships nothing. In a multi-service deployment, the web tier
52
+ alone having the key means every row a worker or cron produced is silently lost.
53
+
54
+ ---
55
+
56
+ ## Before the ladder — which lane, and the two inventories
57
+
58
+ **Read [Routing & reliability](routing-reliability.md) first.** It answers the
59
+ capability questions — what falls through, what is stored per attempt, what is
60
+ not built — so you never need to read the engine source to migrate correctly.
61
+
62
+
63
+ The autonomy ladder (T0–T2) is *how* you migrate one call site. **The lane is
64
+ which strategy fits the whole codebase** — decided once, up front, from the shape
65
+ of the code. Get this wrong and you either over-engineer a naive app or try to
66
+ externalize a routing engine that already exists. You (the coding agent) work the
67
+ lane assessment and the inventories yourself, then **stop at the checkpoints** (⏸)
68
+ and present to your human.
69
+
70
+ ### The three lanes
71
+
72
+ - **Lane A — naive call sites.** Static prompts, a fixed model per call, no
73
+ provider abstraction. This is the greenfield case the autonomy ladder was written for:
74
+ extract routes (Tier 2), replace call sites with `rig.run`. First route in under
75
+ an hour.
76
+ - **Lane B — an existing router / orchestrator.** The code already has a provider
77
+ abstraction, runtime-assembled prompts, and env- or logic-resolved model choice
78
+ (a `callProvider(provider, prompt, model)` dispatcher is the tell). **Do not
79
+ write per-task routes for this.** Migrate ONE naive call site as a reference
80
+ route, then integrate the router at the **`rig.runRaw` adapter seam** — replace
81
+ the dispatcher's body with `rig.runRaw({ provider, model, apiKey, systemPrompt,
82
+ userPrompt, … })`. `runRaw` is BYOK — pass your provider key; ModelRig holds it
83
+ for the one call and never stores it. Already assemble one combined prompt string?
84
+ Put it ALL in `userPrompt` and leave `systemPrompt` empty. ModelRig provides
85
+ telemetry, typed failures, and accounting; your code keeps prompt assembly and
86
+ cache lifecycle. The raw lane has no candidate ladder — a failure is yours to
87
+ handle, surfaced as a typed class. BYOK, zero-margin, no route externalization.
88
+
89
+ **runRaw-only construction (`routesDir: null`) — 0.4.0+.** A
90
+ Lane-B deployment that only ever calls `runRaw`/`runRawStream` has no routes.
91
+ Construct the rig with `routesDir: null` so `createRig` skips route loading, the
92
+ `rig.yaml` manifest, and the serveability gate — a stray route file it never
93
+ calls can't down it, and the empty-temp-dir workaround is gone:
94
+
95
+ ```ts
96
+ const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
97
+ await rig.runRaw({ provider, model, apiKey, systemPrompt, userPrompt });
98
+ ```
99
+
100
+ `loadConfigFromEnv()` always yields a string `routesDir` (there is no env var
101
+ that disables routes, by design) — the opt-out is the deliberate code-level
102
+ override above. `rig.run` and every route-dependent surface then throws a
103
+ terminal `invariant_violation` naming the fix; keep a reference route (a routed
104
+ rig) if you also need `run`/bake-offs.
105
+ - **Lane C — full route migration of a Lane-B system.** Externalizing a dynamic
106
+ prompt assembler into route templates is a scoped *project* (prompt
107
+ externalization), never an onboarding step. Recognize it, name it, and leave it
108
+ for later.
109
+
110
+ The lane picks which tiers apply: Lane A runs the full T0→T2 ladder; Lane B stops
111
+ at the `runRaw` seam (T0/T1 plus the one reference route) and defers T2-for-everything
112
+ to Lane C.
113
+
114
+ ### ⏸ C1 — Fit assessment (present the verdict + lane)
115
+
116
+ Inventory the LLM call sites and classify the codebase (naive vs. router layer).
117
+ Present which lane you chose and why. See "Call-site scope" for what counts.
118
+
119
+ ### ⏸ C2 — Caching, grounding & sampling inventory (the stop-gate)
120
+
121
+ **Before writing any route,** grep the target for provider caching,
122
+ provider-native grounding, **and explicit sampling parameters** — the three
123
+ behaviours that vanish *silently* on a naive migration (a cost regression, a
124
+ quality regression, and a consistency regression that only telemetry reveals).
125
+ Search for:
126
+
127
+ ```
128
+ cachedContent # Gemini explicit cache resource
129
+ cache_control # Anthropic marker cache
130
+ prompt_cache_key # OpenAI / Grok key-hint cache
131
+ cached_tokens | cache_read | cachedContentTokenCount # cache usage in responses
132
+ temperature | top_p | topP | max_output_tokens | maxOutputTokens | max_tokens
133
+ # explicit sampling — a classifier at temperature 0.1 is a
134
+ # deliberate choice, not a default
135
+ useGoogleSearch | googleSearch | grounding # provider-native grounding
136
+ serviceTier | service_tier # flex / priority service tier
137
+ thinkingLevel | thinkingConfig | reasoning_effort # per-step reasoning effort
138
+ ```
139
+
140
+ **Any hit is a STOP-gate.** The route must carry the corresponding surface:
141
+
142
+ - **Caching** → the customer cache handle rides `rig.run(task, { cache: { key,
143
+ provider } })` (or `rig.runRaw({ …, apiKey, cache: { key } })`) — a one-field
144
+ pass-through of the resource the customer already owns. ModelRig prices the hits
145
+ but never manages the resource; a long job still needs the customer's TTL
146
+ heartbeat. Full mechanics: [Caching lifecycle](https://modelrig.dev/caching-lifecycle.html).
147
+ - **Grounding** → on a route, `require: [grounded]` plus the route's grounding
148
+ mode. On the Lane-B
149
+ `runRaw` seam, pass `grounding: { mode: "native" }` for provider-native web
150
+ grounding — **gemini** (Google Search); **grok** is fail-closed for now (xAI retired Live Search; its Responses-API `web_search` is tracked as CE-9b). A grounded step that also caches must bake the
151
+ `googleSearch` tool into the cache resource (see the caching-lifecycle doc).
152
+ - **Service tier** → on the
153
+ raw lane, `serviceTier: "standard" | "flex" | "priority"`; `meta.servedTier`
154
+ reports what actually served, and a flex shed surfaces as `capacity_shed` for
155
+ your own retry ladder. The raw-lane `timeoutMs` default is tier-aware since
156
+ 0.5.0 (standard/priority 300 s, flex 900 s) — **flex ⇒ set `timeoutMs` in
157
+ minutes** (default 900 s), and a `timeout` failure teaches the tier + budget in
158
+ its `fixHint`. **§3.2 flex degrade is customer-side**: ModelRig does
159
+ NOT replicate the gemini direct-path infra-retry wrapper on the raw lane, so a
160
+ `PIPELINE_SERVICE_TIER=flex` capacity rejection is yours to retry at
161
+ `"standard"` — a NAMED difference for your migration record, not a silent one.
162
+ - **Reasoning / thinking** →
163
+ on the raw lane, `reasoning: { level: "minimal" | "low" | "medium" | "high" }`.
164
+ Since 0.4.0, this generalizes beyond gemini WITHOUT a per-model
165
+ allowlist — openai/deepinfra/fireworks forward the level as `reasoning_effort`
166
+ and let the provider enforce (a rejection is classified teachably); grok
167
+ coarsens it (`minimal,low → low`; `medium,high → high`); gemini stays
168
+ thinking-model-gated. **DeepSeek reasoning is model selection, not a knob**:
169
+ there is no effort control — select the `deepseek-reasoner` model and DON'T
170
+ declare `reasoning` (it fails closed with exactly that fixHint). anthropic
171
+ extended thinking is unwired (fails closed).
172
+ - **Response format** _(0.4.0+)_ → on the raw lane,
173
+ `responseFormat: "json_object"` forces syntactic JSON on the OpenAI-dialect
174
+ providers + gemini. This is MIME-shaping, NOT schema enforcement — the raw lane
175
+ still never validates or repairs (F4), so `meta.validated` stays `false`;
176
+ anthropic fails closed. On DeepSeek the prompt must contain the word "JSON"
177
+ (the caller's responsibility on the raw lane).
178
+ - **Which provider honors which knob** is the single-source
179
+ [provider × knob matrix](https://modelrig.dev/route-bundles.html#raw-lane-provider-knob-support-rigrunraw)
180
+ — the runtime guard and that table are generated from the same `RAW_KNOB_SUPPORT`.
181
+ A knob a provider cannot honor FAILS CLOSED (`invariant_violation`,
182
+ pre-dispatch) — never a silent drop. Read `rawKnobSupport(provider)` in code
183
+ instead of duplicating the lists; `GEMINI_THINKING_CAPABLE_MODELS` is exported
184
+ too (so you stop copying the §3.1 thinking-model set).
185
+ - **Sampling** → declare `policy.sampling` on the route:
186
+ ```yaml
187
+ policy:
188
+ sampling: { temperature: 0.1, top_p: 0.9, max_output_tokens: 8192 }
189
+ ```
190
+ All three fields are optional; carry over exactly what the original call set.
191
+ Absent, every adapter keeps its own defaults (Gemini runs at temperature 1.0),
192
+ so a call that relies on a specific temperature — a classifier at 0.1, say —
193
+ MUST declare it or it silently changes behaviour. A declared value is sent to
194
+ the provider **as-is** (no clamp); an out-of-range value is a load-time config
195
+ error (temperature 0–2, top_p (0,1], max_output_tokens ≥ 1). Note for Gemini 3:
196
+ Google recommends temperature 1.0 and warns sub-1.0 values can loop/degrade on
197
+ complex *reasoning* tasks — the loader logs an advisory for a sub-1.0 Gemini-3
198
+ temperature and sends the request as declared (classification/extraction are
199
+ typically fine).
200
+
201
+ If a surface cannot express what the code does yet, **report the gap and the
202
+ quantified cost/quality delta — never migrate past it silently.** Present the
203
+ inventory results and the decision (carry the handle, or a named, quantified
204
+ regression). Silent is the only forbidden outcome.
205
+
206
+ ### The rig lifecycle (learned the hard way in the first Phase-0 swap)
207
+
208
+ - **Create once, close always.** One rig per server process (module singleton);
209
+ per-invocation for one-shot jobs — and a one-shot caller MUST
210
+ `await rig.close()` before exit, or telemetry rows are lost and the SDK's
211
+ timers keep the process alive.
212
+ - **`tags` is required**; declare each tag key you rely on as a
213
+ `dimensions[].key` in `rig.yaml` (tag-safe `[A-Za-z0-9_]{1,64}`) so the
214
+ console can attribute cost by it. A `required: true` dimension missing from a
215
+ run's tags **warns once per process** (and under-attributes that run) — it
216
+ is never rejected. An example that shows `tags: { run_id }` assumes a
217
+ `run_id` dimension exists.
218
+ - **`result.output` is already parsed and schema-validated** — delete the old
219
+ call site's `JSON.parse` and fence-stripping rather than keeping both.
220
+ - **Routes resolve from `./modelrig/routes` relative to the process CWD.** In a
221
+ monorepo, place `modelrig/` at the app root and run from there, or set
222
+ `MODELRIG_ROUTES_DIR` to an absolute path.
223
+ - **Load-check with `modelrig validate` — no key required.** It structurally
224
+ loads `rig.yaml` and every route bundle, prints per-route OK / config error,
225
+ and exits nonzero on any failure — the dry authoring check to run before you
226
+ wire any credential. (Serving still fail-closes keyless: a live `createRig`
227
+ needs at least one provider key so a route has a serveable candidate.)
228
+
229
+ ### Package manager (detect, don't assume)
230
+
231
+ Install with the project's OWN package manager — read it from the lockfile:
232
+ `pnpm-lock.yaml` → `pnpm add modelrig`; `yarn.lock` → `yarn add modelrig`;
233
+ `package-lock.json` → `npm i modelrig`. Running `npm i` in a pnpm workspace
234
+ corrupts the store — never hard-code `npm`. Node ≥ 20; provider keys stay in env
235
+ vars.
236
+
237
+ ### Call-site scope (what "every LLM call" means)
238
+
239
+ - **In scope:** text/JSON *generation* calls, including the secondary
240
+ extractor/repair follow-ups that are part of a generation flow (a JSON-extractor
241
+ retry on a malformed response is in scope — it is the same logical call).
242
+ - **Out of scope (v1, state it explicitly):** embeddings, web-search tool calls,
243
+ and rerankers. These are not generation routes; say so rather than guessing.
244
+
245
+ ### ⏸ C3 — Candidate ratification · ⏸ C4 — Prove it
246
+
247
+ C3 (ratify the proposed candidate list) and C4 (tests + diff + the week-one
248
+ cache-hit-rate/cost panel comparison) are the Tier 2 gates below — the human
249
+ merges on a green report. A zero cache-hit rate on a route that cached before
250
+ migration is a regression; say so at C4.
251
+
252
+ **The cache-ratio-vs-baseline metric is a REAL-run measurement, not a probe.**
253
+ The cached-token ratio a migration must preserve comes from the pipeline running
254
+ its actual traffic (a warm cache, real prompt prefixes) — a single-call probe
255
+ cannot produce it and never closes this gate. At the checkpoint, mark
256
+ cache-ratio-vs-baseline **deferred-to-first-run** and read it off the week-one
257
+ telemetry panel; do not report it "verified" from a probe.
258
+
259
+ **C3 — unprobed models are SERVABLE (ruled 2026-08-20).** Probes gate
260
+ *claims*, not *serving*: any model an adapter can reach may be a candidate,
261
+ probed or not. Pin the model production uses today as candidate #1 — always,
262
+ including models the registry has never probed — and **never silently
263
+ substitute a probed sibling for the one production runs.** What UNPROBED
264
+ actually means for a candidate (present these at C3, they are the honest
265
+ cost of no evidence, not a punishment):
266
+
267
+ - `json: native` may NOT be claimed for it (that is a probed-capability
268
+ claim; the emulation ladder serves structure instead);
269
+ - no leaderboard standing, no bake-off priors, no declared-vs-probed
270
+ discrepancy protection;
271
+ - pricing may be missing until the next pricing sweep — an unpriced model
272
+ charges budget envelopes at the conservative rate (never rides free).
273
+
274
+ Flag each unprobed candidate UNPROBED in the C3 presentation and offer the
275
+ follow-ups: request a probe (probe-request page — customer traffic on a
276
+ model is exactly how it earns a place in the next probe cycle), and/or a
277
+ probed sibling as candidate #2 for the fallback slot. A third follow-up when
278
+ you want a model but do not yet trust it live: **declare it `trial: true`**
279
+ (the console's declare-first picker writes the YAML) so it is never served
280
+ live, then bake it off on captured traffic and promote the winner — evidence
281
+ before exposure, no leap of faith on an unprobed model.
282
+
283
+ **C4 decision — JSON-mode originals.** If the original call used mime-type
284
+ JSON plus app-side validation, declaring a `schema:` on the route is an
285
+ UPGRADE, not identity: the provider then enforces the schema and ModelRig's
286
+ repair ladder engages. Usually the right call — but it is a deliberate
287
+ behaviour change, so present it at C4 as one ("schema enforcement added"),
288
+ or set `policy.json: json_mode` with `schema: null` for closest identity.
289
+ Which wins is the human's ratification, never a silent default.
290
+
291
+ ### ⏸ C5 — Keys & billing handoff (the setup a human must finish)
292
+
293
+ Everything above an agent does; placing **keys** and a **payment method** is the
294
+ one part it must hand to a human. Do not stall on it and do not do it for them —
295
+ present exactly what to place where, for the lane you chose at C1.
296
+
297
+ **The hard rule (non-negotiable): you never touch a key VALUE.** Not read beyond
298
+ an existence check, not print, not paste into the console, not put in a manifest.
299
+ Humans place keys directly. You check only whether a key EXISTS, by NAME:
300
+
301
+ ```bash
302
+ # Existence only — prints the NAME and whether it is set, never the value.
303
+ for v in GEMINI_API_KEY OPENAI_API_KEY ANTHROPIC_API_KEY; do
304
+ test -n "${!v}" && echo "$v: set" || echo "$v: MISSING"
305
+ done
306
+ ```
307
+
308
+ Inventory the providers your written routes actually use (from the ratified
309
+ candidates), then present lane-aware instructions:
310
+
311
+ - **SDK lane (Lane A/B):** keys stay in the customer's own env. List the exact
312
+ env var each provider needs and which are missing. Say plainly: in the SDK
313
+ lane **ModelRig never receives your provider key** — the key stays in your env
314
+ and calls go direct to the provider.
315
+ - **Hosted lane:** keys go in the console vault at **app.modelrig.ai → Keys**
316
+ (`/vault`), placed by the human through the console form.
317
+
318
+ State the pricing fact at this decision point: **BYOK is 1,000,000 free requests
319
+ per month** (you bring the provider key; ModelRig meters, at zero per-request fee
320
+ up to that ceiling). A payment method (app.modelrig.ai → Billing) is only needed
321
+ for metered usage beyond the free tier.
322
+
323
+ **Then, and only if `MODELRIG_API_KEY` is set,** offer to pre-fill the human's
324
+ console checklist with a **metadata-only** manifest — provider NAMES and booleans,
325
+ never a key. Show the EXACT payload first and require an explicit yes:
326
+
327
+ ```bash
328
+ # SHOW this to the human, ask "send this to pre-fill your console checklist?
329
+ # (metadata only — no keys)", and send ONLY on an explicit yes.
330
+ curl -sS https://api.modelrig.ai/v1/setup-needs \
331
+ -H "authorization: Bearer $MODELRIG_API_KEY" \
332
+ -H "content-type: application/json" \
333
+ -d '{
334
+ "manifest_version": 1,
335
+ "lane": "sdk",
336
+ "providers": [{ "provider": "gemini", "has_env_key": true }],
337
+ "routes_count": 2,
338
+ "needs": ["provider_key:gemini", "billing_card"],
339
+ "notes": null
340
+ }'
341
+ ```
342
+
343
+ The server rejects any manifest carrying a secret-shaped value (400 + a teaching
344
+ error) — a backstop, not a licence to send one. **If `MODELRIG_API_KEY` is not
345
+ set, do not send anything** — print the instructions above and stop. On the
346
+ human's next console login, the "Finish your setup" card shows the same items
347
+ with live done/pending states (a key placed in the vault flips its item to done).
348
+
349
+ ---
350
+
351
+ ## Tier 0 — Gateway pointing (transport-only)
352
+
353
+ The lightest possible change: point an existing OpenAI-compatible client at the
354
+ ModelRig **raw lane** by swapping its base URL. Nothing about the request or the
355
+ model changes — the traffic just flows through the gateway, so telemetry and
356
+ cost accounting light up instantly with zero behaviour risk. (The raw lane is
357
+ BYOK and carries no spend envelope — the org daily envelope caps only the managed
358
+ spend ModelRig fronts, not your own key.)
359
+
360
+ Because it is transport-only, the agent **applies it directly after a plan
361
+ review**. This is the correct home for call sites that resist template extraction
362
+ (dynamically-assembled prompts) and for freeform chat / agentic loops — see
363
+ "Stated limits".
364
+
365
+ ## Tier 1 — Instrumentation (behaviour-preserving)
366
+
367
+ Record each pipeline run as a **run → step → artifact** chain — the prompt, the
368
+ raw model text, the parsed output, the evidence it grounded on — with lineage and
369
+ an integrity hash per artifact. This is **behaviour-preserving**: the SDK contract
370
+ is that instrumentation never changes a return value, and with the namespace off
371
+ the pipeline is byte-for-byte identical.
372
+
373
+ Scaffold it with the CLI, then apply the emitted diffs:
374
+
375
+ ```bash
376
+ modelrig observe [path] --scope <one pipeline> # or: modelrig init --observe
377
+ ```
378
+
379
+ `modelrig observe` generates a fresh, self-contained `record-step` seam and an
380
+ `OBSERVE.md` whose per-call-site wiring is emitted as `diff` blocks. The
381
+ instrumentation it produces is a **code-level SDK seam** — there is no zero-code
382
+ observe — but you no longer hand-write it. The agent **plans before editing, then
383
+ applies** (behaviour-preserving), and the validator warns when a stage is
384
+ instrumented that should not be. **Detect existing OpenTelemetry and extend it —
385
+ never duplicate a span the customer already emits.**
386
+
387
+ Full mechanics: [Observe a pipeline](https://modelrig.dev/instrumentation.html).
388
+
389
+ **Tier 1 is not optional for a Lane-B seam.** A pipeline that reaches the model
390
+ through `rig.runRaw` without a `run.start` around it records inferences but no
391
+ run — `/runs` stays empty and `/projects` shows "(no run)". Open the run context
392
+ around each pipeline execution (`rig.artifacts.run.start({ pipeline, episodeKey })`
393
+ … `run.end()`); inside it the SDK names the step and stamps the `run_id` tag —
394
+ and the `step` tag too (a value you pass always wins) — you tag `subject` /
395
+ `feature` yourself. Do not declare a
396
+ migration done until one real execution shows a run with steps on `/runs`.
397
+
398
+ ## Tier 2 — Route extraction + same-model call replacement (behaviour-affecting)
399
+
400
+ Turn a direct provider call into a ModelRig **route**: the prompt becomes a
401
+ `systemTemplate` + declared variables, the schema comes from the call site's Zod /
402
+ TS types (or `schema: null` when it asked for no structure), and the call site
403
+ becomes `rig.run(<route>, …)`. **The replacement is always same-model** — the
404
+ route pins the exact provider/model the original used. This is the C2-exit
405
+ gateway-substitution ruling: T2 changes the *plumbing*, never the *model*. A model
406
+ change is Tier 3, and never the skill's.
407
+
408
+ Because this is behaviour-affecting, the agent **proposes, never applies**:
409
+
410
+ 1. **Draft the route.** `modelrig init --scope <one pipeline>` finds the call
411
+ site and drafts a route bundle + a `MIGRATION.md` line for it. It writes only
412
+ under `modelrig/routes/drafts/` and never edits source. (A route bundle is
413
+ validated at load — missing schema/prompt files, undeclared `{{variables}}`,
414
+ and a bad `json: native` claim all fail loudly, so a drafted bundle either
415
+ loads clean or tells you exactly what is wrong.)
416
+ 2. **Verify before proposing a merge.** The bar is:
417
+ - **type-check** passes,
418
+ - the customer's **own tests** pass, and
419
+ - **`modelrig verify-swap`** returns a passing shadow-equivalence report:
420
+ ```bash
421
+ modelrig verify-swap --route <route> --captures <fixture.json> --out pr-report.md
422
+ ```
423
+ It captures N calls of the original locally, replays the SAME inputs through
424
+ the drafted route **with the original model pinned**, and diffs the outputs.
425
+ Its verdict vocabulary is honest: `EQUIVALENT`, `EQUIVALENT (modulo
426
+ nondeterminism)`, `DIVERGENT` (do not merge), or `REFUSED` (not a T2
427
+ replacement — T0/T1 territory). Exit code is the gate: `0` pass, `1`
428
+ divergent, `2` refused.
429
+ 3. **Embed the report in the PR.** Paste the `pr-report.md` the verifier wrote
430
+ verbatim into the PR body. It carries the plumbing-not-model-choice guarantee
431
+ so a reviewer cannot mistake equivalence for a model-quality claim, and it
432
+ states rollback in the same breath.
433
+ 4. **One call site per PR.** This keeps `git revert` a byte-for-byte restore and
434
+ keeps the diff inside the customer's codebase legible — a product surface.
435
+ 5. **A human merges.** The skill's autonomy ends at a green report and an open PR.
436
+
437
+ ### Replaced-call disposition (RULED, Matt 2026-08-16): DELETE the original, never comment it out
438
+
439
+ Rollback is git-native and the skill makes it explicit:
440
+
441
+ - **(a)** one call site per PR means `git revert` restores the original
442
+ byte-for-byte;
443
+ - **(b)** the PR body quotes the original call **verbatim** with an explicit
444
+ `Rollback: revert this PR` line;
445
+ - **(c)** a one-line breadcrumb comment stays at the site:
446
+ ```ts
447
+ // modelrig: migrated from <original call> — rollback: revert PR #N
448
+ ```
449
+
450
+ **Commented-out originals are banned** — they rot against the live call, and our
451
+ diffs inside customer codebases are a product surface. Because the replacement is
452
+ same-model, behavioural rollback is *additionally* a route-YAML flip, not a code
453
+ operation.
454
+
455
+ ## Tier 3 — Model optimization: NOT the skill's job
456
+
457
+ Choosing a *cheaper or better* model is **permanently out of the skill's hands.**
458
+ The skill's T2 output — routed call sites with same-model parity — is precisely
459
+ the evidence a bake-off needs. From there, optimization stays where it already
460
+ lives: `modelrig bakeoff` replays your own traffic through a challenger, a
461
+ proposal is raised, and **nothing switches** what serves a route without an
462
+ explicit human approval (`modelrig swap execute <id> --approve`). "Actions live in
463
+ your code" — the swap path emits route YAML, it never edits your source. The skill
464
+ recommends; it does not rewrite your model choices.
465
+
466
+ ## Stated limits (the skill flags, never forces)
467
+
468
+ - **Dynamically-assembled prompts** resist template extraction → **Tier 0/1**
469
+ treatment (instrument or point the gateway; do not force a brittle template).
470
+ - **Freeform chat / agentic loops** don't fit task-shaped bundles → **raw lane**
471
+ (a correct outcome, not a failure).
472
+ - **Streaming call sites** are recognized and routed per the batch-first
473
+ deferral — **not silently converted**.
474
+
475
+ Each of these is a call the skill surfaces to you with a reason, never a silent
476
+ downgrade.
477
+
478
+ ## Recognition playbooks (per provider)
479
+
480
+ How the skill recognizes a call site and what the before/after looks like — for
481
+ OpenAI, Anthropic, the Vercel AI SDK, LangChain, and raw `fetch` — lives in the
482
+ [recognition playbooks](https://modelrig.dev/migration-recognition.html), with
483
+ canonical before/after examples for each.
484
+
485
+ ## Quick-reference gotchas
486
+
487
+ The nine things a migrating agent gets wrong from memory. Each is stated in full
488
+ in the tier above; this is the cheat sheet to re-check before proposing a diff.
489
+
490
+ - **Routes resolve from the process CWD** (`./modelrig/routes`) — in a monorepo,
491
+ put `modelrig/` at the app root and run from there, or set `MODELRIG_ROUTES_DIR`.
492
+ - **`await rig.close()` before the process exits**, or telemetry rows are lost and
493
+ the pending flush timers hang the process.
494
+ - **`json: native` is a probe-gated CLAIM, not a preference** — an unprobed
495
+ candidate cannot assert it; the emulation ladder still serves structure.
496
+ - **The `googleSearch` grounding tool must be baked into a cache resource at
497
+ creation** — it cannot be attached to an already-created cache handle later.
498
+ - **Gemini defaults to `temperature` 1.0** (other adapters use their own
499
+ defaults) — carry an explicit classifier temperature over exactly, or it
500
+ silently changes.
501
+ - **The raw lane is BYOK**: pass the provider key on the `apiKey` field of the
502
+ `runRaw` call; ModelRig never receives or stores it.
503
+ - **A combined system+user prompt goes in `userPrompt`** on `runRaw`, not split
504
+ across separate fields.
505
+ - **`tags` require declared dimensions**: declare each tag key you rely on as a
506
+ `dimensions[].key` in `rig.yaml` (tag-safe `[A-Za-z0-9_]{1,64}`). A
507
+ `required: true` dimension missing from a run's tags **warns once per process
508
+ and under-attributes that run on /costs** — the call is not rejected, so the
509
+ only signal is the warning.
510
+ - **Knob support lives in the matrix, not memory**: which provider honors
511
+ reasoning / grounding / responseFormat comes from `rawKnobSupport()` and the
512
+ [raw-lane support matrix](https://modelrig.dev/route-bundles.html) — never assume.