modelrig 0.11.0 → 0.11.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.
@@ -1,4 +1,4 @@
1
- # ModelRig quickstart — see, prove, and save on every model call
1
+ # ModelRig quickstart — See. Grade. Improve.
2
2
 
3
3
  **The question this page answers: how do I get from nothing to my first run on `/runs`, with its steps and artifacts, in about ten minutes?**
4
4
 
@@ -11,11 +11,12 @@ and three things happen at once:
11
11
  artifacts, lineaged and integrity-hashed; see the
12
12
  [instrumentation guide](instrumentation.md)), with cost/tokens/tier and a
13
13
  grade recorded against your tags.
14
- - **Prove** — you get schema-validated output, served only by a model you
15
- authorized, and you can replay that traffic against cheaper candidates in a
14
+ - **Grade** — you get schema-validated output, served only by a model you
15
+ authorized, you attach grades from signals you already have, and you can replay that traffic against cheaper candidates in a
16
16
  bake-off before you change anything.
17
- - **Save** — a winning candidate is a change to your YAML you review; nothing
18
- swaps a model on its own, and your routes are always YAML in your git.
17
+ - **Improve** — a winning candidate is a proposal you review, and nothing swaps
18
+ a model on its own. Never a silent swap: promote it in ModelRig (console or
19
+ MCP; versioned, reversible) or merge it as a PR if you keep routes in git.
19
20
 
20
21
  The path is the same whoever does the work: **mint a key → install → confirm the
21
22
  org → define a route → wrap one pipeline execution in a run → see it on `/runs`.**
