modelrig 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +86 -0
- package/dist/bakeoff/disagreement.d.ts +93 -0
- package/dist/bakeoff/disagreement.d.ts.map +1 -0
- package/dist/bakeoff/disagreement.js +140 -0
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +58 -2
- package/dist/bakeoff/pairwise.d.ts +102 -0
- package/dist/bakeoff/pairwise.d.ts.map +1 -0
- package/dist/bakeoff/pairwise.js +214 -0
- package/dist/bakeoff/replay.d.ts +6 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +6 -2
- package/dist/bakeoff/report.d.ts +7 -0
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +46 -0
- package/dist/bakeoff/types.d.ts +24 -0
- package/dist/bakeoff/types.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -9
- package/dist/mask/apply.d.ts +80 -0
- package/dist/mask/apply.d.ts.map +1 -0
- package/dist/mask/apply.js +228 -0
- package/dist/mask/detect.d.ts +38 -0
- package/dist/mask/detect.d.ts.map +1 -0
- package/dist/mask/detect.js +167 -0
- package/dist/mask/index.d.ts +21 -0
- package/dist/mask/index.d.ts.map +1 -0
- package/dist/mask/index.js +54 -0
- package/dist/mask/mask-policy-cli.d.ts +11 -0
- package/dist/mask/mask-policy-cli.d.ts.map +1 -0
- package/dist/mask/mask-policy-cli.js +33 -0
- package/dist/mask/policy.d.ts +38 -0
- package/dist/mask/policy.d.ts.map +1 -0
- package/dist/mask/policy.js +187 -0
- package/dist/mask/proof.d.ts +54 -0
- package/dist/mask/proof.d.ts.map +1 -0
- package/dist/mask/proof.js +120 -0
- package/dist/mask/receipt.d.ts +30 -0
- package/dist/mask/receipt.d.ts.map +1 -0
- package/dist/mask/receipt.js +49 -0
- package/dist/mask/seam.d.ts +53 -0
- package/dist/mask/seam.d.ts.map +1 -0
- package/dist/mask/seam.js +120 -0
- package/dist/mask/types.d.ts +81 -0
- package/dist/mask/types.d.ts.map +1 -0
- package/dist/mask/types.js +10 -0
- package/dist/mask/vault.d.ts +50 -0
- package/dist/mask/vault.d.ts.map +1 -0
- package/dist/mask/vault.js +81 -0
- package/dist/receipts/types.d.ts +65 -0
- package/dist/receipts/types.d.ts.map +1 -0
- package/dist/receipts/types.js +134 -0
- package/dist/routes/loader-warnings.d.ts +8 -0
- package/dist/routes/loader-warnings.d.ts.map +1 -1
- package/dist/routes/loader-warnings.js +23 -0
- package/dist/routes/loader.d.ts +1 -1
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +24 -25
- package/dist/routes/mask-loader.d.ts +18 -0
- package/dist/routes/mask-loader.d.ts.map +1 -0
- package/dist/routes/mask-loader.js +39 -0
- package/dist/routes/objective-loader.d.ts.map +1 -1
- package/dist/routes/objective-loader.js +51 -17
- package/dist/routes/screen-loader.d.ts +23 -0
- package/dist/routes/screen-loader.d.ts.map +1 -0
- package/dist/routes/screen-loader.js +79 -0
- package/dist/routes/template.d.ts +10 -0
- package/dist/routes/template.d.ts.map +1 -1
- package/dist/routes/template.js +10 -3
- package/dist/run/accept.d.ts +4 -0
- package/dist/run/accept.d.ts.map +1 -1
- package/dist/run/accept.js +2 -0
- package/dist/run/dispatch.d.ts +35 -0
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +33 -0
- package/dist/run/grounding.d.ts +47 -2
- package/dist/run/grounding.d.ts.map +1 -1
- package/dist/run/grounding.js +113 -6
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +6 -1
- package/dist/run/record-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +12 -1
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +19 -16
- package/dist/screen/provenance.d.ts +59 -0
- package/dist/screen/provenance.d.ts.map +1 -0
- package/dist/screen/provenance.js +104 -0
- package/dist/screen/types.d.ts +71 -0
- package/dist/screen/types.d.ts.map +1 -0
- package/dist/screen/types.js +9 -0
- package/dist/status-cli.d.ts +11 -0
- package/dist/status-cli.d.ts.map +1 -0
- package/dist/status-cli.js +21 -0
- package/dist/status-runs.d.ts +38 -0
- package/dist/status-runs.d.ts.map +1 -0
- package/dist/status-runs.js +80 -0
- package/dist/status.d.ts +7 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -2
- package/dist/telemetry/ingest-protocol.d.ts +8 -0
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +11 -0
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +12 -3
- package/dist/types/provider.d.ts +11 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +9 -0
- package/dist/types/route.d.ts +55 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +7 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts.map +1 -1
- package/dist/validate-cli.js +13 -0
- package/dist/verify/types.d.ts +6 -0
- package/dist/verify/types.d.ts.map +1 -1
- package/docs/agent-skill/SKILL.md +131 -0
- package/docs/how-it-fits.md +34 -0
- package/docs/instrumentation.md +221 -0
- package/docs/llms.txt +276 -0
- package/docs/migration-playbook.md +512 -0
- package/docs/quickstart.md +339 -0
- package/package.json +18 -2
- 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.
|