modelrig 0.10.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.
- package/CHANGELOG.md +96 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -3
- package/dist/doctor-cli.d.ts.map +1 -1
- package/dist/doctor-cli.js +2 -1
- package/dist/doctor-preflight.d.ts +14 -2
- package/dist/doctor-preflight.d.ts.map +1 -1
- package/dist/doctor-preflight.js +65 -5
- package/dist/feedback/coverage.d.ts.map +1 -1
- package/dist/feedback/coverage.js +9 -2
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -8
- package/dist/init/init-cli.d.ts.map +1 -1
- package/dist/init/init-cli.js +21 -4
- package/dist/init/observe-cli.js +1 -1
- package/dist/init/observe.d.ts +7 -1
- package/dist/init/observe.d.ts.map +1 -1
- package/dist/init/observe.js +74 -22
- package/dist/init/starter.d.ts +36 -0
- package/dist/init/starter.d.ts.map +1 -0
- package/dist/init/starter.js +85 -0
- package/dist/run/raw-finalize.d.ts +49 -3
- package/dist/run/raw-finalize.d.ts.map +1 -1
- package/dist/run/raw-finalize.js +31 -14
- package/dist/run/raw-search-cost.d.ts +3 -2
- package/dist/run/raw-search-cost.d.ts.map +1 -1
- package/dist/run/raw-search-cost.js +1 -0
- package/dist/run/rawonly.d.ts +29 -5
- package/dist/run/rawonly.d.ts.map +1 -1
- package/dist/run/rawonly.js +54 -19
- package/dist/run/record-call.d.ts +104 -0
- package/dist/run/record-call.d.ts.map +1 -0
- package/dist/run/record-call.js +277 -0
- package/dist/telemetry/attempt-meta.d.ts +11 -0
- package/dist/telemetry/attempt-meta.d.ts.map +1 -1
- package/dist/telemetry/attempt-meta.js +3 -0
- package/dist/telemetry/lanes.d.ts +31 -0
- package/dist/telemetry/lanes.d.ts.map +1 -0
- package/dist/telemetry/lanes.js +41 -0
- package/dist/telemetry/org-identity.d.ts +5 -2
- package/dist/telemetry/org-identity.d.ts.map +1 -1
- package/dist/telemetry/org-identity.js +10 -8
- package/docs/agent-skill/SKILL.md +109 -10
- package/docs/how-it-fits.md +1 -1
- package/docs/instrumentation.md +152 -40
- package/docs/llms.txt +92 -9
- package/docs/quickstart.md +127 -15
- package/package.json +1 -1
package/docs/how-it-fits.md
CHANGED
|
@@ -12,7 +12,7 @@ What is retained beyond that is your choice, per route, and there are two lanes.
|
|
|
12
12
|
|
|
13
13
|
**Pure router** (capture off). Nothing retained. Real, supported, selectable, and the honest answer for a ZDR-strict or data-residency-bound workload.
|
|
14
14
|
|
|
15
|
-
**Optimization on** (recommended). Your traffic is what makes the routing smarter and the bill smaller. **What is kept:** per-inference telemetry rows, and bounded review samples for the bake-offs you create. **Where:** your organization, row-scoped by the database, not by our application code. **How you leave:** see it all in the console
|
|
15
|
+
**Optimization on** (recommended). Your traffic is what makes the routing smarter and the bill smaller. **What is kept:** per-inference telemetry rows, and bounded review samples for the bake-offs you create. **Where:** your organization, row-scoped by the database, not by our application code. **How you leave:** see it all in the console and export it; never a silent swap — promote a change in ModelRig (console or MCP; versioned, reversible) or merge it as a PR if you keep routes in git. **What you get back:** community priors on your very first bake-off and RigIndex-grade evidence on your own routes — at the same flat 2% either way; the corpus earns its place by being useful, not by being a discount.
|
|
16
16
|
|
|
17
17
|
Captures power **bake-offs**: replay your own recent traffic through route variants — a cheaper model, a different prompt — and get the effective cost of conformance for each, with confidence intervals. A winning variant is a proposal for you to apply in your YAML; nothing swaps models automatically.
|
|
18
18
|
|
package/docs/instrumentation.md
CHANGED
|
@@ -37,8 +37,6 @@ path — never blurred with the telemetry axis here.
|
|
|
37
37
|
# set_org_settings MCP tool), not this env var.
|
|
38
38
|
export MODELRIG_ARTIFACTS=1 # local-only namespace toggle: force ON
|
|
39
39
|
# with =1 on a rig with no control plane
|
|
40
|
-
export MODELRIG_ARTIFACT_STEPS=step_a,step_b # optional: instrument a subset
|
|
41
|
-
# (unset/empty = every instrumented step)
|
|
42
40
|
```
|
|
43
41
|
|
|
44
42
|
Rows land in the local telemetry buffer and export to the control plane through
|
|
@@ -54,38 +52,60 @@ agent-driven setup should plan that one handoff rather than stall on it.
|
|
|
54
52
|
The namespace hangs off `rig.artifacts`. Two moving parts: a **run context**
|
|
55
53
|
around the whole pipeline, and a **per-step save** of the work products.
|
|
56
54
|
|
|
55
|
+
No routes yet? Observing needs none. Build the rig raw-only:
|
|
56
|
+
|
|
57
|
+
<!-- BLOCK:RAW_RIG -->
|
|
58
|
+
**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.
|
|
59
|
+
|
|
57
60
|
```ts
|
|
58
61
|
import { createRig, loadConfigFromEnv } from "modelrig";
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
// Raw-only on purpose: no route files needed. rig.runRaw, rig.runRawStream and
|
|
64
|
+
// rig.recordCall work; rig.run needs routes (add them later, drop routesDir: null).
|
|
65
|
+
const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
66
|
+
```
|
|
67
|
+
<!-- /BLOCK:RAW_RIG -->
|
|
61
68
|
|
|
69
|
+
Then wrap the pipeline. Here each step's model call stays your own direct call,
|
|
70
|
+
reported with `rig.recordCall` so the step carries tokens and a cost:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
62
73
|
// 1. Open a run context around the pipeline. episodeKey bridges grades;
|
|
63
74
|
// it is a grouping key, not a unique id — a natural choice is your own
|
|
64
75
|
// per-run attempt id. environment fills the Env column on /runs.
|
|
65
76
|
const run = rig.artifacts.run.start({
|
|
66
77
|
pipeline: "my-pipeline",
|
|
67
78
|
episodeKey: runAttemptId,
|
|
68
|
-
environment: process.env.NODE_ENV,
|
|
79
|
+
environment: process.env.NODE_ENV ?? "development", // e.g. "production", "staging", "test"
|
|
69
80
|
});
|
|
70
81
|
try {
|
|
71
82
|
for (const step of steps) {
|
|
72
83
|
const prompt = assemble(step);
|
|
73
|
-
const
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
const result = await callModel(prompt); // your own direct model call
|
|
86
|
+
|
|
87
|
+
// 2. Report the call: tokens → cost on the step and on /inferences.
|
|
88
|
+
rig.recordCall({
|
|
89
|
+
provider: "openai",
|
|
90
|
+
model: "gpt-5.4-mini",
|
|
91
|
+
usage: { tokensIn: result.usage.inputTokens, tokensOut: result.usage.outputTokens },
|
|
92
|
+
latencyMs: Date.now() - started,
|
|
93
|
+
stepKey: step.key,
|
|
94
|
+
});
|
|
74
95
|
|
|
75
|
-
//
|
|
96
|
+
// 3. Save the step's artifacts. stepKey groups them in the console's
|
|
76
97
|
// chain view; task carries the semantic task name.
|
|
77
|
-
const p = rig.artifacts.artifact.save(prompt, { name: `${step}.prompt`, type: "prompt", stepKey: step });
|
|
78
|
-
const raw = rig.artifacts.artifact.save(result.rawText, { name: `${step}.raw`, type: "raw_response", stepKey: step });
|
|
98
|
+
const p = rig.artifacts.artifact.save(prompt, { name: `${step.key}.prompt`, type: "prompt", stepKey: step.key });
|
|
99
|
+
const raw = rig.artifacts.artifact.save(result.rawText, { name: `${step.key}.raw`, type: "raw_response", stepKey: step.key });
|
|
79
100
|
const parsed = rig.artifacts.artifact.save(result.json, {
|
|
80
|
-
name: `${step}.output`, type: "step_output", task: step, stepKey: step, schema: step.schema,
|
|
101
|
+
name: `${step.key}.output`, type: "step_output", task: step.key, stepKey: step.key, schema: step.schema,
|
|
81
102
|
});
|
|
82
103
|
|
|
83
|
-
//
|
|
84
|
-
// prompt consumed the previous step's output.
|
|
104
|
+
// 4. Lineage: the parsed output derives from the prompt + raw.
|
|
85
105
|
if (p && parsed) rig.artifacts.artifact.link(parsed, p, "derived_from");
|
|
86
106
|
if (raw && parsed) rig.artifacts.artifact.link(parsed, raw, "derived_from");
|
|
87
107
|
|
|
88
|
-
//
|
|
108
|
+
// 5. Grades (optional, deterministic): flag conformance, degraded
|
|
89
109
|
// provenance, anything you can decide without an LLM judge.
|
|
90
110
|
if (parsed) rig.grade({ kind: "artifact", id: parsed.id }, {
|
|
91
111
|
score: conforms(result.json) ? 1 : 0, kind: "deterministic", grader: "schema-check",
|
|
@@ -96,8 +116,13 @@ try {
|
|
|
96
116
|
run.end("failed");
|
|
97
117
|
throw err;
|
|
98
118
|
}
|
|
119
|
+
await rig.close(); // once, at shutdown
|
|
99
120
|
```
|
|
100
121
|
|
|
122
|
+
Here `step` is your own `{ key, schema }` record, and `callModel` returns the
|
|
123
|
+
provider's text, parsed JSON and usage. A call routed through `rig.run` or
|
|
124
|
+
`rig.runRaw` records its own step, so it needs no `recordCall`.
|
|
125
|
+
|
|
101
126
|
For **concurrent** runs in one process, use `rig.artifacts.run.scope(opts, fn)`
|
|
102
127
|
instead of `start`/`end` — each scope keeps its own ambient context so
|
|
103
128
|
overlapping runs never cross-attribute. `run.start` uses `enterWith`, which is
|
|
@@ -111,9 +136,55 @@ right for one run at a time on a request/async path.
|
|
|
111
136
|
- A **step** is an execution row. `rig.run()` auto-creates a **ground-truth**
|
|
112
137
|
step (its inference is real, so cost and latency are measured) when it runs
|
|
113
138
|
inside a run context. A stage whose model call does *not* go through the rig
|
|
114
|
-
|
|
115
|
-
and
|
|
116
|
-
|
|
139
|
+
and is not reported with `rig.recordCall` produces artifacts with `step_key`
|
|
140
|
+
set and **no step row**, which is correct and honest: the console renders
|
|
141
|
+
"cost n/a" for that step rather than inventing a number. Report the call
|
|
142
|
+
(below) and the step gets its tokens and cost.
|
|
143
|
+
|
|
144
|
+
### Record a call your code made itself (`rig.recordCall`)
|
|
145
|
+
|
|
146
|
+
_Needs `modelrig` 0.11.0 or newer._
|
|
147
|
+
|
|
148
|
+
If a stage's model call has to stay a direct provider call, report it instead:
|
|
149
|
+
`rig.recordCall({ provider, model, usage: { tokensIn, tokensOut, tokensCached }, servedTier, latencyMs, stepKey })`
|
|
150
|
+
right after the call returns. Inside a run context it records a step row for
|
|
151
|
+
that `stepKey` (stored as `ground-truth`, the only joined-inference value),
|
|
152
|
+
joined to the recorded inference, so the step gets a cost instead of "cost n/a".
|
|
153
|
+
The console shows that step and its call as **recorded · unverified**, never as
|
|
154
|
+
measured:
|
|
155
|
+
ModelRig neither dispatched nor validated the call, and the tokens are the ones
|
|
156
|
+
you reported. It is synchronous and never throws (bad input warns and returns
|
|
157
|
+
`inferenceId: null`); the returned `inferenceId` is gradeable with `rig.grade`.
|
|
158
|
+
Full contract, usage convention and cost notes:
|
|
159
|
+
[Not captured → record them](not-captured.md#calls-your-code-makes-itself-record-them).
|
|
160
|
+
|
|
161
|
+
<!-- BLOCK:RECORD_CALL -->
|
|
162
|
+
**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`.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const started = Date.now();
|
|
166
|
+
const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
|
|
167
|
+
const u = resp.usageMetadata;
|
|
168
|
+
const finishReason = resp.candidates?.[0]?.finishReason;
|
|
169
|
+
|
|
170
|
+
const rec = rig.recordCall({
|
|
171
|
+
provider: "gemini",
|
|
172
|
+
model: "gemini-3.1-pro-preview",
|
|
173
|
+
usage: {
|
|
174
|
+
tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
|
|
175
|
+
tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
|
|
176
|
+
tokensCached: u?.cachedContentTokenCount ?? 0,
|
|
177
|
+
// optional fields: spread only when the provider reported them
|
|
178
|
+
...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
|
|
179
|
+
},
|
|
180
|
+
servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
|
|
181
|
+
latencyMs: Date.now() - started,
|
|
182
|
+
stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
|
|
183
|
+
...(finishReason !== undefined ? { finishReason } : {}),
|
|
184
|
+
});
|
|
185
|
+
if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
|
|
186
|
+
```
|
|
187
|
+
<!-- /BLOCK:RECORD_CALL -->
|
|
117
188
|
|
|
118
189
|
## Tags, dimensions, and the graph
|
|
119
190
|
|
|
@@ -220,7 +291,9 @@ const { bundle: runnable, arm } = resolveCallConfigForRun(bundle, resolved, { in
|
|
|
220
291
|
|
|
221
292
|
// Thread the arm onto the run so it lands on inferences.meta.arm.
|
|
222
293
|
await rig.artifacts.run.scope({ pipeline: "report", episodeKey: runId }, async () => {
|
|
223
|
-
|
|
294
|
+
// arm is string | null; RunOptions.arm is optional, so pass it only when drawn
|
|
295
|
+
// (a bare `arm` fails under exactOptionalPropertyTypes).
|
|
296
|
+
const out = await rig.runBundle(runnable, { input, tags: { subject, feature }, stepKey, ...(arm !== null ? { arm } : {}) });
|
|
224
297
|
rig.artifacts.artifact.save(out.output, { name: "section.output", type: "step_output" });
|
|
225
298
|
});
|
|
226
299
|
```
|
|
@@ -277,14 +350,14 @@ orgs and the console picker: [Orgs and keys](https://modelrig.dev/orgs-and-keys.
|
|
|
277
350
|
Check first that the console is showing the org your key exports to:
|
|
278
351
|
|
|
279
352
|
<!-- BLOCK:CONFIRM_ORG -->
|
|
280
|
-
**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
|
|
353
|
+
**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).
|
|
281
354
|
|
|
282
355
|
```bash
|
|
283
356
|
npx modelrig doctor
|
|
284
|
-
# [modelrig] key rig_sk_…AbCd → org «Your Org» ✓ (ingest https://api.modelrig.ai, …)
|
|
357
|
+
# [modelrig] key rig_sk_…AbCd → org «Your Org» (your-org) ✓ (ingest https://api.modelrig.ai, …)
|
|
285
358
|
|
|
286
|
-
# No CLI in that environment? Ask the API directly:
|
|
287
|
-
curl -H "Authorization: Bearer $MODELRIG_API_KEY" https://api.modelrig.ai/v1/whoami
|
|
359
|
+
# No CLI in that environment? Ask the API directly (same host the SDK uses):
|
|
360
|
+
curl -H "Authorization: Bearer $MODELRIG_API_KEY" "${MODELRIG_INGEST_URL:-https://api.modelrig.ai}/v1/whoami"
|
|
288
361
|
```
|
|
289
362
|
<!-- /BLOCK:CONFIRM_ORG -->
|
|
290
363
|
|
|
@@ -302,22 +375,33 @@ an AI — attaches to a subject: one model call (an **inference**), a whole run
|
|
|
302
375
|
[optimization coverage](grade-protocol.md) is built from — the evidence level
|
|
303
376
|
that decides how much confidence a swap proposal carries.
|
|
304
377
|
|
|
378
|
+
<!-- BLOCK:GRADE_RUN -->
|
|
379
|
+
**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.
|
|
380
|
+
|
|
305
381
|
```ts
|
|
306
|
-
|
|
307
|
-
const result = await rig.run("example.support_summarize", {
|
|
308
|
-
|
|
382
|
+
const run = rig.artifacts.run.start({ pipeline: "support", episodeKey: ticketId, environment: "production" });
|
|
383
|
+
const result = await rig.run("example.support_summarize", {
|
|
384
|
+
input: { ticket, product, priorContext },
|
|
385
|
+
tags: { subject: customer },
|
|
386
|
+
stepKey: "summarize",
|
|
387
|
+
});
|
|
388
|
+
run.end("succeeded");
|
|
309
389
|
|
|
310
|
-
//
|
|
311
|
-
rig.grade({ kind: "
|
|
390
|
+
// one call: the inference id rides on every result
|
|
391
|
+
rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "deterministic", grader: "schema-check" });
|
|
392
|
+
// the whole run: its id is run.runId from run.start (shows on the run page and on /projects)
|
|
393
|
+
rig.grade({ kind: "run", id: run.runId }, { score: 0, kind: "human", comment: "missed the refund request" });
|
|
312
394
|
```
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
`{ kind: "
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
is
|
|
395
|
+
<!-- /BLOCK:GRADE_RUN -->
|
|
396
|
+
|
|
397
|
+
- **The subject** is `{ kind: "inference", id }` (`result.meta.inferenceId`, or
|
|
398
|
+
the `inferenceId` `rig.recordCall` returns), `{ kind: "run", id }` (the run's
|
|
399
|
+
id, `run.runId` from `run.start`; a grade carrying the run's `episodeKey`
|
|
400
|
+
also shows, on the run page only), or `{ kind: "artifact", id }`. **`opts`**
|
|
401
|
+
is `{ score, scale?, comment?, kind, grader?, source? }` — the `score` in
|
|
402
|
+
`[0, 1]` is the primitive (up = 1, down = 0), and the pass/fail verdict is
|
|
403
|
+
derived from it against the route threshold. A subject naming no known
|
|
404
|
+
inference, run, or artifact attaches to nothing — attribution is never guessed.
|
|
321
405
|
- **Emit it from a signal you already have.** Most pipelines already decide
|
|
322
406
|
pass/fail somewhere — a schema validator, a downstream accept/reject, a human
|
|
323
407
|
QA gate. Emit `rig.grade` from that (score 1 on a clean pass, 0 on a rejection)
|
|
@@ -351,15 +435,40 @@ const caseId = rig.artifacts.artifact.promoteToEval(handle, { task: "report.sect
|
|
|
351
435
|
|
|
352
436
|
```bash
|
|
353
437
|
# Replay the promoted eval cases through variants — the raw/run-based lane.
|
|
438
|
+
# The variants (default, cheap) are read from the route bundle named report.section.
|
|
354
439
|
modelrig bakeoff --from-eval-cases report.section --variants default,cheap
|
|
355
440
|
```
|
|
356
441
|
|
|
357
442
|
- `promoteToEval(ref, { task?, corrected?, origin? })` binds the case to a route by
|
|
358
443
|
`task` (from `opts`, else the task the artifact was saved with this session), and
|
|
359
444
|
is also reachable from the console's one-click **"make golden"**.
|
|
360
|
-
- `--route`
|
|
361
|
-
|
|
362
|
-
|
|
445
|
+
- `--route` replays **captured route traffic**; `--from-eval-cases <task>` replays
|
|
446
|
+
the promoted cases instead, so it is the path for a `runRaw` seam with no
|
|
447
|
+
captured traffic. Both read the variants from a route bundle (`--route`, or the
|
|
448
|
+
bundle named `<task>`): selecting the cases needs no routes directory, but a
|
|
449
|
+
case that replays needs that bundle and its `variants:` block. Either way a
|
|
450
|
+
bake-off **measures** — it never switches what serves a route; never a silent swap — promote it in ModelRig (console or MCP; versioned, reversible) or merge it as a PR if you keep routes in git.
|
|
451
|
+
|
|
452
|
+
<!-- BLOCK:BAKEOFF -->
|
|
453
|
+
**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.
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# 1. in modelrig/routes/example.support_summarize.yaml: capture traffic, and declare
|
|
457
|
+
# a cheaper variant (narrowing only: its candidates must be in the route's list)
|
|
458
|
+
capture: true
|
|
459
|
+
variants:
|
|
460
|
+
- name: cheap
|
|
461
|
+
candidates:
|
|
462
|
+
- provider: gemini
|
|
463
|
+
model: gemini-3.1-flash-lite
|
|
464
|
+
|
|
465
|
+
# 2. after traffic accumulates, compare the route as declared (default) with cheap
|
|
466
|
+
npx modelrig bakeoff --route example.support_summarize --variants default,cheap --replay-last 50
|
|
467
|
+
|
|
468
|
+
# no captured route traffic? promote a real /runs output to a golden, then
|
|
469
|
+
npx modelrig bakeoff --from-eval-cases example.support_summarize --variants default,cheap
|
|
470
|
+
```
|
|
471
|
+
<!-- /BLOCK:BAKEOFF -->
|
|
363
472
|
|
|
364
473
|
## A reusable per-step seam
|
|
365
474
|
|
|
@@ -371,15 +480,18 @@ as diffs to apply — it never edits your source. The seam is equally yours to
|
|
|
371
480
|
write by hand; here it is, self-contained:
|
|
372
481
|
|
|
373
482
|
```ts
|
|
374
|
-
import { createRig, loadConfigFromEnv, type Rig } from "modelrig";
|
|
483
|
+
import { createRig, loadConfigFromEnv, type RecordCallInput, type Rig } from "modelrig";
|
|
375
484
|
|
|
376
|
-
|
|
485
|
+
// routesDir: null — the seam needs no route files, so importing it never throws.
|
|
486
|
+
const rig: Rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
377
487
|
|
|
378
|
-
/**
|
|
488
|
+
/** Report the call (tokens → cost), save prompt + raw + parsed, link lineage,
|
|
489
|
+
* flag conformance. */
|
|
379
490
|
function recordStep(
|
|
380
491
|
stepKey: string,
|
|
381
|
-
work: { prompt: string; rawText: string; json: unknown; conforms: boolean },
|
|
492
|
+
work: { prompt: string; rawText: string; json: unknown; conforms: boolean; call?: Omit<RecordCallInput, "stepKey"> },
|
|
382
493
|
): void {
|
|
494
|
+
if (work.call !== undefined) rig.recordCall({ ...work.call, stepKey });
|
|
383
495
|
const p = rig.artifacts.artifact.save(work.prompt, { name: `${stepKey}.prompt`, type: "prompt", stepKey });
|
|
384
496
|
const raw = rig.artifacts.artifact.save(work.rawText, { name: `${stepKey}.raw`, type: "raw_response", stepKey });
|
|
385
497
|
const parsed = rig.artifacts.artifact.save(work.json, { name: `${stepKey}.output`, type: "step_output", task: stepKey, stepKey });
|
package/docs/llms.txt
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> grounding, and caching measured empirically, with a capability-aware
|
|
5
5
|
> gateway that routes by them. Registry data CC BY 4.0; code Apache-2.0.
|
|
6
6
|
|
|
7
|
-
_This reference is as of modelrig v0.
|
|
7
|
+
_This reference is as of modelrig v0.11.1 (the published npm package). Any surface below that a newer workspace adds is marked "unreleased — workspace only" until its release; nothing is so marked as of this version._
|
|
8
8
|
|
|
9
9
|
## Docs
|
|
10
10
|
|
|
@@ -60,14 +60,14 @@ _The default ingest host is https://api.modelrig.ai (override with `MODELRIG_ING
|
|
|
60
60
|
MODELRIG_API_KEY=rig_sk_…
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
**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
|
|
63
|
+
**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).
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
66
|
npx modelrig doctor
|
|
67
|
-
# [modelrig] key rig_sk_…AbCd → org «Your Org» ✓ (ingest https://api.modelrig.ai, …)
|
|
67
|
+
# [modelrig] key rig_sk_…AbCd → org «Your Org» (your-org) ✓ (ingest https://api.modelrig.ai, …)
|
|
68
68
|
|
|
69
|
-
# No CLI in that environment? Ask the API directly:
|
|
70
|
-
curl -H "Authorization: Bearer $MODELRIG_API_KEY" https://api.modelrig.ai/v1/whoami
|
|
69
|
+
# No CLI in that environment? Ask the API directly (same host the SDK uses):
|
|
70
|
+
curl -H "Authorization: Bearer $MODELRIG_API_KEY" "${MODELRIG_INGEST_URL:-https://api.modelrig.ai}/v1/whoami"
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
**Wrap one pipeline execution in a run.** Every pipeline execution is a run. Wrap each execution in `run.scope` (or `run.start` / `run.end`), name one step per model call with `stepKey`, `artifact.save` the work products, and pass `environment` so the Env column on /runs is filled. Inside the run the SDK stamps `run_id` and `step` on every call. `run_id` is reserved: don't set it yourself.
|
|
@@ -96,29 +96,112 @@ await rig.artifacts.run.scope(
|
|
|
96
96
|
await rig.close(); // flushes the exporter — call once at shutdown
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
**What ModelRig does not see.** ModelRig records the calls your code makes through the SDK (`rig.run`, `rig.runBundle`, `rig.runRaw`, `rig.runRawStream`) or the gateway, and
|
|
99
|
+
**What ModelRig does not see unless you record it.** ModelRig records the calls your code makes through the SDK (`rig.run`, `rig.runBundle`, `rig.runRaw`, `rig.runRawStream`) or the gateway, plus any call your code makes itself and reports with `rig.recordCall`. Nothing else is seen. Five things stay invisible or unattached unless you record them: (1) direct provider SDK calls, with or without a JSON schema; (2) streams you read from a provider client yourself; (3) a fallback your code makes after the raw lane refuses a `responseSchema` it cannot enforce (deepseek, deepinfra, fireworks and zai fail closed with `invariant_violation`); (4) calls made outside `run.scope` / `run.start`, which land on /inferences but on no run; (5) embeddings. A recorded call shows as "recorded · unverified": ModelRig neither dispatched nor validated it, and its tokens are the numbers you reported. Details and the console tells: https://modelrig.dev/not-captured.html.
|
|
100
100
|
|
|
101
101
|
**The console is empty: five checks, in order.** If /runs (or /inferences) is empty for the org in the header badge, work these checks in order. The full tree is https://modelrig.dev/troubleshooting.html.
|
|
102
102
|
|
|
103
103
|
```text
|
|
104
104
|
1. npx modelrig doctor, from the environment your app runs in.
|
|
105
105
|
no export sink configured → this process does not see MODELRIG_API_KEY: set it, redeploy, open a new shell.
|
|
106
|
+
malformed key → the value is truncated, quoted or padded: paste the whole rig_sk_… secret again.
|
|
106
107
|
AUTH FAILED (401 / 403) → the key is mistyped, revoked or not accepted: mint a key, set it, redeploy, open a new shell.
|
|
108
|
+
whoami unreachable → no network path to the ingest host (firewall, proxy, wrong MODELRIG_INGEST_URL): fix the route out, retry.
|
|
107
109
|
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.
|
|
108
110
|
3. /inferences has rows but /runs is empty → the calls are not inside run.scope / run.start.
|
|
109
111
|
4. A run is missing a step or a cost → that call is not on a ModelRig lane (see not-captured).
|
|
110
112
|
5. A doc describes something your install lacks → compare `npm ls modelrig` with the version stamp in modelrig.dev/llms.txt.
|
|
111
113
|
```
|
|
112
114
|
|
|
113
|
-
**After you change the key on a hosted worker.** A hosted process reads the environment it was started with. After you set or change MODELRIG_API_KEY, wait for the redeploy to finish, then open a new shell: a shell attached before the deploy still has the old environment. Confirm from inside it that the key resolves to the org you expect.
|
|
115
|
+
**After you change the key on a hosted worker.** A hosted process reads the environment it was started with. After you set or change MODELRIG_API_KEY, wait for the redeploy to finish, then open a new shell: a shell attached before the deploy still has the old environment. Confirm from inside it that the key resolves to the org you expect: `--expect-org` takes the org's slug, the part in parentheses in doctor's output.
|
|
114
116
|
|
|
115
117
|
```bash
|
|
116
|
-
npx modelrig doctor --expect-org your-org
|
|
118
|
+
npx modelrig doctor --expect-org your-org # exits non-zero if the key belongs to another org
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**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.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { createRig, loadConfigFromEnv } from "modelrig";
|
|
125
|
+
|
|
126
|
+
// Raw-only on purpose: no route files needed. rig.runRaw, rig.runRawStream and
|
|
127
|
+
// rig.recordCall work; rig.run needs routes (add them later, drop routesDir: null).
|
|
128
|
+
const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**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`.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
|
|
136
|
+
const u = resp.usageMetadata;
|
|
137
|
+
const finishReason = resp.candidates?.[0]?.finishReason;
|
|
138
|
+
|
|
139
|
+
const rec = rig.recordCall({
|
|
140
|
+
provider: "gemini",
|
|
141
|
+
model: "gemini-3.1-pro-preview",
|
|
142
|
+
usage: {
|
|
143
|
+
tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
|
|
144
|
+
tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
|
|
145
|
+
tokensCached: u?.cachedContentTokenCount ?? 0,
|
|
146
|
+
// optional fields: spread only when the provider reported them
|
|
147
|
+
...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
|
|
148
|
+
},
|
|
149
|
+
servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
|
|
150
|
+
latencyMs: Date.now() - started,
|
|
151
|
+
stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
|
|
152
|
+
...(finishReason !== undefined ? { finishReason } : {}),
|
|
153
|
+
});
|
|
154
|
+
if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**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.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const run = rig.artifacts.run.start({ pipeline: "support", episodeKey: ticketId, environment: "production" });
|
|
161
|
+
const result = await rig.run("example.support_summarize", {
|
|
162
|
+
input: { ticket, product, priorContext },
|
|
163
|
+
tags: { subject: customer },
|
|
164
|
+
stepKey: "summarize",
|
|
165
|
+
});
|
|
166
|
+
run.end("succeeded");
|
|
167
|
+
|
|
168
|
+
// one call: the inference id rides on every result
|
|
169
|
+
rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "deterministic", grader: "schema-check" });
|
|
170
|
+
// the whole run: its id is run.runId from run.start (shows on the run page and on /projects)
|
|
171
|
+
rig.grade({ kind: "run", id: run.runId }, { score: 0, kind: "human", comment: "missed the refund request" });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**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.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# 1. in modelrig/routes/example.support_summarize.yaml: capture traffic, and declare
|
|
178
|
+
# a cheaper variant (narrowing only: its candidates must be in the route's list)
|
|
179
|
+
capture: true
|
|
180
|
+
variants:
|
|
181
|
+
- name: cheap
|
|
182
|
+
candidates:
|
|
183
|
+
- provider: gemini
|
|
184
|
+
model: gemini-3.1-flash-lite
|
|
185
|
+
|
|
186
|
+
# 2. after traffic accumulates, compare the route as declared (default) with cheap
|
|
187
|
+
npx modelrig bakeoff --route example.support_summarize --variants default,cheap --replay-last 50
|
|
188
|
+
|
|
189
|
+
# no captured route traffic? promote a real /runs output to a golden, then
|
|
190
|
+
npx modelrig bakeoff --from-eval-cases example.support_summarize --variants default,cheap
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Pin the org a deployment must export to.** `--expect-org` and `expectOrg` take the org's slug, not its display name. `modelrig doctor` prints both, as `→ org «Your Org» (your-org) ✓`: the slug is the part in parentheses. In the console, hover the org badge in the header to see it. With `--expect-org <slug>`, doctor exits non-zero when the key belongs to another org, so a deploy check fails loudly instead of exporting somewhere else.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// The same check in code: the exporter asks whoami on its first flush and logs
|
|
197
|
+
// ORG MISMATCH on every flush while the key belongs to another org.
|
|
198
|
+
const rig = createRig({ ...loadConfigFromEnv(), expectOrg: "your-org" });
|
|
117
199
|
```
|
|
118
200
|
|
|
119
201
|
## Capabilities (the full surface)
|
|
120
202
|
|
|
121
203
|
- **Runs & projects** — the run → step → artifact graph at /runs, grouped by tag at /projects (the standard path above): https://modelrig.dev/instrumentation.html.
|
|
204
|
+
- **Recorded calls (`rig.recordCall`, modelrig ≥ 0.11.0)** — a provider call your code makes itself (no lane fits) is reported after it returns: one row, priced at list for the served tier from the tokens you report, joined to the run/step, gradeable, badged "recorded · unverified" (ModelRig neither dispatched nor validated it), never in routed/conformance math: https://modelrig.dev/not-captured.html.
|
|
122
205
|
- **Tenants & statements** — a `subject` tag or project-scoped key becomes a per-customer tenant with a derived statement (attempted, verified, billable, true cost; never content or another org's data): https://modelrig.dev/tenants.html.
|
|
123
206
|
- **Published receipts** — a signed, login-free receipt of a bake-off or route at /r/<id>, verifiable by anyone at /v1/verify/<id>; internal names aliased, content never leaves: https://modelrig.dev/receipts.html.
|
|
124
207
|
- **Analytics query API** — a self-describing, read-only query surface over your own telemetry (`GET /v1/analytics/meta`, `POST /v1/analytics/query`): an allowlisted SELECT-only grammar that returns metadata only (enums, numbers, hashes), never content: https://modelrig.dev/optimization-loop.html.
|
|
@@ -134,7 +217,7 @@ npx modelrig doctor --expect-org your-org-slug # exits non-zero if the key bel
|
|
|
134
217
|
- Rates below are sampled statistics with confidence intervals in the source data — do not quote them without their n and date.
|
|
135
218
|
- For PRICING, do not recall list prices — read them: the leaderboard (https://modelrig.dev/leaderboard.html) ranks models by effective cost per 1,000 schema-conformant outputs (list price ≠ what conformance costs), and each model's per-token rates ride the registry JSON with as_of stamps. Quote the effective cost, not the sticker rate.
|
|
136
219
|
- Attach a GRADE to what you run — capture records what ran, a grade records whether it was good: `rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "human" })` (or `{ kind: "run", id: run_id }` for a whole run) from a signal you already have (a validator, a downstream accept/reject, a human QA gate). A grade is a 0–1 score with an optional comment; up = 1, down = 0. It is a synchronous local write, exporter-mirrored, and never blocks or throws. A route with ten attached grades reaches Rung 1 on /coverage; a route with no schema to conform to (a prose output) gains proposal confidence only this way. From a non-SDK surface, `POST /v1/grade` with a grade-scoped `rig_sk_` key. See https://modelrig.dev/grade-protocol.html.
|
|
137
|
-
- Prove a cheaper model on YOUR own traffic on the lane that fits: a DECLARED route uses `modelrig bakeoff --route <route> --replay-last N`; a raw/run-based pipeline (a `rig.runRaw` seam, no routes) promotes a real /runs output to a golden (`promoteToEval`, or the console's one-click "make golden") and runs `modelrig bakeoff --from-eval-cases <task> --variants default,<challenger>`. A bake-off measures; it never switches what serves a route — a swap
|
|
220
|
+
- Prove a cheaper model on YOUR own traffic on the lane that fits: a DECLARED route uses `modelrig bakeoff --route <route> --replay-last N`; a raw/run-based pipeline (a `rig.runRaw` seam, no routes) promotes a real /runs output to a golden (`promoteToEval`, or the console's one-click "make golden") and runs `modelrig bakeoff --from-eval-cases <task> --variants default,<challenger>`. A bake-off measures; it never switches what serves a route — never a silent swap — promote it in ModelRig (console or MCP; versioned, reversible) or merge it as a PR if you keep routes in git. See https://modelrig.dev/bakeoffs.html.
|
|
138
221
|
|
|
139
222
|
## Ask this data directly (MCP)
|
|
140
223
|
|