@@ -102,9 +103,16 @@ import { createRig, loadConfigFromEnv } from "modelrig";
102
103
  Environment (read once, in ModelRig's config layer only):
103
104
 
104
105
  ```bash
105
- GEMINI_API_KEY=… # only providers you configure get adapters
106
+ # Provider keys: only the providers you configure get adapters. Set the ones
107
+ # your routes (or runRaw calls) use; these are the names the SDK reads.
108
+ GEMINI_API_KEY=…
106
109
  OPENAI_API_KEY=…
107
110
  DEEPSEEK_API_KEY=…
111
+ ANTHROPIC_API_KEY=…
112
+ XAI_API_KEY=… # grok
113
+ ZAI_API_KEY=… # Z.ai / Zhipu
114
+ MODELRIG_DEEPINFRA_API_KEY=…
115
+ MODELRIG_FIREWORKS_API_KEY=…
108
116
 
109
117
  # Where your telemetry goes — pick ONE lane:
110
118
  MODELRIG_API_KEY=rig_sk_… # hosted: rows go to ModelRig,
@@ -119,14 +127,14 @@ MODELRIG_ENVELOPE_BUDGET_USD=25 # default envelope budget
119
127
  ### 2. Confirm the org
120
128
 
121
129
  <!-- BLOCK:CONFIRM_ORG -->
122
- **Confirm the org before you trust an empty console.** Run `modelrig doctor` from the same environment your app runs in. It asks GET /v1/whoami which organization the key belongs to, and the name it prints must match the org badge in the console header. If it names a different organization, your data is landing there: switch the console to that organization, or mint a key in the one you meant (https://modelrig.dev/orgs-and-keys.html).
130
+ **Confirm the org before you trust an empty console.** Run `modelrig doctor` from the same environment your app runs in, once your app has started with the key. It asks GET /v1/whoami which organization the key belongs to and prints the org's name and, in parentheses, its slug; the name must match the org badge in the console header. If it names a different organization, your data is landing there: switch the console to that organization, or mint a key in the one you meant (https://modelrig.dev/orgs-and-keys.html).
123
131
 
124
132
  ```bash
125
133
  npx modelrig doctor
126
- # [modelrig] key rig_sk_…AbCd → org «Your Org» ✓ (ingest https://api.modelrig.ai, …)
134
+ # [modelrig] key rig_sk_…AbCd → org «Your Org» (your-org) ✓ (ingest https://api.modelrig.ai, …)
127
135
 
128
- # No CLI in that environment? Ask the API directly:
129
- curl -H "Authorization: Bearer $MODELRIG_API_KEY" https://api.modelrig.ai/v1/whoami
136
+ # No CLI in that environment? Ask the API directly (same host the SDK uses):
137
+ curl -H "Authorization: Bearer $MODELRIG_API_KEY" "${MODELRIG_INGEST_URL:-https://api.modelrig.ai}/v1/whoami"
130
138
  ```
131
139
  <!-- /BLOCK:CONFIRM_ORG -->
132
140
 
@@ -176,6 +184,14 @@ CRITICAL: respond with a single JSON object, no markdown fences.
176
184
  Variables must be declared in `prompt.variables`; referencing an undeclared
177
185
  variable fails at load time, not at run time.
178
186
 
187
+ The route references two files next to it, and both must exist or the route
188
+ fails to load (`schema file not found` / `prompt.system file not found`).
189
+ `npx modelrig init` writes this starter route's two files,
190
+ `modelrig/routes/schemas/support_summarize.schema.json` and
191
+ `modelrig/routes/prompts/support_summarize.system.md`, without overwriting
192
+ files you already have. Then `npx modelrig validate` checks the bundle with no
193
+ provider key.
194
+
179
195
  The `schema` file is a JSON Schema. ModelRig validates output under **JSON
180
196
  Schema 2020-12** as the primary dialect and **also accepts draft-07** (a schema
181
197
  declaring either `$schema` compiles). Validation runs non-strict with union
@@ -185,6 +201,23 @@ unless you have a draft-07 schema already; both work.
185
201
 
186
202
  ### 4. Call it
187
203
 
204
+ No route yet, because you are observing first or calling providers through a
205
+ `rig.runRaw` seam? You don't need one:
206
+
207
+ <!-- BLOCK:RAW_RIG -->
208
+ **No routes yet? Build the rig raw-only.** Observing a pipeline, or recording calls your code makes itself, needs no route files. Construct the rig with `routesDir: null`: `rig.runRaw`, `rig.runRawStream` and `rig.recordCall` work at once, and `rig.run` / `rig.runBundle` refuse with a teaching error until you add routes. Without it, createRig loads ./modelrig/routes: a directory you configured that is missing throws, and a missing default directory falls back to raw-only with a one-line warning.
209
+
210
+ ```ts
211
+ import { createRig, loadConfigFromEnv } from "modelrig";
212
+
213
+ // Raw-only on purpose: no route files needed. rig.runRaw, rig.runRawStream and
214
+ // rig.recordCall work; rig.run needs routes (add them later, drop routesDir: null).
215
+ const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
216
+ ```
217
+ <!-- /BLOCK:RAW_RIG -->
218
+
219
+ With the route above in place:
220
+
188
221
  ```ts
189
222
  const rig = createRig(loadConfigFromEnv());
190
223
 
@@ -200,7 +233,7 @@ result.meta.costEstimateUsd; // priced from the pinned LiteLLM snapshot
200
233
  result.meta.servedTier; // vs meta.requestedTier — downgrades visible
201
234
  result.meta.attemptsByClass; // retries consumed, per failure class
202
235
 
203
- rig.close();
236
+ await rig.close(); // flushes telemetry; await it once at shutdown (R6)
204
237
  ```
205
238
 
206
239
  ### 5. Wrap it in a run — the standard path
@@ -240,6 +273,37 @@ await rig.close(); // flushes the exporter — call once at shutdown
240
273
 
241
274
  - **Raw lane:** a `rig.runRaw` call inside a run context records a ground-truth
242
275
  step too (since 0.5.0), so grounded/cached pipelines fill the same graph.
276
+ - **A call your code makes itself** (a direct provider client, a stream, an
277
+ embedding) is invisible until you record it:
278
+
279
+ <!-- BLOCK:RECORD_CALL -->
280
+ **Record a call your code makes itself.** A provider call your code makes directly (a client ModelRig has no lane for, a stream you read yourself, an embedding, a fallback) is invisible until you report it. Call `rig.recordCall` right after it returns, inside the run, with the tokens the provider reported: the call shows on /inferences as "recorded · unverified", joins its run and step, and counts toward cost. It is synchronous and never throws; invalid input warns once and records nothing (`inferenceId: null`). Spread optional fields only when the provider reported them, so the call type-checks under `exactOptionalPropertyTypes`.
281
+
282
+ ```ts
283
+ const started = Date.now();
284
+ const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
285
+ const u = resp.usageMetadata;
286
+ const finishReason = resp.candidates?.[0]?.finishReason;
287
+
288
+ const rec = rig.recordCall({
289
+ provider: "gemini",
290
+ model: "gemini-3.1-pro-preview",
291
+ usage: {
292
+ tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
293
+ tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
294
+ tokensCached: u?.cachedContentTokenCount ?? 0,
295
+ // optional fields: spread only when the provider reported them
296
+ ...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
297
+ },
298
+ servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
299
+ latencyMs: Date.now() - started,
300
+ stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
301
+ ...(finishReason !== undefined ? { finishReason } : {}),
302
+ });
303
+ if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
304
+ ```
305
+ <!-- /BLOCK:RECORD_CALL -->
306
+
243
307
  - **Reserved tags:** `run_id` and `step` are stamped by the SDK; the full list of
244
308
  reserved keys is in the
245
309
  [instrumentation guide](instrumentation.md#reserved-tag-keys-the-one-list).
@@ -260,7 +324,9 @@ warns if a rig records calls but never starts a run. If the page is empty:
260
324
  ```text
261
325
  1. npx modelrig doctor, from the environment your app runs in.
262
326
  no export sink configured → this process does not see MODELRIG_API_KEY: set it, redeploy, open a new shell.
327
+ malformed key → the value is truncated, quoted or padded: paste the whole rig_sk_… secret again.
263
328
  AUTH FAILED (401 / 403) → the key is mistyped, revoked or not accepted: mint a key, set it, redeploy, open a new shell.
329
+ whoami unreachable → no network path to the ingest host (firewall, proxy, wrong MODELRIG_INGEST_URL): fix the route out, retry.
264
330
  2. The org doctor prints is not the org in the badge → wrong org: switch the console, or mint a key in the org you meant.
265
331
  3. /inferences has rows but /runs is empty → the calls are not inside run.scope / run.start.
266
332
  4. A run is missing a step or a cost → that call is not on a ModelRig lane (see not-captured).
@@ -268,6 +334,48 @@ warns if a rig records calls but never starts a run. If the page is empty:
268
334
  ```
269
335
  <!-- /BLOCK:EMPTY_CONSOLE -->
270
336
 
337
+ ### 7. Grade it, then prove a cheaper model
338
+
339
+ <!-- BLOCK:GRADE_RUN -->
340
+ **Attach a grade to a call or a whole run.** A grade is your judgment: a 0–1 score with an optional comment, from a validator, a downstream accept/reject, a human or a model. Grade one call by its inference id (`result.meta.inferenceId`, or the `inferenceId` rig.recordCall returns). Grade a whole run by its run id, `run.runId` on the handle `run.start` returns: that grade shows on the run page and on /projects. A run grade whose id is the run's `episodeKey` also shows, on the run page only. `run_id` is a reserved tag the SDK stamps, not a variable your code has. A grade whose id matches no call or run attaches to nothing. The write is synchronous and local and never throws into your pipeline.
341
+
342
+ ```ts
343
+ const run = rig.artifacts.run.start({ pipeline: "support", episodeKey: ticketId, environment: "production" });
344
+ const result = await rig.run("example.support_summarize", {
345
+ input: { ticket, product, priorContext },
346
+ tags: { subject: customer },
347
+ stepKey: "summarize",
348
+ });
349
+ run.end("succeeded");
350
+
351
+ // one call: the inference id rides on every result
352
+ rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "deterministic", grader: "schema-check" });
353
+ // the whole run: its id is run.runId from run.start (shows on the run page and on /projects)
354
+ rig.grade({ kind: "run", id: run.runId }, { score: 0, kind: "human", comment: "missed the refund request" });
355
+ ```
356
+ <!-- /BLOCK:GRADE_RUN -->
357
+
358
+ <!-- BLOCK:BAKEOFF -->
359
+ **Prove a cheaper model on your own traffic.** A bake-off replays your own inputs through the variants a route declares and reports the effective cost of conformance for each. `--variants` names entries in the route's `variants:` block (`default` is the route as declared), never model ids, and a variant can only narrow the route's candidates. Set `capture: true` first: replay works on traffic captured after that, not before. With no captured traffic (a run-based pipeline), promote a real /runs output to a golden (the console's make golden, or `promoteToEval`) and use `--from-eval-cases <task>`; its variants come from the route bundle named after the task. A bake-off measures and never switches anything.
360
+
361
+ ```bash
362
+ # 1. in modelrig/routes/example.support_summarize.yaml: capture traffic, and declare
363
+ # a cheaper variant (narrowing only: its candidates must be in the route's list)
364
+ capture: true
365
+ variants:
366
+ - name: cheap
367
+ candidates:
368
+ - provider: gemini
369
+ model: gemini-3.1-flash-lite
370
+
371
+ # 2. after traffic accumulates, compare the route as declared (default) with cheap
372
+ npx modelrig bakeoff --route example.support_summarize --variants default,cheap --replay-last 50
373
+
374
+ # no captured route traffic? promote a real /runs output to a golden, then
375
+ npx modelrig bakeoff --from-eval-cases example.support_summarize --variants default,cheap
376
+ ```
377
+ <!-- /BLOCK:BAKEOFF -->
378
+
271
379
  ## Change your route's model live (no redeploy)
272
380
 
273
381
  A route's **call-config** — which model/provider serves it, and its sampling — can be
@@ -375,7 +483,9 @@ const { bundle: runnable, arm } = resolveCallConfigForRun(bundle, resolved, {
375
483
  input, // stickyKey (if set) reads its unit from here…
376
484
  // unitKey: userId, // …or pass the unit key explicitly (sticky assignment)
377
485
  });
378
- const result = await rig.runBundle(runnable, { input, tags, arm });
486
+ // arm is string | null and RunOptions.arm is optional: pass it only when one was
487
+ // drawn (a bare `arm` fails to compile under exactOptionalPropertyTypes).
488
+ const result = await rig.runBundle(runnable, { input, tags, ...(arm !== null ? { arm } : {}) });
379
489
  // arm is null for a pin / no-override / non-experiment call ⇒ no meta.arm (byte-identical).
380
490
  ```
