ModelRig Quickstart Routing & reliability Route bundles Probes Bake-offs & replay How it fits Grade protocol Optimization loop Caching lifecycle Bring your traces (OTLP) Observe a pipeline Migration playbook (T0–T2) Recognition playbooks Tenants & statements Published receipts Provenance & trust Artifact content custody The MCP oracle Use-case templates Template: ticket triage Template: document extraction Template: CS next action Template: lead qualification Template: compliance review Template: catalog cleansing Template: call disposition QA Template: financial classification Template: medical classification Leaderboard

Route bundle reference

One YAML file per route under the routes directory (default modelrig/routes/). Files are loaded, structurally validated, and frozen at createRig() time; any invalid bundle throws a RouteConfigError naming the file and every problem.

Top-level fields

fieldtyperequirednotes
routestringyesThe task handle passed to rig.run(). Unique across the directory (duplicates are a load error).
versioninteger ≥ 1yesBump on ANY change to the bundle, schema, or prompt. Recorded on every telemetry row.
schemarelative path \nullyesJSON Schema file for the output. null = unstructured route (output must still be parseable JSON). Path resolves relative to the bundle file.
candidateslistyes, non-emptyTHE candidate set. Each entry is {provider, model}, plus an optional trial: true (v2, R3 — see below). Providers (the ProviderId union): gemini, openai, deepseek, anthropic, grok, deepinfra, fireworks — all with adapters. Nothing outside this list can serve the route — enforced structurally (branded CandidateRef) and at runtime.
candidates[].trialbooleanno (default false)v2 (R3). A DECLARED-but-never-served-live candidate. A trial: true candidate is dropped from every live path (rig.run, runStream, runBundle, the hosted gateway slug lane) — the run loop only keeps it inside a bake-off replay, and selecting a trial-only variant on a live call is a RouteConfigError that names the bake-off. Declare a model this way to measure it on your real captured traffic before promoting it to a live candidate. Additive: a route with no trial candidate is byte-identical to before. Deploy the server/SDK (≥ 0.5.0) that understands trial: BEFORE any route YAML carries it — an older build ignores the flag (its resolver has no trial-mode filter) and would serve the model live, including an older hosted gateway reading the flag through the mirror. The console's declare-first flow writes this for you and the declare-YAML says so inline.
requirelistnoHard constraints: schema_conformant, grounded, zero_retention, trace_visible (v2 — reasoning trace exposed to the caller). grounded is satisfied natively OR — when a search provider is configured — via the grounding-inject rung (search results injected into the prompt, citations normalized onto RunMeta.citations, search cost accounted). zero_retention is an opt-in data-governance filter (G-3): it routes only to zero-retention–designated endpoints, and fails closed (no designated candidate → the no-eligible-candidate error, never a silent non-ZDR dispatch). A single run can opt in without editing the bundle via RunOptions.zeroRetention: true. How require filters and orders candidates at run time is on Routing & reliability.
preferlistnoAdvisory ordering: cost (ascending via the pricing snapshot; unknown cost sorts last), latency (accepted and currently a no-op — declared order stands; observed-latency sort is deferred to the observed-health work). Ordering is explained on Routing & reliability.
prompt.systemrelative pathyesTemplate file (see below).
prompt.variablesstring listyesEvery {{var}} referenced by the template must be declared here.
policyobjectyesSee below.
capturebooleanno (default false)v2. Local-only replay capture opt-in: every attempt's rendered variables + output text are written to the local SQLite captures table. Captures stay local unless the route's custody consent turns content upload on; the control-plane capture setting wins over this flag.
cachebooleanno (default false)v2 (WS-C). cache: auto stamps the per-provider cache directive on each attempt (gemini excluded); see Caching lifecycle. This is route-level automatic caching — distinct from the customer-owned explicit handle (RunOptions.cache).
repairobjectnov2. Repair rung for the default variant — see below.
variantslistnov2. Named serving variants — see below. Absence = pure v1 semantics.
dimensions live in modelrig/rig.yaml, not the bundle. Project-level cost dimensions ({ key, label, required }) that back /costs and the required-tag hygiene warning are declared once in the rig manifest — see Tags, dimensions, and the graph.

JSON Schema dialect

The schema file is validated with Ajv, configured for JSON Schema 2020-12 as the primary dialect with draft-07 also accepted — a schema declaring "$schema": "…/2020-12/schema" or "…/draft-07/schema" both compile (the draft-07 meta-schema is registered explicitly). Author 2020-12 for new schemas; existing draft-07 schemas keep working.

