Routing Configuration
Routing turns the model id sent by a client into one concrete provider and upstream model.
Top-level routing fields
Section titled “Top-level routing fields”| 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. |
Model alias discovery
Section titled “Model alias discovery”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.
Codex Pool low-quota protection
Section titled “Codex Pool low-quota protection”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.
Model resolution order
Section titled “Model resolution order”opencodex resolves the requested model in this order:
- A configured
policy/<id>or routing-profile alias, executing the policy evaluator and routing the selected candidate. An unresolvedpolicy/<id>falls through to the later rules. - A configured
<account-selector>/<native-openai-model>namespace, routed through exactly the mapped stored Codex account. An invalid or unavailable exact target fails closed. - A canonical
combo/<id>or configured combo alias. Canonical ids win before alias matching. - An explicit
<provider>/<model>namespace whose prefix names a configured provider. - A bare native OpenAI-family id such as
gpt-*,o1-*,o3-*, oro4-*, routed through the canonical enabledopenaiprovider. - An exact match for a provider’s
defaultModel. - A known provider-family model prefix.
- An exact model in a provider’s configured
modelslist. 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.
Blocked-model redirects
Section titled “Blocked-model redirects”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" }}Exact Codex account selectors
Section titled “Exact Codex account selectors”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.
Combos (config.combos)
Section titled “Combos (config.combos)”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.
Combos vs policy profiles
Section titled “Combos vs policy profiles”- 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.
Catalog eligibility
Section titled “Catalog eligibility”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, providermodelContextWindows/contextWindow, a known positivemaxInputTokenson 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 byproviderContextCapswhen set); and - a non-empty
inputModalitiesintersection, 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.
Request history and routing analytics
Section titled “Request history and routing analytics”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 opaquecursorpagination.GET /api/request-history/:requestIdreturns 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.
Index validation and recovery
Section titled “Index validation and recovery”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.
Migration
Section titled “Migration”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.

