Management API
The Management API is opencodex’s control plane. The dashboard at
http://localhost:10100 is one client of it; headless ocx provider, model, combo, account,
settings, diagnostics, and lifecycle commands are clients too. The API is available only while the
proxy is running.
Use the Web Dashboard for an interactive client, or this reference when building automation. Persistent values ultimately follow Configuration.
Authentication model
Section titled “Authentication model”The Management API has its own admin credential, independent of data-plane API keys. At startup, opencodex resolves it in this order:
OPENCODEX_ADMIN_AUTH_TOKEN, when set.- A generated
ocx_admin_*token in a hardened secret file.
The file-backed token is accepted only after its directory and file permissions or ACLs have been hardened. If that cannot be guaranteed, management authentication fails closed and the API returns 503 until an environment token is supplied or the file state is repaired.
Send the admin token in either form:
X-OpenCodex-API-Key: <admin-token>Authorization: Bearer <admin-token>Loopback dashboard sessions
Section titled “Loopback dashboard sessions”On a loopback bind, the dashboard bootstrap can receive a short-lived ocx_session_* credential.
Each session lasts five minutes and is bound to the exact dashboard origin. Safe requests must
match that origin. Unsafe methods also require the browser Origin and the session’s CSRF token.
When data-plane authentication is required, which includes remote binds, the loopback bootstrap
does not mint a session. A remote dashboard gets a 12-hour session only through a trusted Tailscale
identity (remoteGui.allowedTailscaleUsers on the Tailscale management ingress) or a one-use
pairing grant; each authorized request extends it. Otherwise a remote operator authenticates with
the raw admin token, and the dashboard asks for it again after a reload because the session lives
only in page memory. See Remote hub.
Common errors
Section titled “Common errors”All endpoint rows below inherit these boundary errors. The “Notable errors” column lists additional route-specific results rather than repeating this table.
| Status | Type or code | Meaning |
|---|---|---|
| 401 | opencodex admin token required |
The admin token or GUI session is missing, invalid, expired, origin-mismatched, or missing CSRF evidence |
| 403 | cross-origin request blocked |
The request origin is outside the management allowlist |
| 404 | not_found |
No management route matched the method and path |
| 413 | request body too large |
A POST, PUT, or PATCH body exceeds the 2 MiB management limit |
| 503 | management API unavailable |
Admin credential initialization or hardening is unavailable |
| 503 | oauth_mutation_busy |
Another OAuth credential mutation holds the writer; response includes Retry-After: 1 |
| 503 | catalog_busy |
Catalog gathering is already at capacity; response includes Retry-After: 1 |
Endpoint matrix
Section titled “Endpoint matrix”Agent and client settings
Section titled “Agent and client settings”| Method and path | Purpose | Notable errors |
|---|---|---|
GET, PUT /api/v2 |
Read or change native multi-agent v2 mode and thread settings | 400 invalid settings; 502 transition or persistence failure |
GET, PUT /api/injection-model |
Read or set the injected sub-agent model, effort, prompt, and guidance settings | 400 invalid model, effort, or body |
POST /api/injection-model/suggest |
Size a described delegated workload and propose a delegation model and effort without writing | 400 invalid work or model; 409 no sizing model |
GET, PUT /api/effort-caps |
Read or set global and sub-agent reasoning-effort ceilings | 400 invalid ladder value |
GET, PUT /api/subagent-models |
Read or order the models advertised to sub-agents | 400 invalid list or more than five models |
GET, PUT /api/subagent-model-fallback |
Read or set the ordered fallback chain and poll interval | 400 invalid list or poll interval |
GET /api/grok |
Read Grok managed-config status and candidate models | 400 status read failure |
PUT /api/grok/selection |
Persist the excluded Grok models | 400 invalid or oversized selection |
POST /api/grok/apply |
Apply persisted Grok configuration through the managed sync | 409 grok_apply_busy; 400/500 apply failure |
GET /api/grok/reset-coupons?accountId=... |
Read remaining Grok billing reset tokens and validity windows for the active or specified xAI account | 400 missing account; 401 unauthenticated; 502 upstream gRPC-Web error |
POST /api/grok/reset-coupons/consume |
Redeem an eligible reset coupon. Body { accountId?, tokenId?, operationId? }. Optional operationId (UUIDv4) makes redemption idempotent: repeating the same ID replays the durable result without double-redemption. |
400 invalid JSON/UUID; 401 unauthenticated; 409 operation_id_owned_by_another_account / coupon_unavailable / operation_token_mismatch / operation_state_changed / attempt_in_progress / attempt_unresolved; 502 upstream error; 503 ledger capacity / ledger_unavailable; 500 attempt_mark_failed |
GET /api/anthropic/reset-grants?accountId=... |
Read the Claude usage-limit reset grants of one Anthropic OAuth account: eligibility, each grant’s resets left, validity window, and the windows it clears, plus any unconfirmed attempt still retryable | 400 no matching account; 401 re-authentication needed; 502 upstream unavailable |
POST /api/anthropic/reset-grants/consume |
Spend one reset grant. Body { accountId, grantId, operationId }; operationId is a UUIDv4 sent upstream as the request ID, so repeating it retries the same claim. Requires a dashboard session. A settled reset lifts the account’s local cooldown only when it is a quota refusal recorded before the claim was first journaled and the windows the grant reported cleared cover every window that refusal named (both windows when it named none). A refusal recorded while the claim was in flight, an entitlement refusal, or an uncovered window stays, including on a same-id replay. The cached quota is dropped and usage is reread with a fresh request, so the pool can select the account before the old deadline. Model-family exclusions stay until that reread proves a family is no longer rejected. |
400 invalid body; 401 re-authentication needed; 403 session_required; 409 grant_not_usable, in_flight, unresolved_prior_operation, unknown_outcome_expired, operation_identity_mismatch; 500 journal_write_failed; 502 unknown_outcome; 503 journal busy, unavailable, or full |
GET, PUT /api/claude-desktop |
Read or persist the Claude Desktop routed/native profile and its model visibility. PUT requires a valid profile, including when setting or clearing visibility (visibility: omit to keep, null to clear); a visibility-only body returns 400 |
400 missing/invalid profile, invalid or unavailable assignment, invalid visibility, or a visibility that hides every available model; 409 profile or supplied visibility changed during discovery/save |
POST /api/claude-desktop/apply |
Write the saved profile to Claude Desktop’s managed config | 400/500 write failure |
GET, POST /api/codex/desktop-compatibility/certificate |
Inspect or explicitly prepare Windows compatibility certificate trust | 403 paired local dashboard required for POST; 409 stale, busy, or incomplete state |
GET, POST /api/codex/desktop-compatibility/runtime |
Inspect or explicitly start, stop, launch, or run a bounded native compatibility trial | 403 paired local dashboard required for POST; 409 unsupported build, untrusted certificate, or incomplete state |
GET, POST /api/codex/desktop-compatibility/settings |
Read or save the next-start observation preference with its current revision | 403 paired local dashboard required for POST; 409 stale, invalid, or unconfirmed configuration |
GET /api/claude-desktop/status |
Inspect saved-versus-applied profile, Desktop health, and appliedModels: { name: string; label: string }[] | null. This is the selected profile’s on-disk static list, not a verified observation of the running Desktop process; null means no applied gateway profile or no readable static list (including discovery-only mode), not an empty list |
400 status read failure |
GET, PUT /api/claude-code |
Read or update Claude Code gateway, auth-mode, model-map, context, agent, and sidecar settings | 400 invalid field or shape |
tokenId, when supplied, must be a non-empty string; invalid values return 400 invalid_token_id before credentials are read or the ledger is opened. Within the 30-day retention window, a same-operationId retry that omits accountId uses the recorded account, even if the active account changed. An executable open retry that omits tokenId uses its recorded token. An explicit different account or token is still refused. A definitive failure clears a matching speculative pending hold in the dashboard, allowing a new explicitly confirmed attempt; actual unknown outcomes remain held.
An attempt is durably claimed before redemption. Only a confirmed upstream redemption response can record redeemed; a coupon missing from the remaining list does not prove this operation succeeded. After a timeout or missing settlement, retries inspect availability without redeeming again and return attempt_unresolved; a recent attempt returns attempt_in_progress. A late inspection refusal settles only an operation still open. In the same transaction, an identity-matching definitive terminal winner is replayed with HTTP 200 and its original code; an unresolved or identity-mismatched winner returns operation_state_changed, preserving same-ID recovery guidance. An unreadable or locked ledger returns ledger_unavailable without dispatching a redemption.
Once a caller supplies an operationId, every response that cannot prove the operation is settled carries that operationId at the top level and keeps the caller on it: attempt_unresolved, attempt_in_progress, attempt_reconcile_failed, operation_state_changed, operation_token_mismatch, attempt_mark_failed, ledger_unavailable, and ledger capacity (the full ledger cannot reserve the ID, so an earlier request holding it may still open it later). This includes refusals before the ledger opens: a malformed tokenId, an unknown account, or a failed account lookup under a supplied ID return 409 attempt_in_progress with the ID, because another request may already hold that operation. A definitive authentication refusal is recorded atomically as a failed operation bound to the requested coupon, so a retry replays it; an existing operation for a different coupon is never replayed. A failed remaining-reset list settles an unclaimed operation as fetch_resets_failed. A redemption reply counts as confirmed only when its gRPC-Web framing is complete and every grpc-status it carries is the same valid value; conflicting statuses or bytes after the trailer return 502 attempt_unresolved and leave the operation attempted.
An unconfirmed redemption returns HTTP 502 with error.code: "attempt_unresolved" and the effective operationId. Preserve that ID; a replacement ID would start a different operation. The CLI creates the ID before sending and prints --operation-id guidance if delivery or the response is uncertain. Legacy version-one open records and unconfirmed redeem_failed records are quarantined as uncertain; they never authorize another redemption.
The dashboard at Providers > xAI Grok > Accounts lists remaining coupons and their validity windows from each account’s ticket badge. It sends a client-minted operationId. After uncertain delivery or an unknown-outcome response, the dashboard retains that attempt for the browser tab session, including across dialog close/reopen, page navigation, and reload. The hold is written before the request is sent and cleared only by a definitive response for the same operationId. Availability refreshes issue GET only and never authorize another consume POST. ocx account grok-reset-coupons is the terminal equivalent.
Claude usage resets work the same way from Providers > Anthropic > Accounts. Each
signed-in account row carries a ticket badge with its remaining resets, and the dialog
spends one after a second confirmation. A reset refills the 5-hour and weekly limits
without moving the weekly reset day. When a claim does not answer, the dialog keeps its
operationId and offers a retry with that same ID for ten minutes, which is how the
Claude Code client itself recovers; a new operation for the same grant is refused
until then. Spending is dashboard-only: the admin token alone gets 403 session_required.
For the concepts behind the model roster and encrypted worker-task behavior, see Sub-agent Surface.
Client integration rollback journal
Section titled “Client integration rollback journal”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/client-integrations/journal?client=... |
List rollback operations, optionally for one client. Each row includes the server-computed deletable flag. |
400 invalid client |
DELETE /api/client-integrations/journal?opId=... |
Retire one older rollback operation and remove its snapshot when possible. Success returns snapshotRemoved; false means cleanup was retained for maintenance retry. |
400 missing opId; 404 missing or already retired operation; 409 newest operation for that client |
Deletion appends a tombstone instead of rewriting the journal. The newest operation for each client
is protected server-side so the current undo point remains available. For Aside, protection is
per profile, and journal rows include profileId.
Aside profile controls
Section titled “Aside profile controls”Use these dedicated paths with a compatible running proxy. {profileId} is a registered
nonnegative integer account ID returned by the profile list; it is not a browser path.
| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/client-integrations/aside/profiles |
List profiles[], desired enabledCount/allEnabled, actual appliedCount, and total |
HTTP 200 may contain an empty, unsafe aggregate with an error when discovery is unavailable |
PUT /api/client-integrations/aside/profiles |
Set every registered profile’s desired state and apply it; body { "enabled": true }, with optional overwriteConflict when enabling |
400 invalid body; 409 operation busy; 500 preference-save failure; 207 per-profile refusals |
GET /api/client-integrations/aside/profiles/{profileId} |
Read one profile’s desired enabled and actual integration status |
400 invalid ID; 404 unregistered profile |
PUT /api/client-integrations/aside/profiles/{profileId} |
Change one profile with the same body as bulk PUT, leaving sibling preferences unchanged | 400 invalid body/ID; 404 unknown profile; 409 busy or refusal; 500 save/write failure |
GET /api/client-integrations/aside/profiles/journal |
List history across registered Aside profiles | Rows include profileId, snapshot availability, undoable, and deletable |
GET /api/client-integrations/aside/profiles/{profileId}/journal |
List one profile’s history, including matching legacy operations | 400 invalid ID; 404 unknown profile |
DELETE /api/client-integrations/aside/profiles/journal?opId=... |
Retire an older operation, resolving its profile from history | 400 missing ID; 404 missing operation; 409 newest operation for its profile |
DELETE /api/client-integrations/aside/profiles/{profileId}/journal?opId=... |
Retire an older operation belonging to the selected profile | Same deletion errors; an operation cannot target a different profile |
POST /api/client-integrations/aside/profiles/{profileId}/restore |
Undo an operation using { "opId": "..." }; optional confirmDrift: true permits replacing later edits |
404 missing operation/profile; 409 busy, mismatch, or required drift confirmation; 410 expired snapshot; 500 save/write failure |
POST /api/client-integrations/aside/sync |
Refresh enabled profiles through the server’s mutation owner; body {} |
400 nonempty body/profile selector; 409 busy; 207 per-profile refusals |
Previewing an integration change
Section titled “Previewing an integration change”A preview reports what a change would do without doing it. These routes write nothing: no snapshot, no ownership record, no journal row, no lock, and no maintenance or recovery.
| Route | Purpose | Notable responses |
|---|---|---|
POST /api/client-integrations/preview |
Plan apply, overwrite or disable for one client; body { "clientId": "...", "operation": "..." } |
400 invalid client or operation; 400 invalid_aside_profile_path; 409 integration_preview_unavailable |
POST /api/client-integrations/restore/preview |
Plan an undo; body { "opId": "...", "confirmDrift": false } |
404 unknown operation; 400 invalid_aside_profile_path; 409 integration_preview_unavailable |
POST /api/client-integrations/aside/profiles/{profileId}/preview |
Plan one Aside profile’s change, including restore with an opId |
400 invalid body or unscoped request; 404 unknown profile or operation, 409 integration_preview_unavailable |
A plan carries version, clientId, operation, state, foreignEdit, a changes list of
kind and path pairs, an opaque fingerprint, canApply, willChange, and an optional
refusalReason and profileId. Paths are managed schema paths or the fixed $snapshot,
$ownership and $journal markers; a position chosen at runtime appears as *. No
configuration value, file location or selected member identity is returned.
canApply: true with willChange: false means the operation succeeds and changes nothing in
the managed client document, such as applying what is already applied.
An Aside profile change still saves one thing in that case. Confirming it records the desired sync preference for that profile before any client document is touched, so disabling a profile whose managed block is already absent stores the preference and leaves the document and its history untouched.
integration_preview_unavailable means no usable model roster is currently retained. A freshly
started proxy is one way to be in that state; a roster retired because the configuration or the
provider cache moved is another. Reading GET /api/client-integrations establishes one when
discovery succeeds and the configuration can be identified, so it is the usual remedy rather than
a guarantee.
Confirming a previewed change
Section titled “Confirming a previewed change”Mutation routes accept operation and planFingerprint alongside their existing body. Send both
or neither: a request carrying one is rejected, as is one whose operation disagrees with the
change being requested. Aside bindings apply to a single profile, because one fingerprint cannot
describe several independently changing files.
The server re-plans before writing anything and returns 409 integration_preview_stale with a
freshly computed plan when the confirmation no longer describes what would happen. Decide again
against the new plan; the request is not retried automatically.
A fingerprint is an optimistic check, never authorization. Management authentication and every ownership rule still decide whether a change may happen at all.
Bulk PUT returns { ok, clientId, changed, state, message, results }; each result identifies
its profileId and reports the writer outcome. Sync returns { ok, clientId, results }, with
per-profile refresh outcomes. Both return HTTP 200 when all returned attempts succeed and
HTTP 207 with ok: false when any attempt refuses. HTTP 207 is a partial-result envelope,
including when every attempted profile refuses: inspect each result rather than treating a
2xx response as complete success. A successful no-op can have changed: false; sync does not
attempt disabled profiles. Single-profile writes and restores return HTTP 200 on success or
the corresponding error status on refusal.
Explicit changes save desired preferences before writing files. A preference-save failure
leaves profile files unchanged. A later file refusal preserves saved intent and successful
sibling writes; inspect the affected profile before retrying. Refusals may include
snapshotPath and residual: true when recovery did not finish. Restore also reconciles the
target profile’s desired state, so the next sync does not reverse Undo. Deletion returns
snapshotRemoved; false means snapshot cleanup still needs maintenance.
The legacy GET, PUT /api/client-integrations/aside aliases remain available. New clients
should use the dedicated paths above so an older proxy cannot ignore a profile selector.
See Aside profile controls for CLI commands and
the proxy upgrade, restart, and retry sequence.
Remote Workspace
Section titled “Remote Workspace”Requires Hub mode and OCX_REMOTE_WORKSPACE_ENABLED=1 on the Hub process. Disabled status is
readable; mutations refuse without initializing workspace services.
| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/remote-workspace |
Read paired computers, current capabilities, Hub runtimes, and session snapshots | Disabled status when Hub role or explicit opt-in is absent |
POST /api/remote-workspace/pairing |
Create a ten-minute one-use Executor enrollment code | Operator-paired GUI session only; 429 pairing capacity |
GET /api/remote-workspace/runtimes |
Read Codex, Claude Code, and Pi availability on the Hub | — |
GET, POST /api/remote-workspace/sessions |
List sessions or start one bound to a device, root, runtime, and access mode | POST requires an operator-paired GUI session; 409 offline/unavailable/invalid target |
POST /api/remote-workspace/sessions/{id}/prompt |
Continue the bound model session | Operator-paired GUI session only; 409 active turn, offline Executor, or resume failure |
DELETE /api/remote-workspace/sessions/{id} |
Stop the model runtime and encrypted Executor session | Operator-paired GUI session only; 404 unknown session |
DELETE /api/remote-workspace/devices/{id} |
Revoke one computer and stop its sessions | Operator-paired GUI session only; 404 unknown device |
Executor enrollment exchanges a one-use code at POST /remote-workspace/pair and then opens
/remote-workspace/agent as a bearer-authenticated outbound WebSocket. Those two machine endpoints
are not general management API authority. The bearer is device-scoped, and each work session adds a
signed E2EE handshake. Ten failed pairing codes from one kernel-observed peer return 429 with
Retry-After for the remainder of the fixed ten-minute window. Tailscale Serve clients share the
management listener’s loopback peer bucket; the identity header is not used for throttling because
a direct local process could forge it. See Remote Workspace for the
end-user flow and trust boundaries.
Session snapshots include resumable. It becomes true only after the selected coding-agent runtime
has durable history; notably, a new Claude Code session remains false until its first prompt
completes.
Combos
Section titled “Combos”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/combos |
List normalized combos and their public model ids | Catalog work can return catalog_busy |
PUT /api/combos |
Create, replace, or rename one combo | 400 invalid id, target, config, rename, or ordinary collision (a combo validation 400 also names the first invalid top-level field in field, for example targets or decisionProvider); 409 Codex-account namespace collision |
DELETE /api/combos?id=... |
Delete one combo and clear its selection/cooldown state | 400 missing id; 404 unknown combo |
See Combos for target strategies, cooldowns, aliases, and routing failures.
Codex prompt layers
Section titled “Codex prompt layers”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/codex-prompt |
Read the prompt-layer snapshot: layers, base variants, selection, and drift state | — |
GET /api/codex-prompt/text |
Probe the model-visible prompt text via codex debug prompt-input; one caller budget includes metadata, shared wait and execution; synchronous metadata is checked for expiry after it returns |
Fail-soft: an unavailable probe degrades to a status in the body, not an HTTP error; timeout preserves locally read base instructions; earlier external cancellation stays cancelled |
PUT /api/codex-prompt/toggle |
Enable or disable one toggleable layer | 400 invalid body or unknown layer; 409 stale_revision, layer_not_toggleable |
PUT /api/codex-prompt/custom |
Replace the custom-layer set | 400 invalid body, invalid_characters, body_too_large when a normalized UTF-8 layer exceeds 65,536 bytes, composed_too_large over 131,072 bytes; 409 stale_revision |
PUT /api/codex-prompt/base/select |
Select the default base prompt or one stored variant | 400 invalid body, unknown_layer for an id matching no stored variant; 409 stale_revision, developer_instructions_not_owned when the current base is external |
PUT /api/codex-prompt/base |
Create (id omitted or id: null), edit, or delete (delete: true) one base variant. A supplied id is edit-only and must reference a stored variant. body is normalized (tabs expanded, CR/CRLF folded to LF) before it is measured or stored |
400 invalid body, unknown_layer for the default id or an id that matches no stored variant, body_too_large when the normalized UTF-8 body exceeds 65,536 bytes; 409 stale_revision |
POST /api/codex-prompt/adopt |
Import developer_instructions from config.toml as a custom layer |
400 invalid body, invalid_characters, body_too_large, composed_too_large; 409 config_unreadable, nothing_to_adopt, adopt_unsupported_form, stale_revision |
POST /api/codex-prompt/repair |
Repair drift between config.toml and the owned projection |
400 invalid body; 409 config_unreadable, nothing_to_repair, repair_unsupported, stale_revision |
See Codex prompt layers for the layer model and the keys each layer writes.
Configuration, startup, sync, and updates
Section titled “Configuration, startup, sync, and updates”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/config |
Return the redacted, management-safe configuration DTO | — |
PUT /api/config |
Disabled full-config replacement guard | 405; use focused endpoints instead |
GET, PUT /api/settings |
Read runtime/startup settings or update auto-start, stream mode, app-owned memory budget, and codexAccountPickerEnabled |
400 invalid, non-object, or empty update |
GET, PUT /api/native-reasoning-retention |
Read resolved model/account reasoning-retention booleans; partial PUT preserves omitted fields, JSON null restores defaults. See native reasoning retention. |
400 unknown field, invalid type or non-object/non-null body |
GET, PUT /api/workflow-budget/settings |
Read the effective per-task workflow ceilings with defaults and allowed ranges; partial PUT preserves omitted fields, JSON null restores defaults. See workflow limits. |
400 unknown field, non-integer or out-of-range value, or non-object/non-null body |
GET /api/startup-health |
Read cached service/shim startup health | — |
POST /api/startup-action |
Install or repair the service or Codex shim | 400 invalid action; 500 action failure |
GET, POST /api/windows-tray |
Read Windows tray state or install/start/stop/uninstall it | 400 unsupported platform/action; 500 operation failure |
GET /api/diagnostics/project-config |
Read cached project configuration warnings | — |
POST /api/sync |
Sync the current model catalog into Codex | 500 failed sync |
GET /api/update/check |
Asynchronously check the latest or preview package channel and refresh the package cache on success |
400 invalid tag |
POST /api/update/run |
Asynchronously check a fresh package version, then start an update job, optionally followed by restart | 400 invalid body; 409 update_restart_unsupervised when the proxy runs inside a systemd unit other than OpenCodex’s own user service, or the worker cannot leave the service cgroup (with or without restart, since the update stops the proxy first); job-specific conflict/error status |
GET /api/update/status |
Poll an update job by id | 404 unknown job |
GET, PUT /api/sidecar-settings |
Read or update web-search and vision sidecar model/backend settings | 400 invalid shape, backend, or limit |
GET, PUT /api/shadow-call-settings |
Read or update shadow-call interception settings, including sourceModels; GET also returns the runtime-owned sourceModelOptions inventory |
400 invalid shape or value, including an empty, duplicate, whitespace-bearing, or provider-qualified source prefix, or more than 32 prefixes / 128 characters per prefix; source-list failures carry code: shadow_call_source_invalid, target conflicts code: shadow_call_target_invalid |
Logs, usage, and storage
Section titled “Logs, usage, and storage”Request logs retain servedModel when the upstream identifies the model that answered, and
wireModel when the model sent upstream differs from the client-facing model. The dashboard
shows wire → served when those identities differ; its tooltip preserves both values. Missing
upstream model evidence remains absent rather than being inferred from the requested model.
Failed request rows can include bounded upstreamErrorType, upstreamErrorCode, and
upstreamRequestId diagnostics. On the Codex upstream WebSocket path, a bare error followed by
EOF retains recognized rate-limit, overload, or client-refusal codes in the synthesized failed terminal.
Request logs record these as 429 for rate_limit_exceeded / rate_limit_error, 503 for
server_is_overloaded / overloaded_error, and 400/401/403/404 for invalid_request_error /
authentication_error / permission_error / not_found_error. A recognized type without a code
uses the same status. On the streaming path, an explicit unknown code records the 5xx status the Codex
WebSocket wrapper states as a top-level integer status or status_code (one of the two, not
both); otherwise, and for buffered JSON callers, it remains a generic 502 failure. The client
terminal keeps upstream_server_error for unknown codes either way. Bare-error status is provisional:
a genuine completed, failed, or incomplete terminal replaces it before account-health recording.
Refusal precedence is unchanged.
This diagnostic mapping adds no automatic replay or retry.
GET /api/logs accepts an optional opaque cursor from its previous response. The envelope preserves
logs, total, generatedAt and timeZone, and adds cursor and reset. Without a cursor it returns
the full filtered window. A valid unchanged prefix returns only appended rows; reset: true replaces
the client window after edits, eviction, query changes or restart. Invalid cursors return HTTP 400 with
error.code: "invalid_cursor". Authentication is unchanged. The dashboard falls back to full snapshots
for older servers. This reduces response bytes for stable windows; server projection remains bounded
by the current window size.
Successful /api/logs displayMetrics.decodeTokPerSecond values carry
timingBasis: "generation-window" | "legacy-post-visible-output" alongside kind, value
and estimated: true; attempt values identify their own window. Unavailable results keep their
existing reason and no timing basis. The field is derived only at read time: stored JSONL,
end-to-end tokPerSecond, request-history DTOs and aggregate throughput are unchanged.
Older DTOs can omit the field; clients must treat the timing basis as unknown in that case.
The dashboard names these methods Generation window and After visible output, with
Timing unknown for a missing or unfamiliar basis. Request details show Output rate during
generation (est.), Output rate after first visible output (est.), or Output rate (est.;
timing method unknown) and a visible explanation of the timing method when a rate is available.
| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/logs |
Query filtered in-memory request logs | — |
GET, PUT /api/debug |
Read debug flags; set, clear, or reset capture categories | 400 invalid or empty update |
GET /api/debug/logs |
Read bounded provider/debug log entries | — |
GET /api/debug/usage-logs |
Read bounded usage-debug entries | — |
GET /api/debug/injection-logs |
Read bounded guidance-injection debug entries | — |
GET /api/claude/inbound-debug |
Read Claude inbound debug state and entries | — |
GET /api/usage |
Scan the usage ledger into compact aggregates of readable rows, then incrementally fold verified appends; summarize by preset or inclusive custom window and client surface, with a Codex accounts breakdown keyed by stable non-PII log labels |
400 invalid custom bounds; 500 { "error": "read_failed" } if storage cannot be read |
GET /api/usage?jev=1 |
Project persisted JEV decisions and their physical target sends. Optional comboId selects one Combo; range accepts 7d, 30d, or all (default 30d). |
400 invalid comboId; 500 { "error": "read_failed" } if the ledger cannot be read |
GET /api/usage/timeline |
Bucketed usage by model/account; accepts hours, bucketMinutes, metric, aggregation, grouping, comma-separated models and repeated hiddenProvider filters |
400 invalid query or limits |
GET /api/metrics |
Return process-local Prometheus text metrics for requests and cached Kiro quota. Kiro accounts use bounded opaque digest labels; raw account and credential identifiers are never exported. | 404 when metricsExport.enabled was not true at startup; ordinary management authentication is required and data-plane credentials grant no access |
GET /api/storage |
Scan Codex storage usage by bucket | Returns an error: "scan_failed" payload on scan failure |
POST /api/storage/cleanup/preview |
Preview archived-session cleanup and return a binding digest | 400 invalid_json or invalid_percent |
POST /api/storage/cleanup |
Quarantine or permanently remove the previewed archived set | 400 invalid input; 409 stale/busy/referenced state; 500 filesystem/database failure |
GET /api/storage/trash |
List quarantined cleanup entries | 500 trash_list_failed |
POST /api/storage/trash/restore |
Restore one quarantined entry | 400 invalid id; 404 missing trash; 409 busy/destination conflict; 500 restore failure |
GET /api/storage/trash/restore/test-stream |
Test-only restore stream hook | 404 not_available when test hooks are off |
GET, PUT /api/storage/cleanup-policy |
Read or update scheduled cleanup policy and job state | 400 invalid policy |
POST /api/storage/cleanup-policy/run |
Start a manual cleanup-policy run | 409 already_running; 500 cleanup_failed |
GET /api/storage/cleanup-policy/test-stream |
Test-only policy stream hook | 404 not_found when unavailable |
Timeline model identifiers split at the first slash, so nested native ids such as
github-models/openai/gpt-4.1 are supported. Provider exclusions apply before excess series are
folded into other; the additive appliedFilters object echoes the normalized model/provider
filters. The final bucket includes the current partial interval. Companion settings are not
applied implicitly to general API calls.
GET /api/metrics returns Content-Type: text/plain;version=0.0.4. Counters and histograms
reset when the process restarts; opencodex_metrics_process_start_time_seconds identifies that
boundary. Histogram buckets are cumulative and end with le="+Inf", equal to the family count.
| Metric family | Labels | Meaning |
|---|---|---|
opencodex_logical_requests_total |
protocol, result |
One observation per finalized logical request. |
opencodex_physical_sends_total |
protocol |
Actual upstream sends summed from finalized attempts. |
opencodex_recoveries_total |
protocol, recovery |
Distinct recovery kinds observed per attempt, projected to a closed class. |
opencodex_request_failures_total |
protocol, cause |
Finalized requests that did not deliver an answer, by the cause the recorder derived. Counter only; no histogram carries a cause. |
opencodex_request_duration_seconds |
protocol, result |
Fixed-bucket duration histogram for finalized requests. |
opencodex_ttft_seconds |
protocol, result |
Fixed-bucket TTFT histogram for requests with observed first output. |
opencodex_ttft_missing_total |
protocol, result |
Complementary count for requests without observed TTFT. |
opencodex_metrics_process_start_time_seconds |
none | Process-local reset boundary. |
opencodex_kiro_quota_{used_credits,limit_credits,used_percent,seconds_to_reset} |
account (opaque o plus six hex digits; at most 32 distinct live accounts) |
Identity-matched cached Kiro plan readings. Missing, stale, future-dated, or reset-passed readings emit no sample; scraping never probes upstream. |
The recovery label takes one of a fixed set of classes: transient, connection, credential,
rate_limit, quota, policy, ciphertext, payload, empty_completion, effort_downgrade,
fast_downgrade and other. The recovery set is closed; Kiro quota gauges use only the
opaque account digest label described above, never a raw account identifier. fast_downgrade records an Anthropic Fast refusal repaired at standard
speed; it is distinct from the reasoning-effort effort_downgrade class. rate_limit, quota,
policy and ciphertext are separate because the operator response
differs: wait out the limit, move to another account, change the prompt, or drop stale encrypted
state. A rejected opaque reasoning blob counts as ciphertext rather than payload.
If a scanned row exceeds the existing parser size limit, GET /api/usage and GET /api/keys
keep the readable-row aggregates and add usageIncomplete: true with
usageIncompleteReason: "oversized_rows" at response level. This diagnostic survives cached
responses and incremental appends, including empty or unmatched results; a rebuild recalculates it.
No provider, model, or API-key identifier is shortened to make a row fit. An absent flag is not proof
that every ledger record was valid. This is separate from historyTruncated, entriesTruncated,
and token measurement coverage.
The JEV projection reports decision counts and gates, applied versus fail-open picks, decision
latency/confidence, TypeSafe-reported decision tokens, physical target send counts, available model
token/cache totals, reasoning-effort picks, and requests that sent to a target other than JEV’s
initial choice. attempts are summed from each persisted attempt’s sendCount; a zero-send row is
not a fallback. One reported usage object is counted once even when its attempt retried, so the
measured-attempt count can be lower than the physical-send count. Model cardinality is bounded; an
explicit overflow row aggregates additional identities without dropping summary totals.
JEV stats use the same append-only usage.jsonl ledger and cooperative scanner as ordinary usage.
Cold reads scan the current ledger, unchanged polls perform no file read, and verified growth folds
only the appended suffix. Concurrent identical requests share one scan. The persisted JEV record is
closed and content-free: Combo id, selected provider/model/effort, decision gate, latency, optional
confidence/probability, and optional numeric decision usage. It never stores prompts, headers,
credentials, tool arguments, or raw TypeSafe responses. Oversized rows retain the same positive
usageIncomplete diagnostic as the ordinary usage endpoint.
New xAI attempts in usage.jsonl include a request-time credentialSource: grok-oauth
for the resolved Grok CLI OAuth transport, or xai-api-key for the public xAI API key
transport. This fixed label contains no credential or account identifier. It belongs to
each item in attempts, so a combo’s aggregate token total must not be attributed to its
final provider. Custom destinations and historic rows omit the field; consumers must not
infer subscription usage from the current configuration, model name, or inbound API key.
The log reports usage, not subscription invoice amounts.
API-key attempts also record accountLogLabel as k followed by 32 lowercase hex digits.
The label is the first 128 bits of SHA-256 over
JSON.stringify(["ocx-key-account-v1", providerName, entryId ?? null, reference]).
The reference is the configured key value captured for the physical request, before environment
or keychain resolution. Raw keys, references, and pool IDs are not written to the label field.
A consumer can derive the same label from its local configuration without resolving secrets.
Changing a literal key or reference changes the label; replacing the secret behind an unchanged
reference keeps the same logical account. Older unlabeled records cannot be attributed reliably.
Key selection is recorded after queued requests have been rebuilt for the current selection.
When a retry changes keys, attempts retains a separate record for the preceding key, including
reported usage from failed responses. Missing usage remains unreported. Routed adapter terminals
are observed before image/search loops or continuation guards combine their usage. Consumers
sum the flat attempts by provider/account and do not add the parent combo total again. These
records identify usage; provider quota percentages remain separate upstream observations.
GET /api/usage reads ~/.opencodex/usage.jsonl from the beginning through the current ledger
snapshot on a cold start. It processes fixed 1 MiB chunks and retains compact aggregate state rather
than every normalized request row. Later refreshes validate the previous line boundary and fold only
newly appended complete rows. Concurrent callers share the same refresh. Range and surface predicates
are applied to the readable-row aggregate, so the former read-byte window and parsed-row cap cannot omit
an earlier file prefix from 7-day, 30-day, or all-history totals. managementUsageMaxReadBytes remains
accepted for compatibility with bounded legacy readers, but changing it no longer expands or reduces
the history summarized by this endpoint.
Pass both since and until to select an inclusive custom interval. Each accepts integer Unix
epoch milliseconds, or a full ISO datetime with an explicit timezone. Invalid dates, negative
or out-of-range values, reversed bounds, and a single bound are rejected. Custom bounds override
range; the response keeps the preset range field for compatibility and adds customWindow: true,
the exact since, and until. generatedAt remains the time the report was produced.
Custom windows filter individual ledger entries before daily aggregation, including partial first
and last days. They preserve surface, provider, model, and apiKeyId filtering and never reuse
or overwrite unfiltered preset summaries. The daily chart remains capped at 366 local calendar days;
totals cover the full requested interval. Snapshot-window fields describe the scanned ledger before
the time filter, so they can extend beyond the requested bounds.
The Usage page accepts local date/time inputs. Its selected ending minute includes the entire
minute through :59.999. Choosing a preset or clearing the custom window restores preset behavior.
This adds exact range selection and existing cost estimates; it does not add hourly chart buckets
or offline reporting.
The runtime ledger is append-only. Replacing or truncating it, or changing local pricing/time-zone inputs, triggers a complete rebuild. If you manually edit an older row in place while the proxy is running, restart the proxy (or replace the file) before relying on the new total; incremental refreshes verify the append boundary, not every previously aggregated byte.
The response still includes historyTruncated, truncatedPrefixBytes, entriesTruncated, and
entriesDropped so older clients can consume the same wire shape. A successful whole-ledger scan
reports false, 0, false, and 0, respectively. These are legacy compatibility fields, not a
signal that the endpoint read only a configured-size tail.
For GET /api/usage?range=30d&surface=codex, accounts contains one row per observed Codex
pool label. Each row reports accountLogLabel, token totals, usageCoverageRatio, and an optional
estimatedCostUsd based on the currently configured display pricing. Active user modelCosts
overlays take priority over bundled verified catalog and price fallbacks, and historical usage is
re-estimated from the pricing active when the summary is read. This is an API-equivalent estimate,
not a subscription charge. New main-pool requests use the reserved main label; legacy bare
openai rows remain in an ambiguous bucket instead of being reassigned from current configuration.
Manual model prices can also be edited from Models → Price. A manual-pricing badge survives
catalog reloads. Prices are stored in providers.<name>.modelCosts and survive catalog sync.
Explicit all-zero user rates mean a known-zero estimate; Reset to automatic removes the
override and restores the usual catalog fallback. These remain display estimates, not bills.
GET /api/providers/{provider}/model-costs returns { provider, modelCosts }, with sanitized
four-rate entries and their optional promptLengthPricing policy, keyed by exact upstream model ID.
PUT on the same route accepts { modelId, cost }, where cost contains
{ input, output, cacheRead, cacheWrite } and optionally promptLengthPricing, or is null to reset.
All four rates must be finite numbers from 0 through 1,000,000, in USD per 1M tokens.
Unknown fields and malformed rates are rejected. A write preserves other models’ overrides
and returns { ok: true, provider, modelId, cost }; reset returns cost: null.
The Model price dialog offers Automatic, Flat rate, and Custom threshold
prompt-length pricing. Automatic (also the default when the policy is absent) retains catalog
context rules with the saved base rates. Flat disables context adjustments. Custom selects four
absolute alternative rates using one positive safe-integer thresholdTokens and comparison
("gt" for >, "gte" for ≥). All prompt tokens count, including cache reads and writes;
output does not. The selected rates apply to the whole request, including output, not just the
excess tokens. Custom replaces the catalog context rule both below and above its threshold.
Custom rates are standard-speed rates: an applicable Fast/Priority adjustment applies once after
rate selection and follows the catalog’s Priority relation for that model. Only a model whose
catalog relation is lower-bound reports priorityLowerBound: true; a model with no catalog rule
applies any Priority multiplier once.
{ "mode": "automatic" } is accepted on write and stored as an absent policy, so GET and PUT
receipts never contain it.
Reset to automatic removes the entire override, including base rates and the policy.
See the configuration example.
ocx models price ollama/custom-model --jsonocx models set-price ollama/custom-model --input 0.50 --output 1.50ocx models set-price ollama/custom-model --input 0 --output 0ocx models set-price ollama/custom-model --autoOmitted CLI cache-read/cache-write rates default to zero. Use --cache-read and --cache-write
to set them explicitly. A provider name remains an exact configuration identity; account display
labels are not editable provider names.
Rows in models, providers, and days[].models also carry cacheHitRate: the share of input
tokens served from the provider’s prompt cache, clamped to [0, 1]. It is null — never 0 —
when the provider reported no cache telemetry or the row has no input tokens, because “no cache
data” and “a genuine 0% hit rate” are different facts and a chart that renders them alike is
misleading.
Cleanup recovery manifests are published atomically, preserving the previous complete record if a replacement fails before publication. This does not reverse a permanent purge: restore can still fail when a recorded session has no surviving rollout file.
Models and catalog
Section titled “Models and catalog”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/catalog |
Return the installed Codex catalog document. Remote clients should prefer the data-plane GET /v1/catalog, which still requires an ordinary data-plane credential but not an admin token. |
404 catalog not found |
GET /api/models |
Return the dashboard/CLI model rows | catalog_busy when gathering is saturated |
GET /api/client-config?client=... |
Build a read-only client config for any supported file integration | 400 unsupported client; 503 catalog unavailable |
PUT /api/disabled-models |
Replace the shared disabled-model list | 400 invalid JSON |
PUT /api/model-visibility |
Atomically change provider- or model-level visibility | 400 invalid provider, scope, target, or body; 409 initial_model_selection_pending (refresh the model list and retry) |
GET, POST /api/custom-models |
List custom models or add one | 400 invalid fields; 404 provider missing; 409 duplicate model |
PUT, DELETE /api/custom-models/{id} |
Edit or delete one custom model | 400 invalid id/fields; 404 not found; 409 duplicate model |
GET, PUT /api/selected-models |
Read provider allowlists and availability, or replace one allowlist | 400 missing provider/body; 404 unknown provider; PUT 409 initial_model_selection_pending |
GET, PUT /api/model-presets |
Read preset summaries or choose preset/all/custom mode | 400 invalid mode or unsupported preset; 404 unknown provider; PUT 409 initial_model_selection_pending |
PUT /api/model-settings |
Edit one routed model’s capability axes in place | 400 missing provider/modelId, unknown or native/combo provider, malformed field, unknown field, non-exact model id, or an invalid default effort for the resulting ladder; 500 when the config could not be saved (live config left unchanged) |
PUT /api/model-settings takes { provider, modelId } plus any of contextWindow,
inputModalities, reasoningEfforts, and defaultReasoningEffort. It writes the
provider-level per-model maps the runtime already reads (modelContextWindows,
modelCapabilities.<modelId>.inputModalities, modelReasoningEfforts,
modelDefaultReasoningEfforts), so an edited row keeps its discovery provenance instead of
being replaced by a custom model. inputModalities accepts text, image, and audio;
reasoningEfforts must be a subset of the efforts the runtime knows. Unknown request fields
are rejected by name. modelId must be exact, without surrounding whitespace, control
characters, excessive length, or a reserved object-property name. contextWindow must be
null or a positive safe integer.
Every field is optional, and null clears the declaration rather than writing a default,
which hands the fact back to the registry, the catalog, and the provider. An empty
inputModalities array also clears, while an empty reasoningEfforts array is stored as an
explicit “this model has no reasoning rungs” override — it does not clear.
Clearing inputModalities also removes the exact legacy modelInputModalities entry for that
model, so a restore cannot leave an older declaration in force; family keys shared with other
models stay. An emptied per-model map is removed instead of left as {}. A request that changes nothing
answers changed: false with the stored state. The receipt also reports saved (whether this
request published config), hasOverrides (whether any of the four axes remains stored), and
catalogRefresh (committed, skipped, or failed). These fields distinguish a no-op with
stored declarations from a model with nothing to restore. A changed request persists atomically;
an unpublished save failure leaves the live config unchanged. After publication the server
drops that provider’s cached /models result and reports the catalog outcome. A published write
followed by a bookkeeping error still returns saved: true with a skipped refresh. The gather
bakes resolved hints into the rows it caches, and a cache hit may only lower a configured window,
so a raised or cleared override would otherwise read back the previous answer for the whole TTL.
Only routed providers are addressable: openai is the
native passthrough lane and combo is a synthetic row, so neither is a provider whose per-model
maps these facts come from. Display name is not part of this route; use
PUT /api/providers/{provider}/model-display-names.
GET /api/models reports the exact stored contextWindowDeclared separately from the
effective contextWindow. The Models editor starts from the declaration and shows the effective
window as an inherited hint when no declaration exists. If a save outcome is unknown, or the
save succeeded but the model list did not reload, the dialog disables further writes and offers
a read-only Reload. After a successful reload, reopen the row to inspect the stored values.
A catalogRefresh that failed, or was skipped with retryable: true, leaves the save in place and
shows a warning to run Sync; a non-retryable skip means no managed Codex catalog and is a clean save.
A manual model replaces the Models dashboard row with the same provider and model ID.
For OpenAI, the manual row keeps openai/<model> and supports the same visibility controls
as other routed models; removing it restores the bare native dashboard row. Explicit
account-qualified native rows stay separate. This does not rename bare native routes or
change account entitlements. Non-native OpenAI visibility targets must match a configured
manual model.
Valid PUT requests to /api/selected-models and /api/model-presets return HTTP 409 with code initial_model_selection_pending until a reliable initial model list is available. Refresh model discovery (for example, GET /api/models) and retry after it succeeds.
Successful visibility/selection writes to /api/disabled-models, /api/model-visibility,
/api/selected-models, and /api/model-presets report follow-up outcomes in catalogRefresh
and clientIntegrations when that refresh path runs. HTTP 200 and ok: true confirm the
selection save; they do not guarantee every client catalog updated. Inspect
clientIntegrations[] for ok: false, client, optional Aside profileId, and the refusal
reason; recovery details may also include refusalReason, snapshotPath, and residual.
The Models page keeps the saved selection and shows a separate client-refresh warning.
Inspect Integrations and resolve the reported issue before retrying ocx sync. Missing
outcome fields from an older server do not establish successful recovery.
OAuth accounts, provider keys, and data-plane keys
Section titled “OAuth accounts, provider keys, and data-plane keys”OAuth account rows always include plan: a validated upstream subscription label or null
when unknown. Anthropic profile tiers report labels such as max_20x, max_5x, or pro;
Antigravity prefers the paid tier (for example g1-pro-tier) over the current tier.
Labels are read on login/import or refresh, never inferred from quota percentages.
| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/oauth/providers |
List providers with public OAuth login flows | — |
GET /api/key-providers |
List providers configured through API-key login | — |
POST /api/oauth/login |
Start an OAuth login or account-add flow. mode: "code" starts a code-display login (Anthropic only) and returns mode, flowId, expiresAt |
400 unknown/invalid provider, invalid mode, code_mode_unsupported; oauth_mutation_busy |
POST /api/oauth/login/code |
Submit a manual callback URL or authorization code. For a code-display flow (with flowId) returns the token-exchange outcome {ok, account} / {ok:false, error} |
400 invalid provider/code, state_mismatch, invalid_or_expired_code, provider_unreachable, malformed_input; 409 no_pending_login; oauth_mutation_busy |
POST /api/oauth/login/cancel |
Cancel a public in-progress OAuth flow or dismiss its settled failure; an optional flowId names one code-display flow (a flow of another provider is refused) |
400 unknown provider |
GET /api/oauth/status |
Poll one provider’s OAuth flow | 400 unknown provider |
POST /api/oauth/logout |
Remove the selected provider credential | 400 unknown provider; oauth_mutation_busy |
GET /api/oauth/accounts |
List masked accounts; Anthropic and generic OAuth account rows include their paused state. Kiro rows include autoSelectable and a closed skipReason when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. |
400 invalid provider |
DELETE /api/oauth/accounts |
Remove one account | 400 invalid provider/id; 404 account missing; oauth_mutation_busy |
PUT /api/oauth/accounts/active |
Select the active OAuth account | 400 invalid provider/account; 404 account missing; 409 account paused; oauth_mutation_busy |
PUT /api/oauth/accounts/pause |
Pause or resume one Anthropic or generic OAuth account. Body { provider, accountId, paused }; pausing the active account selects the next usable account when available. Pause is durable and independent of pool enablement; resume preserves health and credentials. |
400 unsupported provider or invalid body; 404 account missing; oauth_mutation_busy |
GET, PUT, PATCH /api/pool/settings |
Read or update pool policy for any kind (codex, anthropic, generic); answers with the same keys for all three and declares in supported which the kind honours |
400 unknown provider, a field the kind does not support, or an invalid value |
GET, PUT, PATCH /api/oauth/accounts/pool |
Legacy per-pool policy for Anthropic and generic OAuth providers; superseded by /api/pool/settings and kept for existing clients |
400 codex or api-key provider, or invalid policy |
POST /api/oauth/accounts/clear-cooldown |
Clear one OAuth account’s runtime cooldown | 400 invalid provider/account |
PUT /api/oauth/accounts/alias |
Set or clear an OAuth account alias | 400 invalid provider/account/alias |
GET, POST, DELETE /api/providers/keys |
List masked provider keys, add/activate one, or remove one | 400 invalid input; 404 provider/key missing |
PUT /api/providers/keys/active |
Select a provider’s active key | 400 invalid input; 404 provider/key missing |
PUT /api/providers/keys/alias |
Set or clear a provider-key alias | 400 invalid input; 404 provider/key missing |
GET, POST, PATCH, DELETE /api/keys |
List, create, edit, or delete data-plane admission keys | 400 invalid body/id; 404 key missing |
POST /api/oauth/login/cancel can also dismiss the selected settled failed login attempt. It leaves stored credentials and successful login results intact; status then reports the remaining flow or idle. Cancelling an in-progress attempt still records its cancelled outcome.
For Anthropic, routes is an ordered array of {name, match, accounts, fallback?} rules on both settings endpoints. GET and write echoes include routes (null when absent); unified DTOs expose routes: null for other kinds. A supplied routes on another kind is rejected. Omission preserves rules, [] matches nothing, and null clears them. The accounts values are stored IDs; removed IDs remain valid in a rule so re-adding an account can restore routing. Management responses retain valid configured names; request logs identify a match only as route:#<n> (1-based list position). If a hand-edited stored rule is invalid, both settings GETs return routes: null and a routesError diagnostic instead of presenting that rule as valid; the stored value remains available for correction. Valid or absent rules omit routesError.
Credential list responses are deliberately masked. OAuth access tokens and complete provider API keys are not returned to dashboard clients.
Protocol paths
Section titled “Protocol paths”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/protocols |
Return the protocol contract version, which APIs are served, the protocol settings, the current policy revision, and the request features a preview understands | — |
GET /api/protocols?provider=<name> |
The same body plus provider: { name, adapter, adapterSource, authMode, upstream, modelOverrides: [{ model, adapter, source }] } — the wire the provider receives and who decided it (hard-pin, operator, registry, provider-default), with up to 64 models on a different wire (modelOverridesTruncated: true past that). No credential or base URL |
400 empty, repeated, over 200 characters, or control characters; 404 no provider with that name |
POST /api/protocols/plan |
Preview the path a request would take: { "model": "...", "inbound": "responses" | "chat" | "messages", "features": [...] } returns each route candidate’s request and response path, delivery mode, fidelity, feature effects, and reasons |
400 invalid JSON, unknown field, model over 200 characters, unknown inbound, or more than 24 / unknown features |
PATCH /api/protocols/settings |
Change protocol settings: { "messagesEnabled"?: boolean, "unrepresentable"?: "legacy" | "reject", "rollout"?: { ...boolean switches } }. Returns the same body as GET /api/protocols. Closing Messages also sets claudeCode.enabled to false in the same save; opening it writes only apiSurfaces.messages.enabled |
400 invalid JSON, empty body, unknown field, wrong type, or rollout.managedMessagesNativeOAuth without rollout.managedMessagesNative; 409 configuration busy; 500 save failed (nothing changes) |
A preview is computed from configuration alone. It sends nothing to any provider, costs nothing,
does not advance combo rotation, and is not logged. The API page in the dashboard shows the same
preview under Request path preview. A delivery mode of native describes how the request
travels; it is not a compatibility verification.
The ?provider= form backs the Upstream wire section of a provider’s settings. It reports
the format opencodex sends to that provider; it does not open or close any client API. To change
the provider-wide adapter, save the provider’s settings as usual (PATCH /api/providers).
PATCH /api/protocols/settings is the only writer here and backs the Messages toggle on the
API page. The CLI drives these routes with ocx api protocols, ocx api explain and
ocx api policy. The rollout switches are staged and default off
(Protocol paths); see
API surfaces for how the Messages
setting interacts with claudeCode.enabled across upgrades and downgrades.
Home identity and the served-by header
Section titled “Home identity and the served-by header”GET /healthz includes instanceId, this install’s opaque Home id: home_ followed by 32 hex
characters, generated at random once per install. It is the same value as producer.homeId in
GET /api/quota-pool, it stays the same across restarts, and it is never a hostname or derived from
one. Every response to GET /v1/models carries the same value in the x-opencodex-served-by
header, so a client can tell which Home answered without reading logs. A Remote Link Child relays the
Home’s header unchanged, and a Hub-mode client’s Codex reads the Hub’s response directly, so on any
device the header names the Home that served the model list. GET /readyz lists
served-by-header in features when the build sends it.
Right after a fresh install starts, before its identity file exists, or when that file is
unreadable, both the instanceId field and the header are left out; an empty value is never sent.
Deleting quota-pool-identity.json from the OpenCodex home rotates the id together with every
quota-pool account reference when the proxy next starts; a running proxy keeps reporting the id it
already read.
Providers
Section titled “Providers”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/providers |
List redacted provider configuration and discovery state | — |
POST /api/providers |
Add or replace one validated provider and optionally make it default | 400 invalid/dangerous destination or config; 409 namespace collision |
PATCH /api/providers?name=... |
Update allowed provider fields (including a merged headers block), enabled/default state, or OpenAI account mode |
400 invalid field or transition; 404 unknown provider |
DELETE /api/providers?name=... |
Delete a provider, reassigning the default when possible | 404 unknown provider; 409 last_provider; 409 provider_has_dependent_combos |
POST /api/providers/test?name=... |
Perform a bounded live provider connectivity/model-discovery probe | 404 unknown provider; failures are normally returned as ok: false evidence |
GET /api/provider-quotas |
Read provider quota reports; refresh=1 forces refresh |
— |
GET /api/quota-pool |
Read the passive ocx.quota-pool/1 account quota pool (per-account and pooled windows, opaque account references) from cached evidence only; never probes or refreshes, and ignores refresh. Pooled windows are a plan-weighted estimate over the routable accounts’ readings from the last 30 minutes, plus an availability window while no account can run. Also readable from a Remote Link Child or Hub client at /api/machine/hub-relay/api/quota-pool |
— |
GET /api/quota-resets |
List recently detected quota-window resets and whether detection is enabled; limit=<n> caps the count |
400 invalid limit |
GET /api/fallback |
Read the ocx.fallback/1 fallback state: effective global switches, provider and route overrides, devices with local fallback permission, and the last 20 fallbacks taken or refused. Also readable from a Remote Link Child at /api/machine/hub-relay/api/fallback (link aliases omitted) |
— |
PUT /api/fallback |
Set or clear switches: one write { scope: "global"|"provider"|"route"|"device", key?, setting: "model"|"provider"|"localFallback", value: true|false|null }, or { writes: [ … ] } with up to 8 applied in one config mutation; localFallback is per device; a device key may be a link alias. Returns the new state |
400 invalid body; 404 unknown_device (nothing written); 409 config_unavailable |
GET, PUT /api/provider-context-caps |
Read or update global, all-provider, or one-provider context caps | 400 invalid request; 404 unknown provider |
GET /api/provider-presets |
Return GUI provider presets derived from the runtime registry | — |
Provider reads include the optional boolean inferContextWindow when configured. Provider PATCH
accepts only a boolean or null to clear this setting; omission preserves it. A same-name POST
also preserves an omitted setting, including the latest stored false; an explicit submitted
boolean wins. See Context windows on custom providers
for its default-off, custom-provider-only behavior.
When Antigravity’s quota-summary endpoint has no usable response, quota reports may also include per-model Cloud Code Assist usage windows when the model-discovery response provides them. These measurements are diagnostic; an absent model window does not establish that the model is unavailable.
With quota=1, an Anthropic or Antigravity account row whose probe returned no quota carries
quotaUnavailable: true and, when the cause is known, a closed quotaFailure code:
account_unavailable (sign the account in again), access_denied, rate_limited, timeout,
transport_error, upstream_error or response_unusable (retry later), among others. The code
never includes upstream text, and a quota failure never changes the row’s health.
The provider context-cap response includes caps (active limits) and values (last selected
values, retained while disabled). Enabling a provider without value restores its selection,
or uses the global contextCapValue on first enable. This also applies to OpenAI: the switch
does not select a special 922k mode. An active cap bounds every native window; models with a
supported long-context window may expand only up to their own supported ceiling.
Updating the global value with { "value": 600000, "setAll": true } changes only enabled
provider caps; disabled providers keep their remembered selections when later enabled.
In contrast, { "setAll": true } without value enables every configured provider at the
current global value, replacing their remembered selections. Turning a cap off does not
activate its remembered value or erase the selection.
provider_has_dependent_combos is a safety barrier: remove or edit the dependent combos before
deleting their provider.
Sidebar and consent-bound actions
Section titled “Sidebar and consent-bound actions”| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/github/star |
Read repository star status through the user’s gh session |
Status-specific fixed result codes |
POST /api/github/star |
Star the repository only from an authenticated human action | 403 agent_consent_required for agent-driven callers without dashboard-session evidence |
GET /api/update/badge |
Read cached package badge without a registry lookup; missing, wrong-channel or 40-hour-old cache returns unknown: true. surface=desktop&session=<id> reads only that desktop app session. |
400 invalid surface; missing or expired desktop session returns unknown: true |
POST /api/update/desktop-snapshot |
Desktop shell publishes its Tauri-updater display state through the bound proxy client | 403 if an Origin header is present or the principal is not the raw admin-token; 400 invalid fields; 413 over 1 KiB |
The desktop snapshot is temporary display state, not an install request. The proxy stores at most 32 sessions in memory and expires one 180 seconds after its last heartbeat. A normal browser without surface=desktop continues to read the package badge.
The proxy checks an eligible package install after startup when its cache is missing or older than 20 hours, and checks freshness hourly. Set OCX_DISABLE_UPDATE_CHECK=1 to disable automatic checks. Explicit check and run requests still work.
System lifecycle
Section titled “System lifecycle”POST /api/system/restart keeps the 60-second drain by default, including requests
with no body or {}. An authenticated caller can explicitly send a JSON body such
as {"drainGraceMs":2000} to choose a shorter active-request/scoped-drain grace.
The value must be an integer from 1 to 60000 milliseconds. Invalid bodies or values
return 400 before starting a drain; the standard management-body limit still applies.
The target-bound local restart capability retains the default grace; setting this
body option requires a management session or admin token (otherwise 403).
The response’s drainTimeoutMs reports the accepted grace. Repeated calls retain
the first accepted restart’s grace and do not change its deadline.
Grace is measured from acceptance, including the response-flush delay. The cleanup watchdog remains 60 seconds and replacement readiness retains its separate 70-second budget. A short grace can interrupt a turn that already executed upstream. Check its outcome before resubmitting; OpenCodex adds no automatic replay of ambiguous work, and a healthy replacement does not prove resending is safe. The dashboard, CLI, tray and automatic restarts continue using the 60-second default.
During a restart drain, new data-plane requests receive HTTP 503 with JSON
error.type: "server_error", error.code: "server_restarting", and the message
“OpenCodex is restarting; retry this request.” Responses retain Retry-After: 5
and the receiving listener’s CORS policy. This code lets every Codex version retry
the 503 without reporting model capacity; provider overload errors retain their separate mapping.
| Method and path | Purpose | Notable errors |
|---|---|---|
GET /api/system/memory |
Return scalar process, heap, stream, response-state, watchdog, and active-turn metrics. Response-state diagnostics include spill-write status, consecutive failures, fixed privacy-safe failure class, and last failure/success timestamps. spillLastWriteFailureOrigin is retry_returned_timeout, timeout_memo_refusal, or null; cumulative spillAclRetryReturnedTimeouts and spillAclTimeoutMemoRefusals count terminal failed publications; cumulative spillCapacityRefusals and spillHeadroomEvictions count continuations refused at the spill ceiling and continuations evicted to make room for a newer one. See Windows spill diagnostics for process-local semantics. Raw errors and paths are never returned. |
— |
POST /api/system/restart |
Begin a drain-aware process restart without removing client injection; optional JSON drainGraceMs explicitly shortens the default 60s grace |
Returns 202; repeated calls report the existing grace; 400 invalid body or grace |
POST /api/stop |
Stop the service, restore native Codex, remove managed Grok injection, and drain the proxy | 409 service ownership conflict; 409 respawnable_service when a Windows Task Scheduler wrapper could respawn the proxy and the caller is not ocx stop (nothing is changed); 409 self_unload_service when this proxy is running as the installed launchd/systemd service, because stopping the manager from inside it would end the process before native Codex is restored — run ocx stop instead (nothing is changed); 409 when the installed manager refuses to stop; 409 service_state_unknown when the Task Scheduler state cannot be read (nothing is changed; repair the query and retry) |
GET /api/system/codex-app-server |
Report whether running Codex app-servers predate the current model catalog | — |
POST /api/system/codex-restart |
Refresh the catalog, then restart Codex app-servers. A running Codex desktop app is given a restart attempt so the model picker can reload; the desktopApp response fields report a refusal, surviving targets, or a failed relaunch. A shell newer than the catalog is still reported as desktopApp.pickerState: "unknown", because its start time cannot prove which model_catalog_json binding it loaded; an earlier sync may have rebound an already-existing catalog. A shell no newer than the catalog, or one whose binding this request changed, is stale. A closed desktop returns desktopApp.reason: "no_targets"; its pickerState may be not_running, or stale when this request changed the catalog binding. When the proxy itself is running inside the Codex app, the desktop restart is refused rather than handed off. |
Returns 200 with code: partially_stopped when a target survives |
Codex authentication delegation
Section titled “Codex authentication delegation”GET /api/settings reports the effective codexAccountPickerEnabled boolean. A PUT containing
that strict boolean initializes privacy-safe account selectors when enabling an empty map, preserves
existing selector labels when disabling or re-enabling, persists first, and then requests one bounded
catalog convergence only when effective picker visibility changed. The successful response includes
catalogRefreshPending: false means the catalog commit completed (or no refresh was needed), while
true means the setting was saved but POST /api/sync should be used to retry the catalog refresh.
Persistence or selector-allocation failure rolls the in-memory settings back and does not run
convergence.
The root management dispatcher delegates every /api/codex-auth/* request to the Codex account
manager. Its routes are:
| Method and path | Purpose | Notable errors |
|---|---|---|
GET, POST, DELETE /api/codex-auth/accounts |
List/refresh or delete Codex accounts. POST is retained as a disabled compatibility endpoint; successful DELETE responses include catalogRefreshPending. |
POST always returns 403 manual_import_disabled; 400 invalid DELETE input |
PUT /api/codex-auth/accounts/alias |
Set or clear an account alias | 400 invalid account/alias |
PUT /api/codex-auth/accounts/pause |
Manually pause or resume an account and its existing matching main/pool entries; returns affectedAccountIds |
400 invalid account/state; 404 missing account; 503 main identity busy or unreadable |
PUT /api/codex-auth/accounts/pause-exhausted |
Pause accounts whose quota is exhausted | Mutation-lock failures become 503 |
PUT /api/codex-auth/accounts/credits |
Allow or stop spending ChatGPT credits after the usage limit. Body { id, creditsAfterLimit } for one account, including __main__: true adds the id to creditCodexAccountIds, false removes it. Body { all } for the global switch: true lists __main__ and every pool account, false clears the list. Applies to the next selection. |
400 invalid id or non-boolean value; 404 missing account |
PUT /api/settings with codexQuotaAutoRefresh: { id, window, enabled } |
Enable or disable 5-hour or weekly automatic window activation for one account | 400 invalid id/window/state; 404 missing account; 409 unavailable window |
POST /api/codex-auth/accounts/clear-cooldown |
Clear runtime cooldown for one account or all accounts | 400 invalid id |
GET, PUT /api/codex-auth/active |
Read or select the active account | 400 invalid or missing account; 409 paused/legacy-row conflict or account_selection_unavailable |
PUT /api/codex-auth/auto-switch |
Set the global quota threshold with { threshold }, or an account override with { id, threshold }; null restores global inheritance, and __main__ selects the Desktop login |
400 invalid id/threshold; 404 missing account |
GET /api/codex-auth/preserve-signed-in |
Read policy, current affectedAccountIds, identityKnown, nextAccountId, nextUsesSignedInQuota (actual next choice, even when disabled), pinnedAccountId, bindings, priorityFailbackEnabled, and fallbackConsumesSignedInQuota |
503 native profile busy/unreadable |
PUT /api/codex-auth/preserve-signed-in |
Strict { enabled: boolean, applyToBound?: boolean }; explicit applyToBound: true requires enabled, releases affected bindings/pin for next request; reports releasedPin |
400 invalid body; 503 native profile busy/unreadable |
PUT, PATCH /api/codex-auth/pool-strategy |
Update Codex account-pool selection strategy | 400 invalid strategy/config |
PUT /api/codex-auth/failover |
Set the account failover threshold | 400 invalid threshold |
GET /api/codex-auth/quota |
Read cached quota state by account | — |
GET /api/codex-auth/low-quota-events?limit=20 |
Read only this server’s last 0–100 low-quota log/notice and pause-save events (default 20); includes account id and status (logged for the default log-only alert or a host without a supported notifier; delivered when the opted-in OS notifier accepted the notice; failed when it failed or timed out; cancelled when its episode ended first; succeeded for a completed pause save) |
400 invalid limit; management authentication required |
GET /api/codex-auth/reset-credits |
Inspect reset-credit eligibility for an account | 400 missing account id; upstream status passthrough; 500 lookup failure |
POST /api/codex-auth/reset-credits/consume |
Consume an eligible reset credit. Optional operationId (UUIDv4) makes the redemption idempotent: the same id replays one durable outcome instead of spending a second credit. |
400 missing account id or invalid operationId; 409 identity_mismatch when the id belongs to another account; upstream status passthrough; 503 server_busy, capacity, or unavailable; 500 consume failure |
POST /api/codex-auth/login |
Start Codex login or reauthentication | 400 invalid request; conflict/busy login states |
POST /api/codex-auth/login/code |
Submit a manual code for a Codex login flow | 400 invalid flow/code |
POST /api/codex-auth/login/cancel |
Cancel only the pending Codex login identified by { "flowId": "..." } |
400 missing, unknown, or non-pending flow ID |
GET /api/codex-auth/login-status |
Poll a flow or account login state. A completed new-account flow includes catalogRefreshPending: true only when recovery is needed. |
Unknown flows report expired; no active flow reports idle |
PUT /api/codex-auth/active requires { "accountId": "<id>" } or an explicit
{ "accountId": null } to clear selection. Re-selecting the current account is an explicit
selection even if another process changed the saved account. The server preserves unrelated
saved settings and rejects targets removed or paused since the dashboard loaded. An unavailable
or invalid saved configuration leaves the previous live selection and affinity unchanged; reload
settings before retrying. A successful response describes the committed selection. Existing quota
and failover rules can still release a pin; pinDrained reports a currently known drain reason.
For reset-credit consumption, a different operationId supplied while the same physical
account has an unfinished operation joins that operation as an alias. Its retry uses the
original upstream request ID and records the outcome under that same identity, so later
requests with the original ID or a known alias replay the stored result without another
consume request. A previously unseen ID supplied after settlement starts a new explicit
redemption; clients retrying an existing action should keep its ID.
After a confirmed manual reset, OpenCodex checks fresh usage for that same account
and can reconcile its eligible pre-existing shared reset-derived cooldown immediately.
Paused accounts, accounts requiring reauthentication and cooldowns already owned by an
in-flight probe remain excluded from this recovery; their cooldowns are retained. Usage
started before the reset, incomplete or exhausted usage, a changed account, and a newer
quota failure do not qualify. Older main-account usage responses cannot replace a newer
published observation. If usage needs credential refresh, recovery requires that refresh’s
confirmed lineage; an externally replaced credential does not qualify merely because it
belongs to the same account. Explicit Retry-After, Reserve cooldowns, pause
settings, pins and the selected account are preserved. already_redeemed and durable
replay do not prove a new reset and do not gain this recovery behavior.
A failed or busy usage refresh after a confirmed reset or already_redeemed does not
turn the completed consumption into an error: the response remains HTTP 200 with its
consume code, omitting remaining when no fresh count was obtained. This response
confirms the consume outcome, not that the account is now routable. Refresh usage to
check availability; do not consume another credit to retry a failed usage refresh.
If a new account config row is saved but credential setup cannot finish, OAuth login-status reports
status: "error" with
code: "codex_credential_persistence_failed", accountId, needsReauth: true, and optional
catalogRefreshPending: true; storage-error details are not exposed. The account row remains saved:
reauthenticate or delete it before retrying account creation.
Configuration-writer or credential-refresh lock timeouts under this delegated family return HTTP
503 with code CONFIG_MUTATION_LOCK_UNAVAILABLE. Clients should retry shortly rather than treating
that response as a permanent account failure.
Account creation and deletion commit credentials/configuration before catalog convergence. A failed or deferred catalog attempt never rolls back the durable account mutation and never reflects internal provider, account, path, or credential details; clients receive only the completion boolean. Deleting an account retains its selector binding so exact routes fail closed while the account is absent and the same selector is restored if that account id is added again.
Windows compatibility certificate setup
Section titled “Windows compatibility certificate setup”GET /api/codex/desktop-compatibility/certificate returns public certificate status:
support, state, fingerprint, expiry, renewal notice, trust observation and any active operation.
The certificate response also reports mutationAuthorized for the current session and ingress;
this flag is read-only guidance, and each mutation checks authorization again.
It never generates a key, decrypts private material or registers OS trust. Certificate trust alone
does not mean a compatibility relay is running or the private key has been verified in this process.
POST accepts action: "prepare" | "trust" | "remove-trust" | "renew" and confirmed: true.
Trust actions also require the exact uppercase SHA-256 fingerprint returned by status.
These mutations require an operator-paired GUI-session principal from trusted loopback ingress;
an admin token or an automatically issued loopback GUI session receives 403. They are intended for
a local confirmation flow, not unattended certificate enrollment.
Prepare stores a constrained 30-day authority with a Windows CurrentUser-DPAPI-protected private key. Subsequent preparations reuse it. Trust actions never create missing state or silently replace an expired root. Removal refuses while Codex is running or process ownership cannot be established. Cancellation or uncertain OS command completion is checked against the actual certificate store. Unknown state remains an error rather than authorizing an automatic retry. Replies contain no PEM, private keys, account data or subprocess output.
Renewal requires the current fingerprint and an absent Codex app. It verifies removal of
the old certificate’s trust before publishing a validated encrypted replacement. Refused
or uncertain removal preserves the old identity. The replacement is prepared but untrusted;
register it with a separate trust confirmation using its new fingerprint. A stale retry
cannot replace the new identity again. No certificate backup chain is retained.
This setup API does not enable a relay, change login, alter usage, restart Codex or install a watcher. CurrentUser protection does not isolate secrets from other processes running as the same OS user.
Experimental Windows compatibility runtime
Section titled “Experimental Windows compatibility runtime”GET /api/codex/desktop-compatibility/runtime is an inert status read. POST requires an
operator-paired local GUI session and { action, confirmed: true }, where action is start, stop,
observe, launch, or apply. Only apply additionally requires accountWideConsent: true.
The endpoint is experimental. The dashboard exposes it under Codex Set → Desktop compatibility;
the Resume observation when OpenCodex starts toggle saves the optional
desktopCompatibility.startOnProxyStart preference. OpenCodex managed client mode does not offer these
local controls or forward them to the shared hub.
Open the dashboard at the configured loopback origin. Before changing compatibility settings,
use the panel’s ocx gui pair --origin "<dashboard-origin>" command and submit its one-use pairing
code in the panel. Controls stay disabled until the new local session is verified; pairing does
not itself change certificate trust, runtime state or the auto-start preference.
Start requires an already prepared, trusted certificate, a freshly verified native file-based
ChatGPT login and the assessed Codex Windows build 26.924.2738.0. It begins in Observe mode.
HTTP/HTTPS and authenticated SOCKS5 outbound proxies use the same explicit selection for HTTP,
identity verification and upgraded sockets. NO_PROXY is honored; failures never retry directly.
An invalid or unsupported setting returns egress_proxy_invalid. Start never registers a certificate or changes
login. Launch preserves package identity and refuses an app that is already running.
PAC and CONNECT ports are persisted after successful first binding and reused on restart.
connection_unavailable means binding failed; connection_invalid or connection_changed
means stored endpoint identity could not be accepted. These failures never silently rotate
ports or overwrite the existing connection record. Restart returns to Observe, not Apply.
native_routing_unverified means the native root/profile routing cannot be matched to this
process’s bound OpenCodex listener. Runtime status may expose contextFailure for this condition
or build_unverified. These states disable response correction without claiming an app repair.
A status response can also carry buildAssessment, the public metadata of the last completed
build check: assessedVersion (the exact package this runtime was assessed against),
observedVersion, verified and completedAt. It records a past observation, not a live
install reading: a stopped runtime, an app update or a pending or interrupted check can leave it
older than the installed package, and only a completed check replaces it. observedVersion is
null unless discovery matched the assessed package family and entry point; an identified newer
build keeps its own version with verified false, and verified always describes the package
observed at completedAt, never whatever is installed later. The field is absent until the
service creates its probe during start and null until that probe finishes a check; reading
status never starts one.
Apply requires a recently observed eligible exhaustion snapshot and lasts at most three minutes at the response layer. It changes two account-UI gate flags, not usage percentages, credits, spending restrictions or server limits. The usage response does not identify the selected model: this trial cannot promise an effect limited to external models. An accepted activation or produced response does not prove the app accepted the new snapshot or enabled its composer. An available or protected original usage record returns the controller to Observe and invalidates pending corrections. Later exhaustion requires another explicit Apply request. Changed root TOML or configured model/provider/fallback routing invalidates the runtime’s observation context until stop/start; reverting those settings does not reactivate it.
Observe disarms correction and refreshes only validated usage streams. Stop closes this runtime’s
listeners and connections; the PAC includes DIRECT fallback. The certificate stays installed for
reuse. Stop the runtime and close Codex before removing trust or renewing the certificate.
Failed cleanup reports cleanup-required; it does not claim that the runtime is off. OS login,
system proxy settings and application files are not modified by these runtime commands.
The separate settings endpoint returns { startOnProxyStart, revision }. POST requires
{ startOnProxyStart: boolean, revision, confirmed: true } from an operator-paired local GUI
session on trusted loopback ingress; an unpaired session receives 403. A stale
revision refuses the write; a valid update preserves unrelated config fields and verifies
the persisted result. This preference affects the next proxy start, never current runtime
state, app launch or an Apply trial. An uncertain response must be followed by GET, not an
automatic POST retry. Invalid or missing configuration is preserved rather than recreated.
Choosing a client
Section titled “Choosing a client”For ordinary administration, the Web Dashboard gives the safest guided
workflow. For headless hosts and automation, use the corresponding ocx commands: they call this
same live API and return a nonzero result when the proxy is unreachable or the operation fails.
Direct HTTP is most useful for integrations that need the exact endpoint contracts above.
Remote sessions and data-key rotation
Section titled “Remote sessions and data-key rotation”POST /api/keys/rotate {id} starts a ten-minute overlap and returns the new data secret once. POST /api/keys/rotate/commit {id,rotationId} commits it; DELETE /api/keys/rotate {id,rotationId} aborts it. All require management authentication; data keys cannot call them. POST /api/session/logout requires the current gui-session, matching Origin, and CSRF. An admin token receives 403 and can never mint or exchange into a consent session.
Anthropic account usage threshold
Section titled “Anthropic account usage threshold”PUT /api/oauth/accounts/auto-switch
Anthropic OAuth only; { provider: "anthropic", accountId, threshold } accepts integer 0–100 or null to inherit. Missing threshold is invalid. Stored override survives restart and is removed with the account.
Account-list DTOs include autoSwitchThresholdOverride (integer/null), autoSwitchThreshold (pool default), and effectiveAutoSwitchThreshold. 0 disables usage-driven switching only; it never disables pause or 429 recovery.
HTTP: 400 invalid/unsupported; 404 missing account; oauth_mutation_busy on lock contention.
Forced Claude Code subagent model
Section titled “Forced Claude Code subagent model”The Subagents page offers Force all subagents onto one model, off by default. Select an exposed roster-style id, such as combo/tev-auto, then enable the switch. The roster is offered first; unavailable saved roster entries cannot be force targets.
ocx agent subagents force combo/tev-auto sets claudeCode.subagentModelForce; ocx agent subagents force - clears it. ocx agent status reports the setting. GET /api/subagent-models returns force, forceAvailable, and forceStatus; PUT accepts { "force": "combo/tev-auto" } or { "force": null } without changing the roster. Omitting force leaves it unchanged. Invalid or unexposed targets are rejected on write; stale targets are reported and skipped at launch.
This takes effect on the next routed ocx claude launch, injecting CLAUDE_CODE_SUBAGENT_MODEL as an explicit proxy alias (with [1m] only for an authoritative million-token window; native Claude targets use a reversible native alias) and CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1. Each nonempty shell-exported variable independently wins. Native launches inject neither variable; plain claude is not affected. No plugin files or settings.json are modified by this setting.
Claude Code 2.1.257 or newer is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with model: inherit keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available.
The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in settings.json → env (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support.