Two properties of the configuration are load-bearing when you author a schema:

The gateway and the probe harness (packages/modelrig-probes) build an identical Ajv instance by design, so a fixture scores exactly as it serves.

policy

fieldtypenotes
retriesmapPer-failure-class retry budgets, e.g. { content_invalid: 2, network: 4, capacity_shed: 3 }. Missing class = 0 retries. Budgets are non-fungible. Terminal classes (budget_exhausted, invariant_violation) are rejected here at load time.
timeout_mspositive integerWall-clock deadline per attempt; breach maps to the timeout class.
tierstandard \flex \priorityRequested service tier. The tier actually served is recorded separately (servedTier) — silent downgrades become visible in telemetry.
jsonnative \json_modeEmission rung. native: candidates need native strict schema enforcement (structured_native); the schema goes to the provider natively. json_mode: candidates need either mechanism; the schema is additionally coached into the prompt with strict formatting guidance.
samplingobjectnoDeclared sampling — { temperature?, top_p?, max_output_tokens? }. Preserve exactly what the original call used. Absent = every adapter keeps its own defaults (Gemini runs at temperature 1.0); a call that relied on a specific temperature must declare it or its behaviour silently changes. Each field is optional and sent to the provider as declared (no clamp); ranges are validated at load — temperature 02, top_p (0, 1], max_output_tokens integer ≥ 1 — an out-of-range value is a config error. The block is strict: an unknown or misspelled key (e.g. temperatur, or camelCase maxOutputTokens) is a config error too, so a typo can't silently drop your sampling. max_output_tokens is the routed-lane exposure of the existing adapter cap. The route's sampling applies to the primary attempt AND its repair/extractor followups (one knob per route).

policy:
  timeout_ms: 60000
  tier: standard
  json: native
  sampling: { temperature: 0.1, max_output_tokens: 8192 }   # preserve the original call's sampling
Gemini 3 note. Google recommends temperature: 1.0 for Gemini 3 and warns that sub-1.0 values can loop / degrade on complex reasoning tasks (classification and extraction are typically fine). Declaring a sub-1.0 temperature on a Gemini-3 candidate loads and is sent as declared, with a load-time advisory naming the model and value.

Variants (bundle format v2 — Phase 3)

A route may declare variants — named bindings served via rig.run(route, { variant: "name" }). Omitting variant (or passing "default") serves the route's base config exactly as v1 did; v1 bundles remain valid unchanged. Every telemetry row records served_variant.


variants:
  - name: cheap
    candidates:               # narrowing ONLY — must be ⊆ the route's list
      - provider: openai
        model: gpt-5.4-mini
    json: json_mode           # rung override for this variant
    repair:
      max_repairs: 2
      repair_model: openai/gpt-5.4-mini
  - name: scaffolded
    scaffold: |               # reasoning scaffold, appended before coaching
      Think through the drivers step by step before emitting JSON.
  - name: reworded
    prompt_override: ./prompts/echo-v2.system.md   # inline text also accepted
fieldnotes
nameUnique per route; "default" is reserved for the base config.
candidatesSubset of the route's declared candidates — variants narrow, never widen (load error otherwise; the candidate-set invariant holds through variants).
prompt_overrideReplaces the system template (rendered with the same variables + capability conditionals). Mutually exclusive with prompt_append.
prompt_appendAppended to the rendered system prompt.
scaffoldReasoning-scaffold text appended after prompt_append, before json coaching. Manual authoring only this phase.
jsonRung override (native \json_mode).
repairVariant-level repair rung, overrides the route-level repair.

repair (the repair rung of the candidate ladder — Phase 3)


repair:
  max_repairs: 2                     # 1 = retry-with-errors only; 2 adds the repair model
  repair_model: deepseek/deepseek-chat   # required when max_repairs is 2; must be a declared candidate

This is the repair rung of the candidate ladder — the ladder's rungs (resolve → render → dispatch → validate → repair → fall-through) are named in full on Routing & reliability.

On a schema-invalid output with repair enabled: attempt 1 re-asks the SAME model with the ajv error summary appended; attempt 2 hands {invalid output, errors, schema} to the designated repair model with a fixed repair prompt. Repair attempts draw from their own repair budget (never the content_invalid budget), and repaired rows carry repaired_by in telemetry — repair cost is visible, not hidden.

Requirement → capability mapping (registry-wired since Phase 3)