381
491
 
@@ -419,7 +529,7 @@ key never burns backoff time. Failures that still billed tokens (refusals,
419
529
  truncations) carry their token usage, so envelopes and telemetry account the
420
530
  real spend.
421
531
 
422
- Each class — what it means, whether it retries, and how it backs off — is on [Routing & reliability](routing-reliability.md#5-failure-classes); how the whole candidate ladder runs (ordering, validate → repair → fall-through, and what is not built) is on that page too.
532
+ Each class — what it means, whether it retries, and how it backs off — is on [Routing & reliability](routing-reliability.md#5-failure-classes); that page also covers how the whole candidate ladder runs: the ordering, the validate → repair → fall-through sequence, and the parts that are not built yet.
423
533
 
424
534
  ## Where the telemetry goes
425
535
 
@@ -427,7 +537,9 @@ Every attempt (not just every run) writes one row to package-owned SQLite,
427
537
  tagged with your `tags` — always, with no key. Setting `MODELRIG_API_KEY` turns
428
538
  on **api export mode**: a background exporter ships batches to the ingest API
429
539
  (`api.modelrig.ai`, override with `MODELRIG_INGEST_URL`), which the console
430
- reads — fire-and-forget, queue-on-failure; the inference path never blocks on it.
540
+ reads — fire-and-forget: a transient failure (network, 5xx) is retried, a
541
+ rejected key (401/403) drops the rows and logs `AUTH FAILED`; the inference path
542
+ never blocks on it.
431
543
  Inspect live rows in your local SQLite file, or in the console once the key is
432
544
  set — never a database URL of your own.
433
545
 
@@ -438,7 +550,7 @@ only the web tier silently loses every row a worker or cron produced.
438
550
 
439
551
  Turning **Optimization on** for a route (`capture: true`) is the **recommended
440
552
  default** — your traffic is what makes the routing smarter and the bill smaller,
441
- and everything it keeps you can see, export and delete in the console. Keeping the
553
+ and everything it keeps you can see and export in the console. Keeping the
442
554
  prompts, outputs and evidence too is [content custody](content-custody.md), which
443
555
  is **on by default** — every new org is created managed, having accepted the
444
556
  published terms at signup — and captures into your managed store, scrubbed for the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelrig",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "private": false,
5
5
  "description": "ModelRig — capability-aware AI gateway: routes-as-code, provider adapters, typed failures, budget envelopes, and local-first telemetry.",
6
6
  "license": "Apache-2.0",