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.
- package/CHANGELOG.md +48 -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-preflight.d.ts.map +1 -1
- package/dist/doctor-preflight.js +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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/rawonly.d.ts +29 -5
- package/dist/run/rawonly.d.ts.map +1 -1
- package/dist/run/rawonly.js +54 -19
- package/docs/agent-skill/SKILL.md +107 -10
- package/docs/how-it-fits.md +1 -1
- package/docs/instrumentation.md +135 -40
- package/docs/llms.txt +90 -8
- package/docs/quickstart.md +127 -15
- package/package.json +1 -1
|
@@ -65,14 +65,14 @@ MODELRIG_API_KEY=rig_sk_…
|
|
|
65
65
|
<!-- /BLOCK:KEY -->
|
|
66
66
|
|
|
67
67
|
<!-- BLOCK:CONFIRM_ORG -->
|
|
68
|
-
**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
|
|
68
|
+
**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).
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
npx modelrig doctor
|
|
72
|
-
# [modelrig] key rig_sk_…AbCd → org «Your Org» ✓ (ingest https://api.modelrig.ai, …)
|
|
72
|
+
# [modelrig] key rig_sk_…AbCd → org «Your Org» (your-org) ✓ (ingest https://api.modelrig.ai, …)
|
|
73
73
|
|
|
74
|
-
# No CLI in that environment? Ask the API directly:
|
|
75
|
-
curl -H "Authorization: Bearer $MODELRIG_API_KEY" https://api.modelrig.ai/v1/whoami
|
|
74
|
+
# No CLI in that environment? Ask the API directly (same host the SDK uses):
|
|
75
|
+
curl -H "Authorization: Bearer $MODELRIG_API_KEY" "${MODELRIG_INGEST_URL:-https://api.modelrig.ai}/v1/whoami"
|
|
76
76
|
```
|
|
77
77
|
<!-- /BLOCK:CONFIRM_ORG -->
|
|
78
78
|
|
|
@@ -112,7 +112,103 @@ await rig.close(); // flushes the exporter — call once at shutdown
|
|
|
112
112
|
**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.
|
|
113
113
|
<!-- /BLOCK:NOT_CAPTURED -->
|
|
114
114
|
|
|
115
|
-
A provider call you can't move onto a lane (an unsupported client, a stream you read yourself, a fallback): record it
|
|
115
|
+
A provider call you can't move onto a lane (an unsupported client, a stream you read yourself, a fallback, an embedding): record it right after it returns, never leave it invisible (https://modelrig.dev/not-captured.html):
|
|
116
|
+
|
|
117
|
+
<!-- BLOCK:RECORD_CALL -->
|
|
118
|
+
**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`.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const started = Date.now();
|
|
122
|
+
const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
|
|
123
|
+
const u = resp.usageMetadata;
|
|
124
|
+
const finishReason = resp.candidates?.[0]?.finishReason;
|
|
125
|
+
|
|
126
|
+
const rec = rig.recordCall({
|
|
127
|
+
provider: "gemini",
|
|
128
|
+
model: "gemini-3.1-pro-preview",
|
|
129
|
+
usage: {
|
|
130
|
+
tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
|
|
131
|
+
tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
|
|
132
|
+
tokensCached: u?.cachedContentTokenCount ?? 0,
|
|
133
|
+
// optional fields: spread only when the provider reported them
|
|
134
|
+
...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
|
|
135
|
+
},
|
|
136
|
+
servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
|
|
137
|
+
latencyMs: Date.now() - started,
|
|
138
|
+
stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
|
|
139
|
+
...(finishReason !== undefined ? { finishReason } : {}),
|
|
140
|
+
});
|
|
141
|
+
if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
|
|
142
|
+
```
|
|
143
|
+
<!-- /BLOCK:RECORD_CALL -->
|
|
144
|
+
|
|
145
|
+
No routes yet (observe, or a `runRaw` seam)? Build the rig raw-only:
|
|
146
|
+
|
|
147
|
+
<!-- BLOCK:RAW_RIG -->
|
|
148
|
+
**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.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { createRig, loadConfigFromEnv } from "modelrig";
|
|
152
|
+
|
|
153
|
+
// Raw-only on purpose: no route files needed. rig.runRaw, rig.runRawStream and
|
|
154
|
+
// rig.recordCall work; rig.run needs routes (add them later, drop routesDir: null).
|
|
155
|
+
const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
156
|
+
```
|
|
157
|
+
<!-- /BLOCK:RAW_RIG -->
|
|
158
|
+
|
|
159
|
+
Grade from a signal you already have, and prove a cheaper model on your own traffic:
|
|
160
|
+
|
|
161
|
+
<!-- BLOCK:GRADE_RUN -->
|
|
162
|
+
**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.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const run = rig.artifacts.run.start({ pipeline: "support", episodeKey: ticketId, environment: "production" });
|
|
166
|
+
const result = await rig.run("example.support_summarize", {
|
|
167
|
+
input: { ticket, product, priorContext },
|
|
168
|
+
tags: { subject: customer },
|
|
169
|
+
stepKey: "summarize",
|
|
170
|
+
});
|
|
171
|
+
run.end("succeeded");
|
|
172
|
+
|
|
173
|
+
// one call: the inference id rides on every result
|
|
174
|
+
rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "deterministic", grader: "schema-check" });
|
|
175
|
+
// the whole run: its id is run.runId from run.start (shows on the run page and on /projects)
|
|
176
|
+
rig.grade({ kind: "run", id: run.runId }, { score: 0, kind: "human", comment: "missed the refund request" });
|
|
177
|
+
```
|
|
178
|
+
<!-- /BLOCK:GRADE_RUN -->
|
|
179
|
+
|
|
180
|
+
<!-- BLOCK:BAKEOFF -->
|
|
181
|
+
**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.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# 1. in modelrig/routes/example.support_summarize.yaml: capture traffic, and declare
|
|
185
|
+
# a cheaper variant (narrowing only: its candidates must be in the route's list)
|
|
186
|
+
capture: true
|
|
187
|
+
variants:
|
|
188
|
+
- name: cheap
|
|
189
|
+
candidates:
|
|
190
|
+
- provider: gemini
|
|
191
|
+
model: gemini-3.1-flash-lite
|
|
192
|
+
|
|
193
|
+
# 2. after traffic accumulates, compare the route as declared (default) with cheap
|
|
194
|
+
npx modelrig bakeoff --route example.support_summarize --variants default,cheap --replay-last 50
|
|
195
|
+
|
|
196
|
+
# no captured route traffic? promote a real /runs output to a golden, then
|
|
197
|
+
npx modelrig bakeoff --from-eval-cases example.support_summarize --variants default,cheap
|
|
198
|
+
```
|
|
199
|
+
<!-- /BLOCK:BAKEOFF -->
|
|
200
|
+
|
|
201
|
+
On a hosted worker, pin the org a deployment must export to:
|
|
202
|
+
|
|
203
|
+
<!-- BLOCK:EXPECT_ORG -->
|
|
204
|
+
**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.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// The same check in code: the exporter asks whoami on its first flush and logs
|
|
208
|
+
// ORG MISMATCH on every flush while the key belongs to another org.
|
|
209
|
+
const rig = createRig({ ...loadConfigFromEnv(), expectOrg: "your-org" });
|
|
210
|
+
```
|
|
211
|
+
<!-- /BLOCK:EXPECT_ORG -->
|
|
116
212
|
|
|
117
213
|
Which lane fits a codebase (observe, the `runRaw` seam, or named routes):
|
|
118
214
|
<https://modelrig.dev/choosing-a-lane.html>. Hosted workers:
|
|
@@ -122,11 +218,12 @@ Which lane fits a codebase (observe, the `runRaw` seam, or named routes):
|
|
|
122
218
|
|
|
123
219
|
Before migrating anything, you can just **watch** an existing pipeline. ModelRig
|
|
124
220
|
records each run as a run → step → artifact chain you click through in the
|
|
125
|
-
console — no routes moved, no model calls rerouted, and with
|
|
221
|
+
console — no routes moved, no model calls rerouted, and with no key set the
|
|
126
222
|
pipeline behaves byte-for-byte as before. Scaffold it:
|
|
127
223
|
`npx modelrig observe [path] --scope <one pipeline>` writes a self-contained
|
|
128
|
-
`record-step` seam
|
|
129
|
-
|
|
224
|
+
`record-step` seam (a raw-only rig, `routesDir: null`, that reports each call
|
|
225
|
+
with `rig.recordCall` so steps carry a cost) and an `OBSERVE.md` whose wiring,
|
|
226
|
+
including `environment` on the run, is emitted as diffs you apply. The instrumentation is a code-level SDK seam (no zero-code observe), but
|
|
130
227
|
you no longer hand-write it — see <https://modelrig.dev/instrumentation.html>.
|
|
131
228
|
The namespace is **on by default once a control plane is configured** (a
|
|
132
229
|
`rig_sk_` API key, or the self-host Supabase pair); a pure local-only rig stays
|
|
@@ -150,8 +247,8 @@ behaviour-affecting, so the agent *proposes* it, proves it with
|
|
|
150
247
|
never the skill's job.
|
|
151
248
|
|
|
152
249
|
**Runs are the standard path, not an appendix.** Every pipeline execution should
|
|
153
|
-
be a **run**: wrap it in `rig.artifacts.run.start` / `run.scope`, name
|
|
154
|
-
|
|
250
|
+
be a **run**: wrap it in `rig.artifacts.run.start` / `run.scope`, name each
|
|
251
|
+
model call's step with `stepKey`, `artifact.save` the work products, and tag
|
|
155
252
|
`subject` / `feature`. Inside a run context the SDK auto-stamps `tags.run_id` and
|
|
156
253
|
`tags.step` for you, so the run fills `/runs` **and** `/projects`; acceptance is
|
|
157
254
|
the run appearing on `/runs` with its steps. `run_id` is reserved: never set it
|
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,10 @@ 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.
|
|
117
143
|
|
|
118
144
|
### Record a call your code made itself (`rig.recordCall`)
|
|
119
145
|
|
|
@@ -132,6 +158,34 @@ you reported. It is synchronous and never throws (bad input warns and returns
|
|
|
132
158
|
Full contract, usage convention and cost notes:
|
|
133
159
|
[Not captured → record them](not-captured.md#calls-your-code-makes-itself-record-them).
|
|
134
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 -->
|
|
188
|
+
|
|
135
189
|
## Tags, dimensions, and the graph
|
|
136
190
|
|
|
137
191
|
ModelRig has **two** grouping mechanisms, and they compose:
|
|
@@ -237,7 +291,9 @@ const { bundle: runnable, arm } = resolveCallConfigForRun(bundle, resolved, { in
|
|
|
237
291
|
|
|
238
292
|
// Thread the arm onto the run so it lands on inferences.meta.arm.
|
|
239
293
|
await rig.artifacts.run.scope({ pipeline: "report", episodeKey: runId }, async () => {
|
|
240
|
-
|
|
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 } : {}) });
|
|
241
297
|
rig.artifacts.artifact.save(out.output, { name: "section.output", type: "step_output" });
|
|
242
298
|
});
|
|
243
299
|
```
|
|
@@ -294,14 +350,14 @@ orgs and the console picker: [Orgs and keys](https://modelrig.dev/orgs-and-keys.
|
|
|
294
350
|
Check first that the console is showing the org your key exports to:
|
|
295
351
|
|
|
296
352
|
<!-- BLOCK:CONFIRM_ORG -->
|
|
297
|
-
**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).
|
|
298
354
|
|
|
299
355
|
```bash
|
|
300
356
|
npx modelrig doctor
|
|
301
|
-
# [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, …)
|
|
302
358
|
|
|
303
|
-
# No CLI in that environment? Ask the API directly:
|
|
304
|
-
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"
|
|
305
361
|
```
|
|
306
362
|
<!-- /BLOCK:CONFIRM_ORG -->
|
|
307
363
|
|
|
@@ -319,22 +375,33 @@ an AI — attaches to a subject: one model call (an **inference**), a whole run
|
|
|
319
375
|
[optimization coverage](grade-protocol.md) is built from — the evidence level
|
|
320
376
|
that decides how much confidence a swap proposal carries.
|
|
321
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
|
+
|
|
322
381
|
```ts
|
|
323
|
-
|
|
324
|
-
const result = await rig.run("example.support_summarize", {
|
|
325
|
-
|
|
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");
|
|
326
389
|
|
|
327
|
-
//
|
|
328
|
-
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" });
|
|
329
394
|
```
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
`{ kind: "
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
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.
|
|
338
405
|
- **Emit it from a signal you already have.** Most pipelines already decide
|
|
339
406
|
pass/fail somewhere — a schema validator, a downstream accept/reject, a human
|
|
340
407
|
QA gate. Emit `rig.grade` from that (score 1 on a clean pass, 0 on a rejection)
|
|
@@ -368,15 +435,40 @@ const caseId = rig.artifacts.artifact.promoteToEval(handle, { task: "report.sect
|
|
|
368
435
|
|
|
369
436
|
```bash
|
|
370
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.
|
|
371
439
|
modelrig bakeoff --from-eval-cases report.section --variants default,cheap
|
|
372
440
|
```
|
|
373
441
|
|
|
374
442
|
- `promoteToEval(ref, { task?, corrected?, origin? })` binds the case to a route by
|
|
375
443
|
`task` (from `opts`, else the task the artifact was saved with this session), and
|
|
376
444
|
is also reachable from the console's one-click **"make golden"**.
|
|
377
|
-
- `--route`
|
|
378
|
-
|
|
379
|
-
|
|
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 -->
|
|
380
472
|
|
|
381
473
|
## A reusable per-step seam
|
|
382
474
|
|
|
@@ -388,15 +480,18 @@ as diffs to apply — it never edits your source. The seam is equally yours to
|
|
|
388
480
|
write by hand; here it is, self-contained:
|
|
389
481
|
|
|
390
482
|
```ts
|
|
391
|
-
import { createRig, loadConfigFromEnv, type Rig } from "modelrig";
|
|
483
|
+
import { createRig, loadConfigFromEnv, type RecordCallInput, type Rig } from "modelrig";
|
|
392
484
|
|
|
393
|
-
|
|
485
|
+
// routesDir: null — the seam needs no route files, so importing it never throws.
|
|
486
|
+
const rig: Rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
394
487
|
|
|
395
|
-
/**
|
|
488
|
+
/** Report the call (tokens → cost), save prompt + raw + parsed, link lineage,
|
|
489
|
+
* flag conformance. */
|
|
396
490
|
function recordStep(
|
|
397
491
|
stepKey: string,
|
|
398
|
-
work: { prompt: string; rawText: string; json: unknown; conforms: boolean },
|
|
492
|
+
work: { prompt: string; rawText: string; json: unknown; conforms: boolean; call?: Omit<RecordCallInput, "stepKey"> },
|
|
399
493
|
): void {
|
|
494
|
+
if (work.call !== undefined) rig.recordCall({ ...work.call, stepKey });
|
|
400
495
|
const p = rig.artifacts.artifact.save(work.prompt, { name: `${stepKey}.prompt`, type: "prompt", stepKey });
|
|
401
496
|
const raw = rig.artifacts.artifact.save(work.rawText, { name: `${stepKey}.raw`, type: "raw_response", stepKey });
|
|
402
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.11.
|
|
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.
|
|
@@ -103,17 +103,99 @@ await rig.close(); // flushes the exporter — call once at shutdown
|
|
|
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)
|
|
@@ -135,7 +217,7 @@ npx modelrig doctor --expect-org your-org-slug # exits non-zero if the key bel
|
|
|
135
217
|
- Rates below are sampled statistics with confidence intervals in the source data — do not quote them without their n and date.
|
|
136
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.
|
|
137
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.
|
|
138
|
-
- 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.
|
|
139
221
|
|
|
140
222
|
## Ask this data directly (MCP)
|
|
141
223
|
|