requirementsatisfied by
schema_conformant + json: nativestructured_native
schema_conformant + json: json_modestructured_native OR json_mode
groundedgrounded_native (native directive on dispatch) OR a configured search provider (grounding-inject)
trace_visibletrace_visible (e.g. deepseek-reasoner's exposed reasoning_content)
zero_retentionzero_retention — a model the credential-scoped registry facts designate zero-retention under our managed account (a BYOK key is scoped separately). Opt-in; fail-closed when absent.

Capability flags are resolved from registry facts (the packaged registry/registry.json, env-overridable via MODELRIG_REGISTRY_PATH) with per-flag precedence capabilityOverrides > probed > declared — a probed-false trumps a declared-true (DeepSeek's declared schema support is revoked because every probed sample served via json_mode coaching). Models absent from the registry fall back to the adapter-static baseline below. This table is the registry-absent fallback for gemini, openai, and deepseek; a provider not shown here falls back to its own adapter's declared capability set.

capabilitygeminiopenaideepseek
structured_native
json_mode
grounded_native
context_cache_explicit
prompt_cache_key
prefix_cache_implicit
tier_flexflex-eligible models only (no mini/nano)

Overridable per provider via RigConfig.capabilityOverrides (replaces the whole flag set for that provider — useful for pinning a route to one candidate in tests).

Templates

Versioning discipline

The (route, version) pair keys the compiled validator cache, telemetry rows, and the console's routes mirror. Editing a bundle without bumping version makes telemetry lie across the change — bump every time.

When it doesn't work

These are the load-time and first-run failures a bundle actually hits, in the loader's own words. The first three fail LOUDLY (the bundle won't load); the last fails SILENTLY (it loads and then behaves unlike the code it replaced) — which is why it is the one to be most careful about.

Still stuck? Open an issue at <https://github.com/modelrig/modelrig/issues> — a human reads them.

Raw-lane defaults (rig.runRaw)

What the raw lane sends when you omit a knob. Each knob is additive — absent ⇒ byte-identical dispatch. The one default with teeth is timeoutMs, which is tier-aware (since 0.5.0): a passthrough should not be shorter-fused than the tier it advertises, so standard/priority target 5 min and flex targets the 15-minute budget ModelRig advertises. An explicit timeoutMs always wins (no pre-dispatch rejection — a short deadline is a choice), and a timeout failure on the raw lane carries a fixHint naming the tier, its budget, and the two fixes (raise timeoutMs, or drop to serviceTier: "standard"). On flex, set timeoutMs in minutes. This table is generated from the RAW_DEFAULTS constant, so it cannot drift from what the runtime resolves.

KnobDefaultNote
timeoutMs300 s (standard, priority) · 900 s (flex)tier-aware wall-clock deadline; an explicit timeoutMs always wins, and a timeout failure carries a fixHint naming the tier + budget
serviceTierstandardforwarded to the adapter; meta.servedTier reports what actually served. On flex, set timeoutMs in minutes (default 900 s)
temperatureadapter default — not sentabsent ⇒ no temperature on the request (byte-identical dispatch)
topPadapter default — not sentabsent ⇒ no topP on the request (byte-identical dispatch)
maxOutputTokensadapter default — not sentabsent ⇒ no output-token cap on the request (byte-identical dispatch)
groundingabsent ⇒ byte-identicalnative provider grounding; provider support per the raw-lane knob matrix
reasoningabsent ⇒ byte-identicalreasoning/thinking effort; provider support per the raw-lane knob matrix
responseFormatabsent ⇒ byte-identicalsyntactic-JSON forcing (mime-shaping only); provider support per the raw-lane knob matrix
cachenonecustomer cache handle passed through verbatim when supplied; ModelRig never creates, refreshes, or deletes the resource
tagsnonelane=raw and rung=primary are stamped by the SDK and cannot be overridden by a caller tag
tools / toolChoicenoneabsent ⇒ no tools declared; on the raw lane a grounding-originated functionCall part is NOT a tool-call turn
stepKeyauto — tags.step, else <provider>/<model>#<n> inside a run contextnames the ground-truth step a run context records for this call (G1); explicit key wins; no run context ⇒ no step

Raw-lane provider × knob support (rig.runRaw)

Since 0.4.0: reasoning beyond gemini, responseFormat, and the DeepInfra/Fireworks prompt_cache_key mapping. The openai/grok prompt-cache hints were already true on 0.3.0.

The Lane-B rig.runRaw seam forwards a set of provider knobs beyond the core prompt/sampling ones. Each is ADDITIVE — absent ⇒ byte-identical dispatch — and a knob a provider cannot honor FAILS CLOSED pre-dispatch (RigFailureError, class invariant_violation; no meter, no provider call, no telemetry row), never a silent drop. This table is the single source of truth: the runtime guard (assertRawKnobsSupported) and this matrix are both generated from the same RAW_KNOB_SUPPORT table, so what you read here is exactly what the guard enforces.

ProvidergroundingreasoningresponseFormatcacheserviceTierdestination _(UNRELEASED)_
gemininative — Google Search (googleSearch tool)levelthinkingConfig.thinkingLevel, thinking-capable models only (see GEMINI_THINKING_CAPABLE_MODELS)responseMimeType: application/json (mime-shaping only — no schema, validated stays false)cache.key = a cachedContent resource handle (customer-owned lifecycle)flex / standard / priority honoreddirect only — vendor key (BYOK / configured)
openainative — web_search tool (Responses API) — released (live gate passed 2026-09-09)levelreasoning_effort (1:1); a non-reasoning model's rejection is classified teachablyresponse_format: { type: json_object }cache.keyprompt_cache_key; ttlSeconds ≥ 86400prompt_cache_retention: 24hflex (allowlist) / priority; flex→standard is a customer-side degrade (§3.2)direct only — vendor key (BYOK / configured)
deepseekfail-closedno effort control — reasoning is model selection (deepseek-reasoner)response_format: { type: json_object } — the prompt must contain the word "JSON" (caller's responsibility on the raw lane)automatic prefix cache — the key is not usedno tiers — always standarddirect only — vendor key (BYOK / configured)
anthropicfail-closedextended thinking is not wired on the raw lane (future work)fail-closed — no json_object equivalent (structured output is a forced tool call)prefix caching via cache_control markers — the key is not usedno tiers — always standarddirect only — vendor key (BYOK / configured)
groknative — web_search tool (xAI Agent Tools / Responses API); replaces retired Live Search — released (live gate passed 2026-09-09)coarsened (disclosed): minimal,low → "low"; medium,high → "high"reasoning_effortresponse_format: { type: json_object }cache.keyprompt_cache_key (routing hint)no tiers — always standarddirect only — vendor key (BYOK / configured)
deepinfrafail-closedlevelreasoning_effort (1:1); a non-reasoning model's rejection is classified teachablyresponse_format: { type: json_object }cache.keyprompt_cache_key (routing hint)no tiers — always standarddirect · modelrig-served — ModelRig-owned key (managed serving, UNRELEASED)
fireworksfail-closedlevelreasoning_effort (1:1); a non-reasoning model's rejection is classified teachablyresponse_format: { type: json_object }cache.keyprompt_cache_key (routing hint; caching is automatic host-side)no tiers — always standarddirect · modelrig-served — ModelRig-owned key (managed serving, UNRELEASED)
zaifail-closed — native GLM web_search wired in B4blevelreasoning_effort (1:1); a non-reasoning model's rejection is classified teachablyresponse_format json_objectcache.keyprompt_cache_key (routing hint)no tiers — always standarddirect only — vendor key (BYOK / configured)

Fail-closed is discoverable, not a surprise. A knob a provider cannot honor throws RigFailureError invariant_violation PRE-DISPATCH (no meter, no provider call, no telemetry row) — read this table before you call, not after. reasoning has NO per-model allowlist outside gemini. openai/deepinfra/fireworks forward level verbatim and let the provider enforce; a rejection surfaces as a classified failure that names the fix. grok coarsens (disclosed above). deepseek reasoning is model selection (deepseek-reasoner); anthropic extended thinking is unwired. responseFormat is mime-shaping, not schema enforcement. json_object forces syntactic JSON only — the raw lane still never validates or repairs, and meta.validated stays false (F4). §3.2 flex degrade is customer-side. ModelRig does not replicate the gemini direct-path infra-retry wrapper on the raw lane; under flex a capacity rejection surfaces classified (capacity_shed) for your own → standard retry ladder — a NAMED difference for your migration record, not a silent one. destination is UNRELEASED. The serving-destination column names which providers can be modelrig-served (reached on ModelRig's own key) vs direct only. Managed serving is gated behind a config flag (config.managedServing, default off, set in code — not an env var) and remains UNRELEASED (no GA); until it is enabled every call is direct (ABSENT ⇒ byte-identical).

Ask it in code instead of duplicating this table: rawKnobSupport(provider) returns the row above, and GEMINI_THINKING_CAPABLE_MODELS is the exported set the gemini reasoning gate uses (both from the package root).

Hosted proxy — /v1/chat/completions message handling

The OpenAI-compatible proxy accepts a messages[] array. Two rules keep an untrusted caller turn from being mistaken for a trusted instruction: