modelrig 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/dist/adapters/anthropic.d.ts +4 -4
- package/dist/adapters/anthropic.d.ts.map +1 -1
- package/dist/adapters/anthropic.js +8 -1
- package/dist/adapters/deepseek.d.ts +2 -4
- package/dist/adapters/deepseek.d.ts.map +1 -1
- package/dist/adapters/deepseek.js +9 -5
- package/dist/adapters/gemini.d.ts +4 -1
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +16 -1
- package/dist/adapters/grok.d.ts +2 -4
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +8 -4
- package/dist/adapters/history-mapping.d.ts +126 -0
- package/dist/adapters/history-mapping.d.ts.map +1 -0
- package/dist/adapters/history-mapping.js +230 -0
- package/dist/adapters/openai-compat.d.ts +2 -4
- package/dist/adapters/openai-compat.d.ts.map +1 -1
- package/dist/adapters/openai-compat.js +9 -5
- package/dist/adapters/openai.d.ts +2 -4
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +8 -4
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +9 -4
- package/dist/artifacts/artifact.d.ts.map +1 -1
- package/dist/artifacts/artifact.js +79 -0
- package/dist/artifacts/conversation.d.ts +128 -0
- package/dist/artifacts/conversation.d.ts.map +1 -0
- package/dist/artifacts/conversation.js +172 -0
- package/dist/artifacts/index.d.ts +1 -1
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/media.d.ts +46 -0
- package/dist/artifacts/media.d.ts.map +1 -0
- package/dist/artifacts/media.js +88 -0
- package/dist/artifacts/run-context.d.ts +61 -2
- package/dist/artifacts/run-context.d.ts.map +1 -1
- package/dist/artifacts/run-context.js +183 -12
- package/dist/artifacts/scrub.d.ts +3 -0
- package/dist/artifacts/scrub.d.ts.map +1 -1
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +34 -7
- package/dist/bakeoff/eval-cases.d.ts +5 -0
- package/dist/bakeoff/eval-cases.d.ts.map +1 -1
- package/dist/bakeoff/eval-cases.js +10 -1
- package/dist/bakeoff/replay.d.ts +15 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +19 -1
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +9 -2
- package/dist/bakeoff/turn-replay.d.ts +93 -0
- package/dist/bakeoff/turn-replay.d.ts.map +1 -0
- package/dist/bakeoff/turn-replay.js +150 -0
- package/dist/bakeoff/types.d.ts +12 -0
- package/dist/bakeoff/types.d.ts.map +1 -1
- 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/feedback/grade.d.ts +7 -2
- package/dist/feedback/grade.d.ts.map +1 -1
- package/dist/feedback/grade.js +14 -0
- package/dist/feedback/outcome.d.ts +103 -0
- package/dist/feedback/outcome.d.ts.map +1 -0
- package/dist/feedback/outcome.js +175 -0
- package/dist/gateway/history-bridge.d.ts +32 -0
- package/dist/gateway/history-bridge.d.ts.map +1 -0
- package/dist/gateway/history-bridge.js +64 -0
- package/dist/gateway/serve-client.d.ts.map +1 -1
- package/dist/gateway/serve-client.js +6 -0
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -9
- 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/registry/build.d.ts +4 -0
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/generation-models.d.ts +68 -0
- package/dist/registry/generation-models.d.ts.map +1 -0
- package/dist/registry/generation-models.js +168 -0
- package/dist/registry/model-kind.d.ts +34 -0
- package/dist/registry/model-kind.d.ts.map +1 -0
- package/dist/registry/model-kind.js +40 -0
- package/dist/registry/pricing-shape.d.ts +29 -1
- package/dist/registry/pricing-shape.d.ts.map +1 -1
- package/dist/registry/pricing-shape.js +9 -0
- package/dist/registry/pricing.d.ts +26 -0
- package/dist/registry/pricing.d.ts.map +1 -1
- package/dist/registry/pricing.js +29 -0
- package/dist/registry/rows.d.ts.map +1 -1
- package/dist/registry/rows.js +3 -1
- package/dist/registry/unit-pricing.d.ts +32 -0
- package/dist/registry/unit-pricing.d.ts.map +1 -0
- package/dist/registry/unit-pricing.js +62 -0
- package/dist/routes/input-loader.d.ts +38 -0
- package/dist/routes/input-loader.d.ts.map +1 -0
- package/dist/routes/input-loader.js +80 -0
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +3 -0
- package/dist/routes/resolved-bundle.d.ts +2 -0
- package/dist/routes/resolved-bundle.d.ts.map +1 -1
- package/dist/routes/resolved-bundle.js +3 -0
- package/dist/run/cost.d.ts +28 -0
- package/dist/run/cost.d.ts.map +1 -1
- package/dist/run/cost.js +74 -0
- package/dist/run/dispatch.d.ts +4 -1
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +3 -0
- package/dist/run/limits.d.ts +88 -0
- package/dist/run/limits.d.ts.map +1 -0
- package/dist/run/limits.js +155 -0
- package/dist/run/messages.d.ts +199 -0
- package/dist/run/messages.d.ts.map +1 -0
- package/dist/run/messages.js +430 -0
- package/dist/run/raw-finalize.d.ts +5 -2
- package/dist/run/raw-finalize.d.ts.map +1 -1
- package/dist/run/raw-finalize.js +8 -5
- package/dist/run/raw-request.d.ts.map +1 -1
- package/dist/run/raw-request.js +2 -1
- package/dist/run/raw.d.ts +10 -4
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +11 -3
- 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-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +16 -6
- package/dist/run/record-call.d.ts +13 -2
- package/dist/run/record-call.d.ts.map +1 -1
- package/dist/run/record-call.js +50 -7
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +12 -8
- package/dist/telemetry/attempt-meta.d.ts +49 -1
- package/dist/telemetry/attempt-meta.d.ts.map +1 -1
- package/dist/telemetry/attempt-meta.js +45 -1
- package/dist/telemetry/exporter.d.ts.map +1 -1
- package/dist/telemetry/exporter.js +2 -1
- package/dist/telemetry/grade-mappers.d.ts +1 -1
- package/dist/telemetry/grade-mappers.d.ts.map +1 -1
- package/dist/telemetry/grade-mappers.js +7 -1
- package/dist/telemetry/grade-task.d.ts +49 -0
- package/dist/telemetry/grade-task.d.ts.map +1 -0
- package/dist/telemetry/grade-task.js +85 -0
- package/dist/telemetry/grades-sql.d.ts +16 -1
- package/dist/telemetry/grades-sql.d.ts.map +1 -1
- package/dist/telemetry/grades-sql.js +64 -4
- package/dist/telemetry/ingest-protocol.d.ts +24 -1
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/ingest-protocol.js +72 -43
- package/dist/telemetry/otel-content.d.ts +31 -0
- package/dist/telemetry/otel-content.d.ts.map +1 -0
- package/dist/telemetry/otel-content.js +106 -0
- package/dist/telemetry/otel-hierarchy.d.ts +57 -0
- package/dist/telemetry/otel-hierarchy.d.ts.map +1 -0
- package/dist/telemetry/otel-hierarchy.js +155 -0
- package/dist/telemetry/otel-normalize.d.ts +15 -1
- package/dist/telemetry/otel-normalize.d.ts.map +1 -1
- package/dist/telemetry/otel-normalize.js +24 -39
- package/dist/telemetry/sinks.d.ts +4 -1
- package/dist/telemetry/sinks.d.ts.map +1 -1
- package/dist/telemetry/sinks.js +53 -34
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +8 -0
- package/dist/telemetry/store.d.ts +9 -0
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +10 -0
- package/dist/telemetry/uniform-rows.d.ts +43 -0
- package/dist/telemetry/uniform-rows.d.ts.map +1 -0
- package/dist/telemetry/uniform-rows.js +139 -0
- package/dist/types/failures-teaching.d.ts.map +1 -1
- package/dist/types/failures-teaching.js +11 -0
- package/dist/types/failures.d.ts +1 -1
- package/dist/types/failures.d.ts.map +1 -1
- package/dist/types/failures.js +2 -0
- package/dist/types/provider.d.ts +12 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/route.d.ts +13 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +14 -1
- package/dist/types/telemetry.d.ts.map +1 -1
- package/docs/agent-skill/SKILL.md +122 -12
- package/docs/how-it-fits.md +4 -4
- package/docs/instrumentation.md +148 -44
- package/docs/llms.txt +143 -13
- package/docs/mcp-oracle.md +15 -4
- package/docs/quickstart.md +129 -16
- package/package.json +4 -2
- package/registry/registry.json +456 -0
- package/templates/creative-approval.json +81 -0
- package/templates/support-conversation.json +82 -0
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 -->
|
|
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:
|
|
61
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,13 +116,25 @@ 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
|
|
104
129
|
right for one run at a time on a request/async path.
|
|
105
130
|
|
|
131
|
+
**Why a run failed.** *(needs `modelrig` 0.12.0 or newer)* End a failed run with its
|
|
132
|
+
reason — `run.end("failed", { failureReason: "eps_integrity_failure" })` — or,
|
|
133
|
+
inside `run.scope`, call `rig.artifacts.run.setFailureReason("…")` before the
|
|
134
|
+
callback throws. The reason must be an identifier (≤128 characters, no
|
|
135
|
+
whitespace). It lands in `runs.meta.failure_reason`, and the run-outcome verdict
|
|
136
|
+
on `/runs` uses it as the failed run's label instead of a step's failure class.
|
|
137
|
+
|
|
106
138
|
## Steps vs. artifacts
|
|
107
139
|
|
|
108
140
|
- An **artifact** is a work product — a durable thing your pipeline made. It
|
|
@@ -111,27 +143,58 @@ right for one run at a time on a request/async path.
|
|
|
111
143
|
- A **step** is an execution row. `rig.run()` auto-creates a **ground-truth**
|
|
112
144
|
step (its inference is real, so cost and latency are measured) when it runs
|
|
113
145
|
inside a run context. A stage whose model call does *not* go through the rig
|
|
114
|
-
|
|
115
|
-
and
|
|
116
|
-
|
|
146
|
+
and is not reported with `rig.recordCall` produces artifacts with `step_key`
|
|
147
|
+
set and **no step row**, which is correct and honest: the console renders
|
|
148
|
+
"—" for that step's cost (shown as "cost n/a" with Developer details on)
|
|
149
|
+
rather than inventing a number. Report the call
|
|
150
|
+
(below) and the step gets its tokens and cost.
|
|
117
151
|
|
|
118
152
|
### Record a call your code made itself (`rig.recordCall`)
|
|
119
153
|
|
|
120
|
-
|
|
154
|
+
*Needs `modelrig` 0.11.0 or newer.*
|
|
121
155
|
|
|
122
156
|
If a stage's model call has to stay a direct provider call, report it instead:
|
|
123
157
|
`rig.recordCall({ provider, model, usage: { tokensIn, tokensOut, tokensCached }, servedTier, latencyMs, stepKey })`
|
|
124
158
|
right after the call returns. Inside a run context it records a step row for
|
|
125
159
|
that `stepKey` (stored as `ground-truth`, the only joined-inference value),
|
|
126
|
-
joined to the recorded inference, so the step gets a cost instead of "
|
|
127
|
-
The console shows that step and its call as
|
|
128
|
-
|
|
160
|
+
joined to the recorded inference, so the step gets a cost instead of "—" (shown as
|
|
161
|
+
"cost n/a" with Developer details on). The console shows that step and its call as
|
|
162
|
+
**Reported by your code** (**recorded · unverified** with Developer details on),
|
|
163
|
+
never as measured:
|
|
129
164
|
ModelRig neither dispatched nor validated the call, and the tokens are the ones
|
|
130
165
|
you reported. It is synchronous and never throws (bad input warns and returns
|
|
131
166
|
`inferenceId: null`); the returned `inferenceId` is gradeable with `rig.grade`.
|
|
132
167
|
Full contract, usage convention and cost notes:
|
|
133
168
|
[Not captured → record them](not-captured.md#calls-your-code-makes-itself-record-them).
|
|
134
169
|
|
|
170
|
+
<!-- BLOCK:RECORD_CALL -->
|
|
171
|
+
**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 "Reported by your code", 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`.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const started = Date.now();
|
|
175
|
+
const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
|
|
176
|
+
const u = resp.usageMetadata;
|
|
177
|
+
const finishReason = resp.candidates?.[0]?.finishReason;
|
|
178
|
+
|
|
179
|
+
const rec = rig.recordCall({
|
|
180
|
+
provider: "gemini",
|
|
181
|
+
model: "gemini-3.1-pro-preview",
|
|
182
|
+
usage: {
|
|
183
|
+
tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
|
|
184
|
+
tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
|
|
185
|
+
tokensCached: u?.cachedContentTokenCount ?? 0,
|
|
186
|
+
// optional fields: spread only when the provider reported them
|
|
187
|
+
...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
|
|
188
|
+
},
|
|
189
|
+
servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
|
|
190
|
+
latencyMs: Date.now() - started,
|
|
191
|
+
stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
|
|
192
|
+
...(finishReason !== undefined ? { finishReason } : {}),
|
|
193
|
+
});
|
|
194
|
+
if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
|
|
195
|
+
```
|
|
196
|
+
<!-- /BLOCK:RECORD_CALL -->
|
|
197
|
+
|
|
135
198
|
## Tags, dimensions, and the graph
|
|
136
199
|
|
|
137
200
|
ModelRig has **two** grouping mechanisms, and they compose:
|
|
@@ -237,7 +300,9 @@ const { bundle: runnable, arm } = resolveCallConfigForRun(bundle, resolved, { in
|
|
|
237
300
|
|
|
238
301
|
// Thread the arm onto the run so it lands on inferences.meta.arm.
|
|
239
302
|
await rig.artifacts.run.scope({ pipeline: "report", episodeKey: runId }, async () => {
|
|
240
|
-
|
|
303
|
+
// arm is string | null; RunOptions.arm is optional, so pass it only when drawn
|
|
304
|
+
// (a bare `arm` fails under exactOptionalPropertyTypes).
|
|
305
|
+
const out = await rig.runBundle(runnable, { input, tags: { subject, feature }, stepKey, ...(arm !== null ? { arm } : {}) });
|
|
241
306
|
rig.artifacts.artifact.save(out.output, { name: "section.output", type: "step_output" });
|
|
242
307
|
});
|
|
243
308
|
```
|
|
@@ -294,14 +359,14 @@ orgs and the console picker: [Orgs and keys](https://modelrig.dev/orgs-and-keys.
|
|
|
294
359
|
Check first that the console is showing the org your key exports to:
|
|
295
360
|
|
|
296
361
|
<!-- 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
|
|
362
|
+
**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
363
|
|
|
299
364
|
```bash
|
|
300
365
|
npx modelrig doctor
|
|
301
|
-
# [modelrig] key rig_sk_…AbCd → org «Your Org» ✓ (ingest https://api.modelrig.ai, …)
|
|
366
|
+
# [modelrig] key rig_sk_…AbCd → org «Your Org» (your-org) ✓ (ingest https://api.modelrig.ai, …)
|
|
302
367
|
|
|
303
|
-
# No CLI in that environment? Ask the API directly:
|
|
304
|
-
curl -H "Authorization: Bearer $MODELRIG_API_KEY" https://api.modelrig.ai/v1/whoami
|
|
368
|
+
# No CLI in that environment? Ask the API directly (same host the SDK uses):
|
|
369
|
+
curl -H "Authorization: Bearer $MODELRIG_API_KEY" "${MODELRIG_INGEST_URL:-https://api.modelrig.ai}/v1/whoami"
|
|
305
370
|
```
|
|
306
371
|
<!-- /BLOCK:CONFIRM_ORG -->
|
|
307
372
|
|
|
@@ -319,22 +384,33 @@ an AI — attaches to a subject: one model call (an **inference**), a whole run
|
|
|
319
384
|
[optimization coverage](grade-protocol.md) is built from — the evidence level
|
|
320
385
|
that decides how much confidence a swap proposal carries.
|
|
321
386
|
|
|
387
|
+
<!-- BLOCK:GRADE_RUN -->
|
|
388
|
+
**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.
|
|
389
|
+
|
|
322
390
|
```ts
|
|
323
|
-
|
|
324
|
-
const result = await rig.run("example.support_summarize", {
|
|
325
|
-
|
|
391
|
+
const run = rig.artifacts.run.start({ pipeline: "support", episodeKey: ticketId, environment: "production" });
|
|
392
|
+
const result = await rig.run("example.support_summarize", {
|
|
393
|
+
input: { ticket, product, priorContext },
|
|
394
|
+
tags: { subject: customer },
|
|
395
|
+
stepKey: "summarize",
|
|
396
|
+
});
|
|
397
|
+
run.end("succeeded");
|
|
326
398
|
|
|
327
|
-
//
|
|
328
|
-
rig.grade({ kind: "
|
|
399
|
+
// one call: the inference id rides on every result
|
|
400
|
+
rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "deterministic", grader: "schema-check" });
|
|
401
|
+
// the whole run: its id is run.runId from run.start (shows on the run page and on /projects)
|
|
402
|
+
rig.grade({ kind: "run", id: run.runId }, { score: 0, kind: "human", comment: "missed the refund request" });
|
|
329
403
|
```
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
`{ kind: "
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
is
|
|
404
|
+
<!-- /BLOCK:GRADE_RUN -->
|
|
405
|
+
|
|
406
|
+
- **The subject** is `{ kind: "inference", id }` (`result.meta.inferenceId`, or
|
|
407
|
+
the `inferenceId` `rig.recordCall` returns), `{ kind: "run", id }` (the run's
|
|
408
|
+
id, `run.runId` from `run.start`; a grade carrying the run's `episodeKey`
|
|
409
|
+
also shows, on the run page only), or `{ kind: "artifact", id }`. **`opts`**
|
|
410
|
+
is `{ score, scale?, comment?, kind, grader?, source? }` — the `score` in
|
|
411
|
+
`[0, 1]` is the primitive (up = 1, down = 0), and the pass/fail verdict is
|
|
412
|
+
derived from it against the route threshold. A subject naming no known
|
|
413
|
+
inference, run, or artifact attaches to nothing — attribution is never guessed.
|
|
338
414
|
- **Emit it from a signal you already have.** Most pipelines already decide
|
|
339
415
|
pass/fail somewhere — a schema validator, a downstream accept/reject, a human
|
|
340
416
|
QA gate. Emit `rig.grade` from that (score 1 on a clean pass, 0 on a rejection)
|
|
@@ -368,15 +444,40 @@ const caseId = rig.artifacts.artifact.promoteToEval(handle, { task: "report.sect
|
|
|
368
444
|
|
|
369
445
|
```bash
|
|
370
446
|
# Replay the promoted eval cases through variants — the raw/run-based lane.
|
|
447
|
+
# The variants (default, cheap) are read from the route bundle named report.section.
|
|
371
448
|
modelrig bakeoff --from-eval-cases report.section --variants default,cheap
|
|
372
449
|
```
|
|
373
450
|
|
|
374
451
|
- `promoteToEval(ref, { task?, corrected?, origin? })` binds the case to a route by
|
|
375
452
|
`task` (from `opts`, else the task the artifact was saved with this session), and
|
|
376
453
|
is also reachable from the console's one-click **"make golden"**.
|
|
377
|
-
- `--route`
|
|
378
|
-
|
|
379
|
-
|
|
454
|
+
- `--route` replays **captured route traffic**; `--from-eval-cases <task>` replays
|
|
455
|
+
the promoted cases instead, so it is the path for a `runRaw` seam with no
|
|
456
|
+
captured traffic. Both read the variants from a route bundle (`--route`, or the
|
|
457
|
+
bundle named `<task>`): selecting the cases needs no routes directory, but a
|
|
458
|
+
case that replays needs that bundle and its `variants:` block. Either way a
|
|
459
|
+
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.
|
|
460
|
+
|
|
461
|
+
<!-- BLOCK:BAKEOFF -->
|
|
462
|
+
**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.
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# 1. in modelrig/routes/example.support_summarize.yaml: capture traffic, and declare
|
|
466
|
+
# a cheaper variant (narrowing only: its candidates must be in the route's list)
|
|
467
|
+
capture: true
|
|
468
|
+
variants:
|
|
469
|
+
- name: cheap
|
|
470
|
+
candidates:
|
|
471
|
+
- provider: gemini
|
|
472
|
+
model: gemini-3.1-flash-lite
|
|
473
|
+
|
|
474
|
+
# 2. after traffic accumulates, compare the route as declared (default) with cheap
|
|
475
|
+
npx modelrig bakeoff --route example.support_summarize --variants default,cheap --replay-last 50
|
|
476
|
+
|
|
477
|
+
# no captured route traffic? promote a real /runs output to a golden, then
|
|
478
|
+
npx modelrig bakeoff --from-eval-cases example.support_summarize --variants default,cheap
|
|
479
|
+
```
|
|
480
|
+
<!-- /BLOCK:BAKEOFF -->
|
|
380
481
|
|
|
381
482
|
## A reusable per-step seam
|
|
382
483
|
|
|
@@ -388,15 +489,18 @@ as diffs to apply — it never edits your source. The seam is equally yours to
|
|
|
388
489
|
write by hand; here it is, self-contained:
|
|
389
490
|
|
|
390
491
|
```ts
|
|
391
|
-
import { createRig, loadConfigFromEnv, type Rig } from "modelrig";
|
|
492
|
+
import { createRig, loadConfigFromEnv, type RecordCallInput, type Rig } from "modelrig";
|
|
392
493
|
|
|
393
|
-
|
|
494
|
+
// routesDir: null — the seam needs no route files, so importing it never throws.
|
|
495
|
+
const rig: Rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
394
496
|
|
|
395
|
-
/**
|
|
497
|
+
/** Report the call (tokens → cost), save prompt + raw + parsed, link lineage,
|
|
498
|
+
* flag conformance. */
|
|
396
499
|
function recordStep(
|
|
397
500
|
stepKey: string,
|
|
398
|
-
work: { prompt: string; rawText: string; json: unknown; conforms: boolean },
|
|
501
|
+
work: { prompt: string; rawText: string; json: unknown; conforms: boolean; call?: Omit<RecordCallInput, "stepKey"> },
|
|
399
502
|
): void {
|
|
503
|
+
if (work.call !== undefined) rig.recordCall({ ...work.call, stepKey });
|
|
400
504
|
const p = rig.artifacts.artifact.save(work.prompt, { name: `${stepKey}.prompt`, type: "prompt", stepKey });
|
|
401
505
|
const raw = rig.artifacts.artifact.save(work.rawText, { name: `${stepKey}.raw`, type: "raw_response", stepKey });
|
|
402
506
|
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.12.0 (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
|
|
|
@@ -39,9 +39,18 @@ _This reference is as of modelrig v0.11.0 (the published npm package). Any surfa
|
|
|
39
39
|
- [Managed serving](https://modelrig.dev/managed-serving.html): Serve a route's model on ModelRig's own credentials — destination: modelrig-served — paid from your prepaid balance at provider list cost plus the flat fee: declare the destination, enable managedServing in code, mint a serve-scoped key, opt the organization in, fund the balance; what each refusal teaches; the destination tag on the row; and the honest direct default that nothing here changes.
|
|
40
40
|
- [Orgs and keys](https://modelrig.dev/orgs-and-keys.html): One key = one org: how to see which organization a key exports to (`modelrig doctor`, `GET /v1/whoami`, the console badge), how the console picks the org it shows (the picker, `409 org_required`, `401 session_expired`), why a valid key in the wrong org looks like an empty console, and `doctor --expect-org` / `createRig({ expectOrg })`.
|
|
41
41
|
- [Choosing a lane](https://modelrig.dev/choosing-a-lane.html): Observe (wrap what you have in a run), the `rig.runRaw` seam (one adapter for a router you already own) or named routes (`rig.run`): when each fits, what you get, what you give up, what it costs — and why to start with observe.
|
|
42
|
-
- [Not captured](https://modelrig.dev/not-captured.html): What ModelRig does not see — direct provider SDK calls, streams read outside the SDK, fallbacks after a fail-closed schema, calls outside a run, embeddings — and the console tells: `cost n/a`
|
|
42
|
+
- [Not captured](https://modelrig.dev/not-captured.html): What ModelRig does not see — direct provider SDK calls, streams read outside the SDK, fallbacks after a fail-closed schema, calls outside a run, embeddings — and the console tells: steps with no cost (`—`; `cost n/a` with Developer details on), a missing `inferenceId`, a run total below your provider bill.
|
|
43
43
|
- [Deploying on hosted workers](https://modelrig.dev/deploying.html): Set MODELRIG_API_KEY on every process; Render's env-group vs service-override precedence; a change needs a finished redeploy and a new shell; rotate keys mint → set → redeploy → doctor → revoke; verify after every deploy with `doctor --expect-org`, then one run on /runs; notes for Vercel, Fly and Docker.
|
|
44
44
|
- [Troubleshooting an empty console](https://modelrig.dev/troubleshooting.html): The "console is empty" decision tree: is the process exporting (doctor: AUTH FAILED / no export sink / org ✓), is it the org you are viewing, are the calls inside a run, is the missing call on a ModelRig lane, and does your install match the docs.
|
|
45
|
+
- [Conversations, outcomes and limits](https://modelrig.dev/conversations-outcomes-limits.html): Wrap a chatbot conversation as one run with turns, post outcomes (resolved, booked, CTR) from a webhook, and stop runaway agent loops in-process with run limits.
|
|
46
|
+
- [Template: support conversation](https://modelrig.dev/template-support-conversation.html): A starting point for grading a whole support conversation: resolved for real, policy adherence, correct tool use, tone, escalated when needed. No accuracy claims.
|
|
47
|
+
- [Template: creative approval](https://modelrig.dev/template-creative-approval.html): A starting point for approving generated images, video and audio: on brief, brand safe, legible text, no generation defects, usable as is — human verdicts that feed the approval rate per dollar. Generated media is stored unscreened. No accuracy claims.
|
|
48
|
+
- [Recipe: Vercel AI SDK](https://modelrig.dev/recipe-vercel-ai-sdk.html): Send the Vercel AI SDK's experimental_telemetry traces to ModelRig over OTLP, and set gen_ai.conversation.id so a chat becomes a conversation with turns. Content is not read; attested steps are not priced.
|
|
49
|
+
- [Recipe: OpenAI Agents SDK](https://modelrig.dev/recipe-openai-agents-sdk.html): Forward the OpenAI Agents SDK's traces to OpenTelemetry with a trace processor and export them to ModelRig over OTLP; open an invoke_agent span per user message to number turns.
|
|
50
|
+
- [Recipe: LangGraph / LangChain JS](https://modelrig.dev/recipe-langgraph-js.html): Turn LangChain JS callbacks into OpenTelemetry spans and export them to ModelRig over OTLP; wrap each user message in an invoke_agent span to number turns.
|
|
51
|
+
- [Recipe: Mastra](https://modelrig.dev/recipe-mastra.html): Point Mastra's OTLP telemetry export at ModelRig; each agent request becomes a run, and a conversation when it carries gen_ai.conversation.id and invoke_agent spans.
|
|
52
|
+
- [Recipe: LiveKit Agents](https://modelrig.dev/recipe-livekit-agents.html): Export a LiveKit Agents voice session's OpenTelemetry traces to ModelRig with your conversation id as span metadata — post-call visibility, not duplex voice.
|
|
53
|
+
- [Set up a grader](https://modelrig.dev/set-up-a-grader.html): Teach an AI grader for one step or route: how a task registers (route sync, a runRaw/recordCall step's tags.step, or POST /v1/tasks), a copy-paste prompt your coding agent runs (MCP needs modelrig-oracle 0.3.0+, HTTP otherwise), the console path, TEACHING to CALIBRATED, which grades count, and what is metered.
|
|
45
54
|
- [Leaderboard](https://modelrig.dev/leaderboard.html): Models ranked by effective cost per 1,000 schema-conformant outputs, with declared-vs-probed discrepancy flags.
|
|
46
55
|
|
|
47
56
|
## Source
|
|
@@ -60,14 +69,14 @@ _The default ingest host is https://api.modelrig.ai (override with `MODELRIG_ING
|
|
|
60
69
|
MODELRIG_API_KEY=rig_sk_…
|
|
61
70
|
```
|
|
62
71
|
|
|
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
|
|
72
|
+
**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
73
|
|
|
65
74
|
```bash
|
|
66
75
|
npx modelrig doctor
|
|
67
|
-
# [modelrig] key rig_sk_…AbCd → org «Your Org» ✓ (ingest https://api.modelrig.ai, …)
|
|
76
|
+
# [modelrig] key rig_sk_…AbCd → org «Your Org» (your-org) ✓ (ingest https://api.modelrig.ai, …)
|
|
68
77
|
|
|
69
|
-
# No CLI in that environment? Ask the API directly:
|
|
70
|
-
curl -H "Authorization: Bearer $MODELRIG_API_KEY" https://api.modelrig.ai/v1/whoami
|
|
78
|
+
# No CLI in that environment? Ask the API directly (same host the SDK uses):
|
|
79
|
+
curl -H "Authorization: Bearer $MODELRIG_API_KEY" "${MODELRIG_INGEST_URL:-https://api.modelrig.ai}/v1/whoami"
|
|
71
80
|
```
|
|
72
81
|
|
|
73
82
|
**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,30 +105,146 @@ await rig.artifacts.run.scope(
|
|
|
96
105
|
await rig.close(); // flushes the exporter — call once at shutdown
|
|
97
106
|
```
|
|
98
107
|
|
|
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 "
|
|
108
|
+
**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 "Reported by your code": 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
109
|
|
|
101
110
|
**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
111
|
|
|
103
112
|
```text
|
|
104
113
|
1. npx modelrig doctor, from the environment your app runs in.
|
|
105
114
|
no export sink configured → this process does not see MODELRIG_API_KEY: set it, redeploy, open a new shell.
|
|
115
|
+
malformed key → the value is truncated, quoted or padded: paste the whole rig_sk_… secret again.
|
|
106
116
|
AUTH FAILED (401 / 403) → the key is mistyped, revoked or not accepted: mint a key, set it, redeploy, open a new shell.
|
|
117
|
+
whoami unreachable → no network path to the ingest host (firewall, proxy, wrong MODELRIG_INGEST_URL): fix the route out, retry.
|
|
107
118
|
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
119
|
3. /inferences has rows but /runs is empty → the calls are not inside run.scope / run.start.
|
|
109
120
|
4. A run is missing a step or a cost → that call is not on a ModelRig lane (see not-captured).
|
|
110
121
|
5. A doc describes something your install lacks → compare `npm ls modelrig` with the version stamp in modelrig.dev/llms.txt.
|
|
111
122
|
```
|
|
112
123
|
|
|
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.
|
|
124
|
+
**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
125
|
|
|
115
126
|
```bash
|
|
116
|
-
npx modelrig doctor --expect-org your-org
|
|
127
|
+
npx modelrig doctor --expect-org your-org # exits non-zero if the key belongs to another org
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**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.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { createRig, loadConfigFromEnv } from "modelrig";
|
|
134
|
+
|
|
135
|
+
// Raw-only on purpose: no route files needed. rig.runRaw, rig.runRawStream and
|
|
136
|
+
// rig.recordCall work; rig.run needs routes (add them later, drop routesDir: null).
|
|
137
|
+
const rig = createRig({ ...loadConfigFromEnv(), routesDir: null });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**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 "Reported by your code", 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`.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const started = Date.now();
|
|
144
|
+
const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
|
|
145
|
+
const u = resp.usageMetadata;
|
|
146
|
+
const finishReason = resp.candidates?.[0]?.finishReason;
|
|
147
|
+
|
|
148
|
+
const rec = rig.recordCall({
|
|
149
|
+
provider: "gemini",
|
|
150
|
+
model: "gemini-3.1-pro-preview",
|
|
151
|
+
usage: {
|
|
152
|
+
tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
|
|
153
|
+
tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
|
|
154
|
+
tokensCached: u?.cachedContentTokenCount ?? 0,
|
|
155
|
+
// optional fields: spread only when the provider reported them
|
|
156
|
+
...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
|
|
157
|
+
},
|
|
158
|
+
servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
|
|
159
|
+
latencyMs: Date.now() - started,
|
|
160
|
+
stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
|
|
161
|
+
...(finishReason !== undefined ? { finishReason } : {}),
|
|
162
|
+
});
|
|
163
|
+
if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**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.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const run = rig.artifacts.run.start({ pipeline: "support", episodeKey: ticketId, environment: "production" });
|
|
170
|
+
const result = await rig.run("example.support_summarize", {
|
|
171
|
+
input: { ticket, product, priorContext },
|
|
172
|
+
tags: { subject: customer },
|
|
173
|
+
stepKey: "summarize",
|
|
174
|
+
});
|
|
175
|
+
run.end("succeeded");
|
|
176
|
+
|
|
177
|
+
// one call: the inference id rides on every result
|
|
178
|
+
rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "deterministic", grader: "schema-check" });
|
|
179
|
+
// the whole run: its id is run.runId from run.start (shows on the run page and on /projects)
|
|
180
|
+
rig.grade({ kind: "run", id: run.runId }, { score: 0, kind: "human", comment: "missed the refund request" });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**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.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# 1. in modelrig/routes/example.support_summarize.yaml: capture traffic, and declare
|
|
187
|
+
# a cheaper variant (narrowing only: its candidates must be in the route's list)
|
|
188
|
+
capture: true
|
|
189
|
+
variants:
|
|
190
|
+
- name: cheap
|
|
191
|
+
candidates:
|
|
192
|
+
- provider: gemini
|
|
193
|
+
model: gemini-3.1-flash-lite
|
|
194
|
+
|
|
195
|
+
# 2. after traffic accumulates, compare the route as declared (default) with cheap
|
|
196
|
+
npx modelrig bakeoff --route example.support_summarize --variants default,cheap --replay-last 50
|
|
197
|
+
|
|
198
|
+
# no captured route traffic? promote a real /runs output to a golden, then
|
|
199
|
+
npx modelrig bakeoff --from-eval-cases example.support_summarize --variants default,cheap
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**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.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// The same check in code: the exporter asks whoami on its first flush and logs
|
|
206
|
+
// ORG MISMATCH on every flush while the key belongs to another org.
|
|
207
|
+
const rig = createRig({ ...loadConfigFromEnv(), expectOrg: "your-org" });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Point your OTLP exporter at ModelRig.** ModelRig accepts OpenTelemetry traces at POST /v1/otlp/v1/traces on the ingest host (https://api.modelrig.ai unless you were given another), over OTLP/HTTP as protobuf or JSON, optionally gzipped. Authenticate with a `rig_sk_` key that carries the `ingest` scope; every run and step lands in that key's organization. Each trace becomes one run with one attested step per span: model, provider, operation, timing, status and token usage. Prompts, completions and tool arguments are not read.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Standard OpenTelemetry exporter variables — most OTel SDKs read them as-is.
|
|
214
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.modelrig.ai/v1/otlp/v1/traces
|
|
215
|
+
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20rig_sk_…
|
|
216
|
+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Set gen_ai.conversation.id to your conversation id.** A trace becomes a conversation when one of its spans carries `gen_ai.conversation.id` and it has `invoke_agent` spans. Each top-level `invoke_agent` span is a turn, in start order, and every model call under it becomes a step `turn:<n>.<k>`; the run is marked as a conversation and its episode key is your conversation id. Turns are numbered within a trace: when each user message arrives as its own trace, each shows as its own one-turn conversation run, and the shared conversation id groups them. If your framework does not emit an `invoke_agent` span itself, wrap each user message in one.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { trace } from "@opentelemetry/api";
|
|
223
|
+
|
|
224
|
+
const tracer = trace.getTracer("support-bot");
|
|
225
|
+
|
|
226
|
+
// One span per user message. The model calls your framework makes inside it
|
|
227
|
+
// become this conversation's turn steps; the conversation id joins the runs.
|
|
228
|
+
export function turn<T>(conversationId: string, fn: () => Promise<T>): Promise<T> {
|
|
229
|
+
return tracer.startActiveSpan(
|
|
230
|
+
"invoke_agent support-bot",
|
|
231
|
+
{ attributes: { "gen_ai.operation.name": "invoke_agent", "gen_ai.conversation.id": conversationId } },
|
|
232
|
+
async (span) => {
|
|
233
|
+
try {
|
|
234
|
+
return await fn();
|
|
235
|
+
} finally {
|
|
236
|
+
span.end();
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
}
|
|
117
241
|
```
|
|
118
242
|
|
|
119
243
|
## Capabilities (the full surface)
|
|
120
244
|
|
|
121
245
|
- **Runs & projects** — the run → step → artifact graph at /runs, grouped by tag at /projects (the standard path above): https://modelrig.dev/instrumentation.html.
|
|
122
|
-
- **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"
|
|
246
|
+
- **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 "Reported by your code" ("recorded · unverified" with Developer details on; ModelRig neither dispatched nor validated it), never in routed/conformance math: https://modelrig.dev/not-captured.html.
|
|
247
|
+
- **Diagnosable export drops (`SinkSendError.reasonCode`, modelrig ≥ 0.12.0)** — a batch the server rejects deterministically (`422`) names the database error code in the body's `reason_code` (e.g. `PGRST102`, `23514`); the SDK carries it on the thrown error and the exporter's `POISON-SKIP` line: https://modelrig.dev/troubleshooting.html.
|
|
123
248
|
- **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.
|
|
124
249
|
- **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.
|
|
125
250
|
- **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.
|
|
@@ -135,7 +260,7 @@ npx modelrig doctor --expect-org your-org-slug # exits non-zero if the key bel
|
|
|
135
260
|
- Rates below are sampled statistics with confidence intervals in the source data — do not quote them without their n and date.
|
|
136
261
|
- 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
262
|
- 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
|
|
263
|
+
- 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
264
|
|
|
140
265
|
## Ask this data directly (MCP)
|
|
141
266
|
|
|
@@ -148,6 +273,11 @@ npx modelrig doctor --expect-org your-org-slug # exits non-zero if the key bel
|
|
|
148
273
|
- Register it in Claude Code: add `{"mcpServers":{"modelrig":{"command":"npx","args":["modelrig-oracle"]}}}` to your MCP config.
|
|
149
274
|
- Every answer carries `scope`, `probe_data_as_of`, and a staleness flag. A model absent from an answer is UNPROBED, not incapable — say that rather than implying a limit.
|
|
150
275
|
|
|
276
|
+
## Framework recipes (OTLP) and generated media
|
|
277
|
+
|
|
278
|
+
- FRAMEWORK RECIPES: send a framework's OpenTelemetry traces to `POST /v1/otlp/v1/traces` with a `rig_sk_` ingest key — Vercel AI SDK (https://modelrig.dev/recipe-vercel-ai-sdk.html), OpenAI Agents SDK (https://modelrig.dev/recipe-openai-agents-sdk.html), LangGraph / LangChain JS (https://modelrig.dev/recipe-langgraph-js.html), Mastra (https://modelrig.dev/recipe-mastra.html), LiveKit Agents (https://modelrig.dev/recipe-livekit-agents.html). Each trace is a run of attested steps (model, provider, operation, timing, status, token usage). Set `gen_ai.conversation.id` on a span and emit `invoke_agent` spans and the trace becomes a conversation: generations numbered `turn:<n>.<k>`, tool calls counted per turn. NOT shown: content (prompts/completions are not read — OTLP content extraction is behind a server gate that is OFF) and cost (attested steps are not priced). The recipe configs are marked untested — verify against your framework version.
|
|
279
|
+
- GENERATION CALLS (modelrig ≥ 0.12.0): `rig.recordCall({ provider, model, usage: { tokensIn: 0, tokensOut: 0 }, units: { images?, seconds?, characters?, steps? }, latencyMs })` records an image/video/TTS/STT call priced per unit — from the registry's declared generation entry (sourced rate + as_of), else the vendored pricing map, else `pricingMissing` (never from token rates). Query them with `query_registry({ kind: "image_gen" | "video_gen" | "tts" | "stt" })`: declared-only, unprobed, never ranked. Save generated assets as `image/*` / `video/*` / `audio/*` artifacts (stored unscreened); approve or reject them in the Grade queue's Media approval tab; Costs shows approval rate per $ by model from ten approvals. Starter guide: https://modelrig.dev/template-creative-approval.html.
|
|
280
|
+
|
|
151
281
|
## Live routing & call-config (change what serves a route, no redeploy)
|
|
152
282
|
|
|
153
283
|
- A route's CALL-CONFIG — which model/provider serves it + its sampling — can change WHILE THE APP RUNS, no YAML edit, no redeploy. Two modes: `pin` (one model serves now) and `experiment` (a weighted live A/B whose drawn arm records on `inferences.meta.arm`). Set it in the console's call-config card, or over MCP (below).
|
|
@@ -290,7 +420,7 @@ What `rig.runRaw` sends when you omit a knob. `timeoutMs` is tier-aware since 0.
|
|
|
290
420
|
|
|
291
421
|
## Grades (judgment on inferences, runs & artifacts)
|
|
292
422
|
|
|
293
|
-
- A GRADE is a 0–1 score (with an optional comment) attached to a SUBJECT — an inference, a run (episode), or an artifact — from your code, a human, or an AI. The pass/fail verdict is DERIVED, never stored: `pass = score >= the route's threshold` (default 0.5). It attaches from the SDK (`rig.grade(subject, { score, kind })`) or posts over HTTP from a non-SDK surface: `POST /v1/grade` on modelrig-server, authenticated with a `rig_sk_` key carrying the `grade` scope, body `{inferenceId | episode, score, scale?, comment?, kind?, source?}
|
|
423
|
+
- A GRADE is a 0–1 score (with an optional comment) attached to a SUBJECT — an inference, a run (episode), or an artifact — from your code, a human, or an AI. The pass/fail verdict is DERIVED, never stored: `pass = score >= the route's threshold` (default 0.5). It attaches from the SDK (`rig.grade(subject, { score, kind })`) or posts over HTTP from a non-SDK surface: `POST /v1/grade` on modelrig-server, authenticated with a `rig_sk_` key carrying the `grade` scope, body `{inferenceId | episode | artifactId, score, scale?, comment?, kind?, source?}` (`artifactId` grades an artifact). The org is resolved from the key, never the request body; there is NO public/unauthenticated grade endpoint.
|
|
294
424
|
- Every run that ends gets one deterministic `run-outcome@v1` grade AUTOMATICALLY, with no model call: it scores 1.0 (a pass) iff the run succeeded AND no step failed AND every deterministic grade passed; otherwise 0.0 with a dominant-failure label.
|
|
295
425
|
- An explicit, bounded model judge (`run-judge@v1`) can ADDITIONALLY score a run's outcome-achievement 0–1 as a `model-judge` grade — invoked only on demand (console button or `modelrig judge <runId>`), capped per-org per-day, and metered as a routed gateway call. There is NO automatic or scheduled model-judging.
|
|
296
426
|
- SDK callers attach their own run- or artifact-level grade the same way: `rig.grade({ kind: "run", id: run_id }, { score, kind, comment? })` — the grader `kind` is one of human | model-judge | deterministic.
|
|
@@ -309,7 +439,7 @@ What `rig.runRaw` sends when you omit a knob. `timeoutMs` is tier-aware since 0.
|
|
|
309
439
|
## Use-case templates (start a task with a schema + guide)
|
|
310
440
|
|
|
311
441
|
- A USE-CASE TEMPLATE is a starting point for putting one task on the loop: a starter output schema (with TODO enum slots you fill with your own taxonomy — never our invented values), a starter grading guide in the exact jsonb shape the settings path validates, a golden checklist of what ~20 goldens should cover, and an escalation starter. It carries NO number about how well any model does the task — it is a shape to fill in, and what you fill in is what your goldens and grades then measure. Overview: https://modelrig.dev/use-case-templates.html.
|
|
312
|
-
-
|
|
442
|
+
- Eleven templates, one per task the loop can serve (all eleven ship in the npm package): **ticket triage** (https://modelrig.dev/template-ticket-triage.html), **document extraction** (https://modelrig.dev/template-document-extraction.html), **CS next action** (https://modelrig.dev/template-cs-next-action.html), **lead qualification** (https://modelrig.dev/template-lead-qualification.html), **compliance review** (https://modelrig.dev/template-compliance-review.html), **catalog cleansing** (https://modelrig.dev/template-catalog-cleansing.html), **call disposition QA** (https://modelrig.dev/template-call-disposition-qa.html), **financial classification** (https://modelrig.dev/template-financial-classification.html), **medical classification** (https://modelrig.dev/template-medical-classification.html), **support conversation** (https://modelrig.dev/template-support-conversation.html), and **creative approval** (https://modelrig.dev/template-creative-approval.html). Each machine-readable file also ships in the npm package at `node_modules/modelrig/templates/<use-case>.json`.
|
|
313
443
|
- REGULATED ROWS: financial classification and medical classification handle regulated data. Their fit notes quote the published content custody terms verbatim — do not send protected health information or other regulated data through ModelRig until a BAA or DPA is in place — and name the today-path where those terms are not yet in place: a zero-retention route (or content custody off), so ModelRig holds no content and grading is limited to the scores and metadata your own systems send. Terms: https://modelrig.ai/terms/content-custody. No other compliance claim is made.
|
|
314
444
|
- CHECK FIT FIRST (the overview applies these to each task): a task suits the loop when its distribution is stable (goldens stay valid), a rubric is worth writing (the grading guide), expert examples are on hand to grade, and volume makes calibration pay back. A task that fails one structurally is honest to leave at the teaching stage, not force through the loop.
|
|
315
445
|
- TWO CONSUMPTION PATHS, one file: a human copies the schema and guide from the docs page (they run as-is; TODOs mark where to tighten); or a coding agent feeds the template through the tools that ALREADY EXIST — `create_route` drafts the route bundle from the starter schema (files only, never your source), then `set_task_settings` (the `task-settings` scope) authors the grading guide — the starter criteria run as written; sharpen them into your team's own words as you grade. No new tool, no server change. The starter guide has NO `pass_threshold` (that lives in the route's `grade:` block — the guide teaches the grader, the threshold derives the verdict).
|