Skip to content
18.1k

Routing Configuration

Routing turns the model id sent by a client into one concrete provider and upstream model.

Field Type Default Meaning
defaultProvider string "openai" Final provider used when no earlier model rule matches. It must name an enabled configured provider.
combos? Record<string, OcxComboConfig> {} Virtual combo/<id> models built from ordered provider/model targets.
routingProfiles? Record<string, OcxRoutingProfileConfig> {} Virtual policy/<id> models that select among an explicit candidate allowlist using hard capability requirements and deterministic scoring.

Set preferModelAliasIds: true to publish configured or enabled built-in model aliases as the id in GET /v1/models. The default is false. Promoted rows retain their previous qualified ID in native_id and keep alias_of; both IDs remain routable. Models without aliases keep their existing IDs. If a short alias is ambiguous across providers or is shadowed by another route, the row keeps its qualified ID so selecting it still reaches the intended model. The qualified ID also stays when a built-in short alias would produce an unparseable synthetic Fast selector. Codex’s own model catalog is unchanged.

codexPool.lowQuotaProtection applies only to stored Codex Pool accounts. The Desktop/main account keeps its separate 98% hard lock. The optional shape is:

{"codexPool":{"lowQuotaProtection":{"enabled":true,"threshold":80,"actions":{"pause":true,"notify":true,"desktop":true},"windows":{"short":true,"weekly":true}}}}

Absence or enabled: false disables it. threshold is a finite inclusive percentage from 1 through 100. An enabled policy needs at least one true action and one true window. A selected 5-hour or weekly window triggers at usage >= threshold; either selected window is enough. Only fresh accepted observations qualify: there is no extra polling, and credits-only, cached, expired, monthly, custom-window-only, and raw out-of-range usage observations are ignored for this policy. Display bars may still clamp invalid upstream percentages. The policy is independent of proactive account switching.

With pause, the account leaves Pool selection in memory before the next request. The server coalesces a config save after the observation turn and retries failed saves for a bounded time. Normal shutdown waits briefly for pending saves. A timed-out save that is already running remains pending until it actually succeeds or fails; a queued save is cancelled. A failed save is visible in the event history. A restart before a successful save cannot preserve the pause. In-flight requests keep their captured account. Manual resume suppresses repause across all currently high windows for that account until a below-threshold reading or a new reset boundary re-arms a window. A reset never resumes an account automatically.

With notify, the server writes a local log line with window and percentage but no account id and records a bounded event. Authenticated GET /api/codex-auth/low-quota-events exposes only that server’s events, including account id, window, usage percentage, known reset time, timestamp, and status. The default status is logged: the alert reached the log and event history only. Each account/window logs once per server-local episode.

Optional actions.desktop: true (it requires notify: true) also hands each notice to the operating-system notifier of the machine running the proxy: /usr/bin/osascript on macOS, or notify-send from /usr/bin, /usr/local/bin or /bin on Linux. The notification names the account only by its pseudonymous log label (for example p1a2b3c), never its email, alias or id, and includes the window, usage, threshold and known reset time. Windows and hosts without a supported notifier keep the alert logged. delivered means the OS notifier accepted the notice; it cannot see whether the system’s notification settings then hide it. A failed or timed-out notifier is recorded as failed; an eligible observation at least five minutes later can retry it, without repeating the log line. Turning desktop on or off applies to the next observation without a restart. A notice still in flight is cancelled at shutdown, and when the next quota observation detects that its episode ended (reset boundary, recovery, account removal or policy change); until then the notifier is bounded by a five-second timeout.

opencodex resolves the requested model in this order:

  1. A configured policy/<id> or routing-profile alias, executing the policy evaluator and routing the selected candidate. An unresolved policy/<id> falls through to the later rules.
  2. A configured <account-selector>/<native-openai-model> namespace, routed through exactly the mapped stored Codex account. An invalid or unavailable exact target fails closed.
  3. A canonical combo/<id> or configured combo alias. Canonical ids win before alias matching.
  4. An explicit <provider>/<model> namespace whose prefix names a configured provider.
  5. A bare native OpenAI-family id such as gpt-*, o1-*, o3-*, or o4-*, routed through the canonical enabled openai provider.
  6. An exact match for a provider’s defaultModel.
  7. A known provider-family model prefix.
  8. An exact model in a provider’s configured models list.
  9. defaultProvider, preserving the requested model id.

