What ModelRig does not capture unless you record it
The question this page answers: which of my model calls does ModelRig not see, how can I tell from the console, and how do I record a call my code makes itself?
ModelRig records a call when your code makes it through ModelRig: the SDK (rig.run, rig.runBundle, rig.runRaw, rig.runRawStream) or the OpenAI-compatible gateway. It also records a call your code makes itself when you report it with rig.recordCall (see Calls your code makes itself: record them). It has no other way to see your traffic. It doesn't read your provider account, and it doesn't patch provider client libraries. So the honest boundary is simple: if a call didn't go through ModelRig and you didn't record it, ModelRig didn't see it.
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.
Each case, and what to do about it
| Case | What happens | What to do |
|---|---|---|
| Direct provider SDK calls (an OpenAI, Gemini or Anthropic client your code calls itself), including calls that pass a JSON schema | Nothing is recorded: no row on /inferences, no step on /runs, no cost. | Record it with rig.recordCall right after the call returns (below), or put it behind rig.runRaw with the same provider, model and parameters, or extract it to a route. A schema call on the raw lane passes responseSchema; a route declares schema:. |
| Streams you read from a provider client | Same as a direct call: invisible. | Record the finished stream's usage with rig.recordCall, or use rig.runRawStream, which records the stream when it finishes. |
A responseSchema the raw lane can't enforce on that provider (deepseek, deepinfra, fireworks and zai; gemini, openai, grok and anthropic do enforce it) | ModelRig refuses the call before dispatch, rather than sending it without the schema: your code gets a RigFailureError with class invariant_violation, and no provider call is made. If your code then falls back to calling the provider directly, the fallback is invisible unless you record it. | Check the provider × knob matrix in Route bundles before you add responseSchema. On those four providers, use responseFormat: "json_object" where the matrix shows it and validate in your code, send the step to a provider that enforces schemas, or record the direct fallback with rig.recordCall. |
| Calls outside a run | The call is recorded on /inferences with its cost, but it belongs to no run, so /runs doesn't show it. | Wrap the pipeline execution in run.scope / run.start (Observe a pipeline). |
| Embeddings | Not recorded on any lane. | Record them with rig.recordCall (tokensOut: 0) if you want them in your ModelRig costs; otherwise account for them from your provider bill. |
A call your SDK version doesn't support yet behaves like a direct call if your code works around it. If a page describes a knob your installed version rejects, compare npm ls modelrig with the version stamp at the top of llms.txt.
Calls your code makes itself: record them
rig.recordCall needs modelrig 0.11.0 or newer.
When a call has to stay a direct provider call (a client library ModelRig has no lane for, a stream your code reads itself, a fallback), report it to ModelRig right after it returns. rig.recordCall writes one row for it, joins it to the active run and step, and returns an inferenceId you can grade.
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.
const started = Date.now();
const resp = await genai.models.generateContent({ model: "gemini-3.1-pro-preview", contents });
const u = resp.usageMetadata;
const finishReason = resp.candidates?.[0]?.finishReason;
const rec = rig.recordCall({
provider: "gemini",
model: "gemini-3.1-pro-preview",
usage: {
tokensIn: u?.promptTokenCount ?? 0, // total input, cached included
tokensOut: (u?.candidatesTokenCount ?? 0) + (u?.thoughtsTokenCount ?? 0),
tokensCached: u?.cachedContentTokenCount ?? 0,
// optional fields: spread only when the provider reported them
...(u?.thoughtsTokenCount !== undefined ? { reasoningTokens: u.thoughtsTokenCount } : {}),
},
servedTier: "standard", // the tier that SERVED the call: pass what the provider reports (flex, priority); it is priced at that tier
latencyMs: Date.now() - started,
stepKey: "summarize", // the step on /runs (inside run.scope / run.start)
...(finishReason !== undefined ? { finishReason } : {}),
});
if (rec.inferenceId !== null) rig.grade({ kind: "inference", id: rec.inferenceId }, { score: 1, kind: "deterministic" });
- The contract.
recordCallis synchronous and never throws into your code. Invalid input (missing tokens, a cached count larger thantokensIn, an unknown tier or failure class) logs one[modelrig recordCall] … fix: …warning, records nothing, and returnsinferenceId: null. - Usage convention.
tokensInis the total input and includestokensCachedandtokensCacheWrite, the same convention every ModelRig adapter uses. Anthropic reportsinput_tokenswithout cache reads and writes, so add them back. Token counts andlatencyMs/ttfbMsare rounded to whole numbers (aperformance.now()delta is fine), and aprovider/modellonger than 200 characters is refused. - Failures still count. A call the provider billed but that failed (a timeout after tokens were spent, a refusal) is recorded with
failureClassand still shows its cost. PasswebSearchCallswhen the call billed native web-search tool calls: the surcharge is a separate search-cost row, as on the raw lane. For Gemini, passgroundingMetadata.webSearchQueries.length(1 when the call was grounded but reported no list); the rate is model-aware (modelrig 0.15.0 or newer): Gemini 3+ bills each query, Gemini 2.x one grounded prompt (webSearchCost). To record an explicit cache's storage, seecacheStoragein Caching lifecycle. - Providers ModelRig doesn't price. Any non-empty
provideris accepted. A provider or model with no price entry is recorded at $0 withpricingMissing: true, never at a guessed price.
- Generation calls:
units(needsmodelrig0.12.0 or newer). An image, video, text-to-speech or transcription call is billed per image, second or character, not per token. Reportusage: { tokensIn: 0, tokensOut: 0 }and what it produced inunits: { images?, seconds?, characters?, steps? }(finite numbers ≥ 0; at least one above zero when the tokens are zero). The units ridemeta.unitsand are priced from a sourced rate, never from token rates: first the registry's declared generation entry for thatprovider/model(each rate carries its source and date), then the vendored LiteLLM pricing map's per-mode rate; otherwise the row ispricingMissing. Each model bills one unit — images for an image model, seconds for video and transcription, characters for text-to-speech — and the others are recorded only;stepsis never priced. Save each generated asset as an artifact in the same step, typedimage/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm,audio/mpeg,audio/wav,audio/oggoraudio/webm(any other media type, SVG included, is refused with a warning): it is stored unscreened (the PII/PHI scrub reads text only; under aredact/blockscrub policy the bytes are not uploaded and the row records that), approved or rejected in the Grade queue's Media approval tab, and feeds the approval rate per $ on Costs (see https://modelrig.dev/template-creative-approval.html).
What is verified and what isn't. Nothing about the call itself. ModelRig did not send the request, so it has no prompt or output to hash, no schema to check, and no measurement of its own: the tokens, tier, latency and failure on the row are the numbers you reported (the row's meta.provenance is caller-reported). What ModelRig does own is the arithmetic: the cost is the provider's list price at the served tier applied to your token counts, and the row joins your run, step and grades exactly as a raw-lane call does.
How it shows in the console. On /inferences, in the call detail and in the step panel the call carries a Reported by your code badge (recorded · unverified with Developer details on): "Your code made this call and reported its tokens and cost." ModelRig did not dispatch or validate it. It counts toward run, step, tag and project costs, and its quality signal is your grades. It never enters a route's ladder, fallback or conformance numbers, and lane splits count it separately as recorded. Reading rows yourself (an export, the analytics API)? Recorded rows carry tags.lane = "external"; isRoutedLane(tags.lane) from modelrig is false for them and for raw-lane rows, the same test the console uses.
Cost and reconciliation. A recorded call is spend on your own provider key: ModelRig never charges it to a prepaid balance as provider cost, and it is billed like a raw-lane call on your own key. Its cost estimate is only as accurate as the tokens you report, so reconcile against your provider bill the same way as for any other lane: per day and per model. A recorded call with pricingMissing adds $0 to ModelRig's totals and is the first place to look for a gap. Context-length price bands (for example Gemini above 200k tokens) are not applied, as on every lane.
How to tell from the console
Three tells point at a call ModelRig didn't see:
1. A step shows — for its cost (shown as cost n/a with Developer details on) on /runs/<id>. The step has artifacts (you saved them with that stepKey) but no recorded call. The model call that produced them went around ModelRig and wasn't reported with rig.recordCall. 2. Your code has no inferenceId for a call. Every recorded call returns result.meta.inferenceId. If your logging or grading code finds none for a step, that step's call wasn't recorded, and a grade for it has nothing to attach to. 3. The run's total cost is lower than your provider bill for the same period. Some spend is happening outside ModelRig. Compare per day and per model: the gap is usually one call site.
To find the call site, search your code for direct provider client calls (new OpenAI(, GoogleGenAI, generateContent, messages.create, chat.completions.create) that aren't inside the rig.runRaw seam and aren't followed by a rig.recordCall.
What is recorded for a call ModelRig does see
For each attempt: provider, model, requested and served tier, tokens in, out and cached, cost, latency, failure class, your tags, and the step it belongs to, plus hashes and sizes of the request and the output. The prompt and output text is not kept unless you save it as an artifact or turn on call capture (captureCalls, needs modelrig 0.14.0 or newer); what is then stored follows your content custody setting.
Keep prompts and outputs: turn on call capture. By default ModelRig records every call's metadata and cost (model, tokens, tier, latency, failure, and hashes and sizes of the request and output), not the prompt or output text. You keep text two ways: save it yourself with artifact.save, or turn on call capture in code with artifacts.captureCalls: true. It is off by default and is not an env var, and it needs api mode (MODELRIG_API_KEY): direct and local-only rigs never capture. With it on, each attempt ModelRig sends to a provider through rig.run, rig.runBundle, rig.runStream, rig.runRaw and rig.runRawStream saves the request as rendered before provider-side additions as a prompt artifact and the response text as a raw_response artifact; retried attempts each get their own pair, a failed attempt keeps its prompt plus the model's response when the provider returned one (for example a content_invalid or quality-rejected attempt), and the call page labels them captured (opt-in). It applies only when your organization's content posture is managed, and never under metadata or off, while the SDK has not read (or cannot re-read) the posture, or on a zero-retention route or run. Routes or calls with a mask policy are never captured: capture skips them. Captured text takes the same path as any artifact you save: your scrub setting runs first (a pii or phi route is scrubbed at redact or stronger), the upload size limit applies, and stored content follows your organization's retention setting on /account. rig.recordCall never captures text; save artifacts for those calls yourself. Details: https://modelrig.dev/not-captured.html.
import { createRig, loadConfigFromEnv } from "modelrig";
// Opt in, in code (not an env var): keep each call's prompt and response text.
const rig = createRig(loadConfigFromEnv({ artifacts: { captureCalls: true } }));
Related: Choosing a lane · Troubleshooting an empty console · Routing & reliability