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.
Files changed (199) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/adapters/anthropic.d.ts +4 -4
  3. package/dist/adapters/anthropic.d.ts.map +1 -1
  4. package/dist/adapters/anthropic.js +8 -1
  5. package/dist/adapters/deepseek.d.ts +2 -4
  6. package/dist/adapters/deepseek.d.ts.map +1 -1
  7. package/dist/adapters/deepseek.js +9 -5
  8. package/dist/adapters/gemini.d.ts +4 -1
  9. package/dist/adapters/gemini.d.ts.map +1 -1
  10. package/dist/adapters/gemini.js +16 -1
  11. package/dist/adapters/grok.d.ts +2 -4
  12. package/dist/adapters/grok.d.ts.map +1 -1
  13. package/dist/adapters/grok.js +8 -4
  14. package/dist/adapters/history-mapping.d.ts +126 -0
  15. package/dist/adapters/history-mapping.d.ts.map +1 -0
  16. package/dist/adapters/history-mapping.js +230 -0
  17. package/dist/adapters/openai-compat.d.ts +2 -4
  18. package/dist/adapters/openai-compat.d.ts.map +1 -1
  19. package/dist/adapters/openai-compat.js +9 -5
  20. package/dist/adapters/openai.d.ts +2 -4
  21. package/dist/adapters/openai.d.ts.map +1 -1
  22. package/dist/adapters/openai.js +8 -4
  23. package/dist/adapters/responses-api.d.ts.map +1 -1
  24. package/dist/adapters/responses-api.js +9 -4
  25. package/dist/artifacts/artifact.d.ts.map +1 -1
  26. package/dist/artifacts/artifact.js +79 -0
  27. package/dist/artifacts/conversation.d.ts +128 -0
  28. package/dist/artifacts/conversation.d.ts.map +1 -0
  29. package/dist/artifacts/conversation.js +172 -0
  30. package/dist/artifacts/index.d.ts +1 -1
  31. package/dist/artifacts/index.d.ts.map +1 -1
  32. package/dist/artifacts/media.d.ts +46 -0
  33. package/dist/artifacts/media.d.ts.map +1 -0
  34. package/dist/artifacts/media.js +88 -0
  35. package/dist/artifacts/run-context.d.ts +61 -2
  36. package/dist/artifacts/run-context.d.ts.map +1 -1
  37. package/dist/artifacts/run-context.js +183 -12
  38. package/dist/artifacts/scrub.d.ts +3 -0
  39. package/dist/artifacts/scrub.d.ts.map +1 -1
  40. package/dist/bakeoff/engine.d.ts.map +1 -1
  41. package/dist/bakeoff/engine.js +34 -7
  42. package/dist/bakeoff/eval-cases.d.ts +5 -0
  43. package/dist/bakeoff/eval-cases.d.ts.map +1 -1
  44. package/dist/bakeoff/eval-cases.js +10 -1
  45. package/dist/bakeoff/replay.d.ts +15 -0
  46. package/dist/bakeoff/replay.d.ts.map +1 -1
  47. package/dist/bakeoff/replay.js +19 -1
  48. package/dist/bakeoff/report.d.ts.map +1 -1
  49. package/dist/bakeoff/report.js +9 -2
  50. package/dist/bakeoff/turn-replay.d.ts +93 -0
  51. package/dist/bakeoff/turn-replay.d.ts.map +1 -0
  52. package/dist/bakeoff/turn-replay.js +150 -0
  53. package/dist/bakeoff/types.d.ts +12 -0
  54. package/dist/bakeoff/types.d.ts.map +1 -1
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +1 -0
  57. package/dist/config.d.ts +18 -0
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +23 -3
  60. package/dist/doctor-preflight.d.ts.map +1 -1
  61. package/dist/doctor-preflight.js +6 -1
  62. package/dist/feedback/grade.d.ts +7 -2
  63. package/dist/feedback/grade.d.ts.map +1 -1
  64. package/dist/feedback/grade.js +14 -0
  65. package/dist/feedback/outcome.d.ts +103 -0
  66. package/dist/feedback/outcome.d.ts.map +1 -0
  67. package/dist/feedback/outcome.js +175 -0
  68. package/dist/gateway/history-bridge.d.ts +32 -0
  69. package/dist/gateway/history-bridge.d.ts.map +1 -0
  70. package/dist/gateway/history-bridge.js +64 -0
  71. package/dist/gateway/serve-client.d.ts.map +1 -1
  72. package/dist/gateway/serve-client.js +6 -0
  73. package/dist/index.d.ts +44 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +110 -9
  76. package/dist/init/init-cli.d.ts.map +1 -1
  77. package/dist/init/init-cli.js +21 -4
  78. package/dist/init/observe-cli.js +1 -1
  79. package/dist/init/observe.d.ts +7 -1
  80. package/dist/init/observe.d.ts.map +1 -1
  81. package/dist/init/observe.js +74 -22
  82. package/dist/init/starter.d.ts +36 -0
  83. package/dist/init/starter.d.ts.map +1 -0
  84. package/dist/init/starter.js +85 -0
  85. package/dist/registry/build.d.ts +4 -0
  86. package/dist/registry/build.d.ts.map +1 -1
  87. package/dist/registry/generation-models.d.ts +68 -0
  88. package/dist/registry/generation-models.d.ts.map +1 -0
  89. package/dist/registry/generation-models.js +168 -0
  90. package/dist/registry/model-kind.d.ts +34 -0
  91. package/dist/registry/model-kind.d.ts.map +1 -0
  92. package/dist/registry/model-kind.js +40 -0
  93. package/dist/registry/pricing-shape.d.ts +29 -1
  94. package/dist/registry/pricing-shape.d.ts.map +1 -1
  95. package/dist/registry/pricing-shape.js +9 -0
  96. package/dist/registry/pricing.d.ts +26 -0
  97. package/dist/registry/pricing.d.ts.map +1 -1
  98. package/dist/registry/pricing.js +29 -0
  99. package/dist/registry/rows.d.ts.map +1 -1
  100. package/dist/registry/rows.js +3 -1
  101. package/dist/registry/unit-pricing.d.ts +32 -0
  102. package/dist/registry/unit-pricing.d.ts.map +1 -0
  103. package/dist/registry/unit-pricing.js +62 -0
  104. package/dist/routes/input-loader.d.ts +38 -0
  105. package/dist/routes/input-loader.d.ts.map +1 -0
  106. package/dist/routes/input-loader.js +80 -0
  107. package/dist/routes/loader.d.ts.map +1 -1
  108. package/dist/routes/loader.js +3 -0
  109. package/dist/routes/resolved-bundle.d.ts +2 -0
  110. package/dist/routes/resolved-bundle.d.ts.map +1 -1
  111. package/dist/routes/resolved-bundle.js +3 -0
  112. package/dist/run/cost.d.ts +28 -0
  113. package/dist/run/cost.d.ts.map +1 -1
  114. package/dist/run/cost.js +74 -0
  115. package/dist/run/dispatch.d.ts +4 -1
  116. package/dist/run/dispatch.d.ts.map +1 -1
  117. package/dist/run/dispatch.js +3 -0
  118. package/dist/run/limits.d.ts +88 -0
  119. package/dist/run/limits.d.ts.map +1 -0
  120. package/dist/run/limits.js +155 -0
  121. package/dist/run/messages.d.ts +199 -0
  122. package/dist/run/messages.d.ts.map +1 -0
  123. package/dist/run/messages.js +430 -0
  124. package/dist/run/raw-finalize.d.ts +5 -2
  125. package/dist/run/raw-finalize.d.ts.map +1 -1
  126. package/dist/run/raw-finalize.js +8 -5
  127. package/dist/run/raw-request.d.ts.map +1 -1
  128. package/dist/run/raw-request.js +2 -1
  129. package/dist/run/raw.d.ts +10 -4
  130. package/dist/run/raw.d.ts.map +1 -1
  131. package/dist/run/raw.js +11 -3
  132. package/dist/run/rawonly.d.ts +29 -5
  133. package/dist/run/rawonly.d.ts.map +1 -1
  134. package/dist/run/rawonly.js +54 -19
  135. package/dist/run/record-attempt.d.ts.map +1 -1
  136. package/dist/run/record-attempt.js +16 -6
  137. package/dist/run/record-call.d.ts +13 -2
  138. package/dist/run/record-call.d.ts.map +1 -1
  139. package/dist/run/record-call.js +50 -7
  140. package/dist/run/run.d.ts.map +1 -1
  141. package/dist/run/run.js +12 -8
  142. package/dist/telemetry/attempt-meta.d.ts +49 -1
  143. package/dist/telemetry/attempt-meta.d.ts.map +1 -1
  144. package/dist/telemetry/attempt-meta.js +45 -1
  145. package/dist/telemetry/exporter.d.ts.map +1 -1
  146. package/dist/telemetry/exporter.js +2 -1
  147. package/dist/telemetry/grade-mappers.d.ts +1 -1
  148. package/dist/telemetry/grade-mappers.d.ts.map +1 -1
  149. package/dist/telemetry/grade-mappers.js +7 -1
  150. package/dist/telemetry/grade-task.d.ts +49 -0
  151. package/dist/telemetry/grade-task.d.ts.map +1 -0
  152. package/dist/telemetry/grade-task.js +85 -0
  153. package/dist/telemetry/grades-sql.d.ts +16 -1
  154. package/dist/telemetry/grades-sql.d.ts.map +1 -1
  155. package/dist/telemetry/grades-sql.js +64 -4
  156. package/dist/telemetry/ingest-protocol.d.ts +24 -1
  157. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  158. package/dist/telemetry/ingest-protocol.js +72 -43
  159. package/dist/telemetry/otel-content.d.ts +31 -0
  160. package/dist/telemetry/otel-content.d.ts.map +1 -0
  161. package/dist/telemetry/otel-content.js +106 -0
  162. package/dist/telemetry/otel-hierarchy.d.ts +57 -0
  163. package/dist/telemetry/otel-hierarchy.d.ts.map +1 -0
  164. package/dist/telemetry/otel-hierarchy.js +155 -0
  165. package/dist/telemetry/otel-normalize.d.ts +15 -1
  166. package/dist/telemetry/otel-normalize.d.ts.map +1 -1
  167. package/dist/telemetry/otel-normalize.js +24 -39
  168. package/dist/telemetry/sinks.d.ts +4 -1
  169. package/dist/telemetry/sinks.d.ts.map +1 -1
  170. package/dist/telemetry/sinks.js +53 -34
  171. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  172. package/dist/telemetry/store-migrations.js +8 -0
  173. package/dist/telemetry/store.d.ts +9 -0
  174. package/dist/telemetry/store.d.ts.map +1 -1
  175. package/dist/telemetry/store.js +10 -0
  176. package/dist/telemetry/uniform-rows.d.ts +43 -0
  177. package/dist/telemetry/uniform-rows.d.ts.map +1 -0
  178. package/dist/telemetry/uniform-rows.js +139 -0
  179. package/dist/types/failures-teaching.d.ts.map +1 -1
  180. package/dist/types/failures-teaching.js +11 -0
  181. package/dist/types/failures.d.ts +1 -1
  182. package/dist/types/failures.d.ts.map +1 -1
  183. package/dist/types/failures.js +2 -0
  184. package/dist/types/provider.d.ts +12 -0
  185. package/dist/types/provider.d.ts.map +1 -1
  186. package/dist/types/route.d.ts +13 -0
  187. package/dist/types/route.d.ts.map +1 -1
  188. package/dist/types/telemetry.d.ts +14 -1
  189. package/dist/types/telemetry.d.ts.map +1 -1
  190. package/docs/agent-skill/SKILL.md +122 -12
  191. package/docs/how-it-fits.md +4 -4
  192. package/docs/instrumentation.md +148 -44
  193. package/docs/llms.txt +143 -13
  194. package/docs/mcp-oracle.md +15 -4
  195. package/docs/quickstart.md +129 -16
  196. package/package.json +4 -2
  197. package/registry/registry.json +456 -0
  198. package/templates/creative-approval.json +81 -0
  199. package/templates/support-conversation.json +82 -0
@@ -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
- const rig = createRig(loadConfigFromEnv());
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, // e.g. "production", "staging", "test"
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 result = await callModel(prompt); // your own model call
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
- // 2. Save the step's artifacts. stepKey groups them in the console's
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
- // 3. Lineage: the parsed output derives from the prompt + raw; this step's
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
- // 4. Grades (optional, deterministic): flag conformance, degraded
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
- produces artifacts with `step_key` set and **no step row** — which is correct
115
- and honest. Never route a stage through the rig just to mint a step row; the
116
- console renders "cost n/a" for un-routed steps rather than inventing a number.
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
- _Needs `modelrig` 0.11.0 or newer._
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 "cost n/a".
127
- The console shows that step and its call as **recorded · unverified**, never as
128
- measured:
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
- const out = await rig.runBundle(runnable, { input, tags: { subject, feature }, arm, stepKey });
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, and the name it prints must match the org badge in the console header. If it names a different organization, your data is landing there: switch the console to that organization, or mint a key in the one you meant (https://modelrig.dev/orgs-and-keys.html).
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
- // One call — the inference id rides on every RunResult:
324
- const result = await rig.run("example.support_summarize", { input, tags: { subject } });
325
- rig.grade({ kind: "inference", id: result.meta.inferenceId }, { score: 1, kind: "human" });
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
- // A whole run: the subject id is the run's run_id (its episodeKey, or its id).
328
- rig.grade({ kind: "run", id: runAttemptId }, { score: 0.2, comment: "CAGR off by 10x", kind: "human" });
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
- - **The subject** is `{ kind: "inference", id }` (`result.meta.inferenceId`),
332
- `{ kind: "run", id }` (the run's `run_id` — its `episodeKey`, or its id), or
333
- `{ kind: "artifact", id }`. **`opts`** is `{ score, scale?, comment?, kind,
334
- source? }` — the `score` in `[0, 1]` is the primitive (up = 1, down = 0), and
335
- the pass/fail verdict is derived from it against the route threshold. A subject
336
- naming no known inference, run, or artifact attaches to nothing — attribution
337
- is never guessed.
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` needs a **declared route**; `--from-eval-cases <task>` is the path for a
378
- `runRaw` seam that owns no route. Either way a bake-off **measures** — it never
379
- switches what serves a route; a swap is a human-approved PR.
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
- const rig: Rig = createRig(loadConfigFromEnv());
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
- /** Save prompt + raw + parsed for one step, link lineage, flag conformance. */
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.11.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._
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` steps, a missing `inferenceId`, a run total below your provider bill.
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, and the name it prints must match the org badge in the console header. If it names a different organization, your data is landing there: switch the console to that organization, or mint a key in the one you meant (https://modelrig.dev/orgs-and-keys.html).
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 "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.
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-slug # exits non-zero if the key belongs to another 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" (ModelRig neither dispatched nor validated it), never in routed/conformance math: https://modelrig.dev/not-captured.html.
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 is a human-approved PR. See https://modelrig.dev/bakeoffs.html.
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?}`. The org is resolved from the key, never the request body; there is NO public/unauthenticated grade endpoint.
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
- - Nine ship today, one per task the loop can serve: **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), and **medical classification** (https://modelrig.dev/template-medical-classification.html). Each machine-readable file also ships in the npm package at `node_modules/modelrig/templates/<use-case>.json`.
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).