Disabled providers are excluded. An explicit namespace for a disabled provider fails instead of falling through. Provider entries are checked in their JSON insertion order for rules that can match more than one provider, so use explicit namespaces when a bare model could be ambiguous.

blockedModelRedirects is an optional top-level Record<string, string> of exact resolved model-id replacements, unset by default. Bare keys still match the native model after provider, account, and alias resolution. A target without an explicit, different configured provider keeps that selected provider and account and replaces only the upstream model id, once. This preserves existing bare and slash-valued mappings, including mappings on provider-qualified requests.

A target explicitly naming a different configured provider (for example, google-antigravity/gemini-3.8-flash-high) instead routes through that provider. An exact <source-provider>/<resolved-model> key with such a target takes precedence over a bare key; other qualified keys have no effect. Cross-provider redirects can chain through resolved aliases, with one shared five-edge limit and cycle detection. A pinned account selector never leaves its account: a cross-provider target fails closed, whether its key is bare or account-qualified. The destination uses its own credentials and quota, and never inherits source account fields; a caller Authorization header addressed to the source route is stripped, as for combo and policy routes. A disabled destination, a forward destination, or a key/OAuth destination without usable stored credentials keeps the source route’s legacy behavior. Under a routing policy, every redirect target must itself be a declared eligible candidate, even when it keeps the same provider. The initial evaluation’s eligible provider/model set remains fixed through fallback and subagent recovery. Retries use concrete candidates, so a combo alias cannot replace a candidate’s destination. Different candidates that resolve to a destination already attempted do not send the turn there again. An eligible virtual model keeps its normal wire-model mapping without adding that wire id to the profile’s allowed candidates. If a local replacement is skipped, fallback retains the last upstream failure. Malformed redirect maps are ignored with a warning on load and rejected on configuration writes. Redirected routes record blocked-model-redirect; omitting the setting leaves routing unchanged.

{
"blockedModelRedirects": { "gpt-5.6-terra": "gpt-5.6-luna" }
}

codexAccountNamespaces maps a public selector such as side to one stored Codex account. A request for side/gpt-5.6-sol uses only that account, even when the canonical openai provider is in Direct mode, and sends the bare gpt-5.6-sol model id upstream. Only bare native OpenAI-family ids are valid after the selector. Account-scoped ids observed in Codex’s current model catalog may also be preserved exactly when they are not yet part of opencodex’s static set; the observation must carry the field shape of a real catalog row, stays qualified to its matching account selector, and is never promoted into the global bare model list. That shape check filters malformed and minimal rows — it is not a trust control, because the models cache is a user-owned file and a complete hand-written row is indistinguishable from an upstream observation. Nothing new becomes routable: a bare gpt-* id under an account selector is accepted by the router regardless of the catalog.

Exact selection bypasses Pool assignment strategy and ordinary thread affinity. If the mapped account is missing, paused, cooling down, unusable, or requires reauthentication, the request fails closed instead of switching accounts and does not change the active Pool account. When at least one eligible selector is configured, Codex catalogs hide bare native picker rows and list a separate <selector>/<native-openai-model> row for each selector. Bare native ids retain normal Pool/Direct routing and remain in raw /v1/models discovery unless explicitly disabled. Selectors whose mapped stored account is missing are not advertised. Selector validation, collision rules, and privacy guidance are documented in Provider Configuration.

The Codex Auth page exposes this picker behavior as an opt-in. Disabling it hides generated selector-qualified picker rows and restores the ordinary GPT rows, but it does not remove the mappings or change exact <selector>/<model> routing. Re-enabling therefore restores the same public labels. Account and setting mutations are persisted before a bounded catalog refresh; an ocx sync warning means only that the picker catalog still needs convergence, not that the routing change was lost.

Each combo key is an id matching [A-Za-z0-9][A-Za-z0-9._-]{0,63}. It is always directly addressable as combo/<id> and may also expose one alias. Aliases must be unique, cannot occupy the combo/ namespace, and cannot use reserved bare native families such as gpt-*, o1-*, o3-*, o4-*, or codex-* unless nativeAlias: true explicitly enables the Desktop compatibility contract.

Key Type Default Meaning
targets { provider: string; model: string; weight?: number; lastResort?: boolean; metered?: boolean }[] required Ordered concrete routes. weight is 1–10000 and defaults to 1. lastResort marks an emergency-only target; see cooldownWaitPolicy. metered: true requires key consent or loopback-only combo consent; see consent.
allowMeteredTargets? boolean false Operator consent for local callers on a loopback-only proxy. A non-loopback listener disables this flag; configured-key and linked callers still need key consent.
strategy? "failover" | "round-robin" | "random" | "least-used" | "reset-window" | "jev" "failover" Selection strategy. Target order is failover priority; weights shape round-robin and random draws; least-used follows recorded successes; reset-window follows the soonest quota reset; JEV makes one bounded decision for the initial eligible target and effort, then uses ordinary ordered fallback.
stickyLimit? number 1 Successful requests retained in one round-robin batch. Range 1–100. Applies only to round-robin.
cooldownMs? number unset → upstream fallback (5 s for request-rate 429 codes 1302/1305, otherwise 60 s) Range 1–600000. When set, applies whenever no usable upstream Retry-After or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. Upstream signals take precedence. An explicit upstream Retry-After is capped at 24 hours; reset-derived, configured, and fallback cooldowns are capped at 10 minutes.
waitForCooldownMs? number 0 Maximum wait for the earliest eligible cooling target on each selection attempt. Range 0–600000; an abort cancels the wait. A single-target combo with a nonzero wait holds the request up to this ceiling and retries the same target instead of failing immediately; if no target was ever dispatched the wait ends in combo_unavailable, otherwise the last upstream failure is returned.
cooldownWaitPolicy? "before-last-resort" unset Defers targets marked lastResort: they are used only when no normal target is available, for every strategy. waitForCooldownMs only adds the wait for a cooling normal target, so at its 0 default nothing waits and the last resort is used as soon as no normal target is available. The deferral and the ordinary wait share one waitForCooldownMs budget per selection attempt. Only that exact string opts in; lastResort is inert without it.
defaultEffort? "low" | "medium" | "high" | "xhigh" | "max" | "ultra" | null unset defaultEffort fills an absent reasoning.effort in fallback mode, or overrides valid caller effort in explicit force mode when the combo has a non-null default and the selected target has a known, nonempty supported ladder. If the target supports the configured value, it is retained; otherwise the highest supported rung at or below it is used, or the lowest supported rung when none is lower. Unknown or empty ladders omit the default.
defaultEffortMode? "fallback" | "force" "fallback" Preserves caller precedence by default. Explicit force requires a valid non-null default, respects target capability and can increase cost and latency. reasoningEffortMode remains independent.
reasoningEffortMode? "strict" | "adaptive" "strict" "strict" intersects all known target ladders, including empty ones; "adaptive" excludes empty ladders. Unknown ladders are catalog wildcards in both modes. At dispatch, explicit empty ladders remove effort/thinking controls in both modes; unknown ladders do so only in adaptive. reasoning.summary is preserved. Known nonempty targets retain their effort resolution, and target selection/order is unchanged.
imageInput? "auto" | "disabled" "auto" "auto" publishes image only when every target supports images; "disabled" forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch).
alias? string — Optional public model id in place of the canonical picker slug.
nativeAlias? boolean false Let a currently supported bare native id take precedence only for that unqualified id. Bare gpt-5.6-* ids use Codex Pool/Direct credentials. Account-qualified routes remain distinct. Provider-qualified routes such as openai-apikey/gpt-5.6-* use their configured API-key route and never fall through to the native alias.
displayName? string — Display-only catalog label, required and non-empty for a native alias.
decisionProvider? string "jev" strategy: "jev" only. "jev" (the same as omitting it, and stored as omission) is the TypeSafe decision service, valid without a provider row; any other value must name a configured provider with adapter: "jev-decision" and a full HTTPS decision baseUrl (any path) or a local HTTP /systemone endpoint. Userinfo, query strings, and fragments are refused.
decisionModel? string unset strategy: "jev" only, mutually exclusive with decisionProvider. An ordinary opencodex route (for example ollama/qwen3:4b) asked to pick one offered option as JSON. It runs with the selected provider’s stored credentials, never the caller’s, and cannot resolve to this combo, any JEV combo, or a jev-decision row.
decisionTimeoutMs? number 4000 strategy: "jev" only. Decision deadline before failing open, 1000–120000 ms.
decisionQuotaSignals? boolean false JEV only. Explicit true sends loaded advisory quota evidence to the selected backend; never changes hard eligibility or account selection.
decisionQuotaTiers? { moderate?: number; limited?: number; nearlyExhausted?: number } { limited: 70, nearlyExhausted: 90 } JEV only. Partial overrides merge with defaults; finite 0–100 values must be strictly ascending. moderate is absent unless explicit. Does not enable signals by itself.
sessionPin? { ttlMs: number } off JEV only. Keeps an explicit client session on the target JEV chose for it while the session stays active; ttlMs is the idle window, 60000–86400000. null clears. See Session pin for JEV.

Quota settings, privacy and unknown-pool semantics are described in Advisory quota signals for JEV. Omitted settings survive JEV API updates/rename; null clears, false disables signals without clearing tiers, and switching to another strategy removes both settings.

{
"defaultProvider": "openai",
"combos": {
"coding": {
"targets": [
{ "provider": "anthropic", "model": "claude-sonnet-5" },
{ "provider": "openrouter", "model": "qwen/qwen3-coder-plus" }
],
"strategy": "failover",
"defaultEffort": "high",
"alias": "coding-primary"
}
}
}

For strategy behavior, retryable failures, cooldowns, encrypted v2 task limits, and management commands, see Combos.

The jev strategy is optional. Only the TypeSafe method, used when neither decisionProvider nor decisionModel is set, requires the canonical jev provider credential. That provider is a decision service, publishes no directly routable model, and cannot be a Combo target. JEV sees only currently eligible members of targets; missing, failed, or invalid decisions use the first eligible member, while caller cancellation remains terminal. Adding the provider or Combo never changes defaultProvider or hides direct model rows. See Decision method for the three methods, setup, privacy bounds, and the one-decision-per-call contract. API-key provider/model scopes also apply to TypeSafe and self-hosted decision destinations, before their credentials or request state are read. If the key denies the optional decision destination, no decision request is sent; the ordinary eligible inference fallback still has to satisfy that key’s scope.

Self-hosted decision model (e.g. Ollama tev1)

Section titled “Self-hosted decision model (e.g. Ollama tev1)”

decisionProvider can point a JEV Combo at a self-hosted Jev-API-compatible decision model such as Ollama’s tev1 (Ollama 0.35+, POST /v1/systemone, no API key):

{
"providers": {
"ollama-tev1": {
"adapter": "jev-decision",
"baseUrl": "http://127.0.0.1:11434/v1/systemone",
"allowPrivateNetwork": true,
"defaultModel": "tev1:4b",
"liveModels": false
}
},
"combos": {
"jev-local": {
"strategy": "jev",
"decisionProvider": "ollama-tev1",
"decisionTimeoutMs": 60000,
"targets": [
{ "provider": "openai", "model": "gpt-5.6-sol" },
{ "provider": "openai", "model": "gpt-5.6-luna" }
]
}
}
}

The row’s baseUrl is the full decision endpoint and must end in /systemone; its model is defaultModel, else models[0] (a row with neither fails open without a request). Loopback needs the row’s own explicit allowPrivateNetwork: true, and plain http: is accepted only for localhost or a loopback/RFC 1918/ULA address literal reached without a proxy. Only the row’s own apiKey is sent — none when it is unset — and a key referencing the TypeSafe environment variables or another provider’s keychain entry is refused, so TypeSafe credentials never reach it. Options are sent as description strings, which Ollama requires. tev1 was trained on 2–24 options, so keep target × effort pairs at 24 or fewer (fewer than 2 or more than 26 fail open without a request); its effective context is about 2k tokens and OpenCodex clips the task text to 500 characters. Keep the model resident (OLLAMA_KEEP_ALIVE=-1) and raise decisionTimeoutMs for slow services; see System One-compatible server. In the dashboard, choose System One-compatible server in the JEV Combo’s Decision method section under Models → Combos, then pick the row and set Decision timeout (ms).

Routing policy profiles (config.routingProfiles)

Section titled “Routing policy profiles (config.routingProfiles)”

Routing policy profiles are the Router Intelligence selection layer: an explicitly requested policy/<id> (or configured alias) selects among a fixed candidate allowlist using hard capability requirements and deterministic, explainable scoring. An explicit policy/<id> request (or a configured alias) executes the evaluator and routes the selected candidate. Existing model ids are never routed through a profile implicitly: the policy/ namespace and profile aliases are the only entry points, and both are validated against the model resolution order above.

Each key is an id matching [A-Za-z0-9][A-Za-z0-9._-]{0,63}, always addressable as policy/<id>, with one optional alias. Aliases must be unique and cannot collide with configured providers, the <provider>/<model> routing namespace, combos, codex account namespaces, the policy/ namespace, or reserved bare native families (gpt-*, o1-*, o3-*, o4-*, codex-*).

Key Type Default Meaning
candidates { provider: string; model: string }[] required Explicit allowlist of provider/model refs. No implicit expansion.
alias? string — Optional public model id in place of policy/<id>.
require? object {} Hard capability requirements evaluated before scoring (see below).
optimize? object latency 0.55, health 0.25, cost 0.10, quota 0.10 Scoring weights, normalized deterministically. health, quota, and cost have score dimensions; the configured-priority share is 1 - health - quota - cost (default 0.55), and latency folds into that priority share rather than scoring independently.
limits? object — Hard limits. maxEstimatedCostUsd excludes a candidate when its estimated cost is known and above the cap. When that cap is set, onUnknownCost ("allow" default, or "exclude") controls unknown estimates: allow prevents a cap-specific exclusion and records cost.capOutcome: "unknown-allowed"; exclude emits cost-limit-unknown and capOutcome: "unknown-excluded". onUnknownCost alone (no cap) is inert. Separate from unknownEvidence.cost, which can still exclude or penalize unknown prices via unknown-price / scoring.
unknownEvidence? object capability exclude, health/quota/cost penalize How unknown evidence is treated per dimension: allow, penalize, or exclude. Unknown never becomes zero.

require supports: minContextWindow (positive integer), minQuotaHeadroom (0..1 fraction), and the booleans tools, imageInput, structuredOutput, localOnly, remoteAllowed, encryptedCodexTasks; plus reasoningEffort and serviceTier strings.

For unknownEvidence.capability, penalize currently behaves like allow: scoring has only a configured-priority component until a capability score dimension ships (planned with RI-06+), so penalize cannot yet change the selected candidate.

Request evidence is evaluated against candidate capabilities together with the profile require block; a candidate must satisfy both to be eligible. On the live request path the proxy derives tools and image-input evidence from the request body; context-window size and the remaining evidence dimensions stay unknown at routing time. Use the dry-run API/CLI to inspect the full evidence surface for context-sensitive profiles.

The CLI dry-run accepts request-evidence flags but cannot supply candidate capability evidence yet; candidate evidence is provided through the API (POST /api/routing-profiles/dry-run).

{
"routingProfiles": {
"fast": {
"alias": "ocx/fast",
"candidates": [
{ "provider": "anthropic", "model": "claude-sonnet-5" },
{ "provider": "openai", "model": "gpt-5.6-sol" }
],
"require": { "tools": true, "minContextWindow": 128000 },
"optimize": { "latency": 0.55, "health": 0.25, "cost": 0.10, "quota": 0.10 },
"limits": { "maxEstimatedCostUsd": 0.50, "onUnknownCost": "allow" },
"unknownEvidence": {
"capability": "exclude",
"health": "penalize",
"quota": "penalize",
"cost": "penalize"
}
}
}
}

CLI: ocx route policy list [--json], ocx route policy show <id> [--json], and ocx route policy dry-run <id> [--model-context <tokens>] [--tools] [--image] [--structured-output] [--json]. Dry-run evaluates candidates without sending any upstream request.

Quota evidence (optimize.quota, require.minQuotaHeadroom, unknownEvidence.quota) comes from account-keyed Codex and Anthropic quota caches. A runtime candidate receives cached quota only when the evidence already identifies the account. Unbound canonical openai and Anthropic candidates remain unknown during policy evaluation because Pool selection, Direct caller identity, provider rotation, and thread affinity are resolved after the policy chooses a provider/model; a process-active account is not used as a substitute. Quota evidence never changes account selection, session affinity, cooldowns, or switching behavior — it only feeds policy scoring. To see quota-aware behavior in an API dry-run, supply account refs in the candidate evidence sent to POST /api/routing-profiles/dry-run: candidates[].codexAccountId (Codex pool, provider openai) or candidates[].accountRef (Anthropic) derives the matching cached account quota; an explicit candidates[].quota object is echoed as given. The CLI dry-run cannot supply these per-candidate account fields.

  • A combo is explicit target routing with a selectable strategy (ordered failover, smooth weighted or random balancing, least-used, reset-window, or one bounded JEV first-pick decision): the configured strategy decides, and retryable failures advance through the list.
  • A policy profile is evidence-based selection among configured candidates: hard capability requirements filter first, then deterministic scoring ranks the survivors.

Both are virtual namespaces with aliases and collision validation; they differ in how a candidate is chosen. Profile scoring combines the configured-priority component with the health (RI-06), quota (RI-07), and cost (RI-08) score dimensions where evidence is present; the latency weight folds into the priority share rather than scoring independently. Cost is also enforced through the limits.maxEstimatedCostUsd cap: a candidate whose estimated cost is known and exceeds the cap is excluded (cost-limit). When a cap is configured and the estimate is unknown, the default limits.onUnknownCost: "allow" records cost.capOutcome: "unknown-allowed" on the route-decision trace without a cap exclusion; set onUnknownCost: "exclude" for a fail-closed ceiling (cost-limit-unknown). Cap outcome is not overall eligibility — unknownEvidence.cost: "exclude" can still add unknown-price and mark the candidate ineligible. Per-request route-decision traces are recorded when a policy profile executes.

A combo remains directly routable even when it cannot be listed. ocx sync, /v1/models, and the Codex picker list it only when every target exposes capabilities that can be intersected:

  • a positive contextWindow, from live metadata, registry hints, provider modelContextWindows / contextWindow, a known positive maxInputTokens on the member row, or — when the provider is known and enabled but every source still omits a window — a conservative 128,000-token fallback (clamped by providerContextCaps when set); and
  • a non-empty inputModalities intersection, treating an omitted member value as ["text"].

A target on a disabled provider (even with a complete discovery row), on an unknown provider with no discovery row, or targets with disjoint modalities, removes the combo from the catalog. Sync emits a summary warning and the dashboard marks it Needs attention. Add context metadata, align modalities, or target models with discoverable compatible capabilities.

  • GET /api/request-history - cursor-paginated full history from the derived index (routing-history.sqlite), with filters (provider, model, requestedModel, status, conversationId, surface, inboundProtocol, apiKeyId, profileId, fallback, from, to) and opaque cursor pagination. GET /api/request-history/:requestId returns one canonical row.
  • GET /api/request-history/:requestId/route-decision - the why-this-route explanation: trace (candidates, exclusions, score components, profile + revision), execution attempt sequence, and final outcome.
  • GET /api/routing-analytics - success/failure/cancelled/fallback rates, p50/p95/p99 duration and TTFT, incomplete-stream rate, cooldown-triggering failures, cost per successful request, coverage, confidence, and an explicit truncation flag.
  • GET /api/routing-profiles, POST /api/routing-profiles/dry-run - profile inspection and dry-run evaluation (no upstream dispatch).

Returned history and route-decision payloads expose only masked request metadata (for example opaque apiKeyId labels). They do not include credentials, raw prompt bodies, or provider secrets.

CLI: ocx logs history, ocx logs get <request-id>, ocx logs explain <request-id>, ocx logs rebuild-index, ocx logs index-status, ocx route policy list | show | dry-run | evaluate.

An existing history index is checked in a read-only worker when first opened. History queries wait for that initial check without blocking the proxy’s serving thread. Synchronous routing reads report busy during validation, so routing profiles apply their unknownEvidence.health policy. A successful small inline rebuild is usable immediately; a worker rebuild is checked before publication.

After a successful check or rebuild, the next read at least five minutes later starts one background integrity check. Reads from the verified index continue during that check. Normal refreshes retain schema, source-identity and offset validation without a full synchronous scan, and an idle index starts no timer. Temporary check failures, such as a busy database or unavailable worker, leave the index intact and retry on a subsequent read after a 30-second backoff. An index that has not completed initial validation remains unavailable meanwhile.

Detected corruption triggers automatic reconstruction from usage.jsonl, with one retry for an affected asynchronous history or analytics read. Closing or rebuilding the index discards old check results and waits for the scanner to release the file before replacing it. Validation and recovery never rewrite the canonical ledger. The initial check still takes time proportional to index size; later background checks run only when the index is used.

routingProfiles is optional and additive: existing config files load unchanged. Old usage.jsonl rows without routeDecision parse unchanged. The history index is disposable - deleting routing-history.sqlite triggers an automatic rebuild from usage.jsonl on the next query; ocx logs rebuild-index forces one. Once usage.jsonl reaches 1 MiB, a rebuild runs on a worker thread, so the proxy keeps serving requests while it runs. Until it finishes, history queries wait for it and routing profiles treat historical health as unknown evidence (the profile’s unknownEvidence.health policy applies). Nothing in this system auto-tunes weights, budgets, or candidate sets.