Skip to content

Latest commit

 

History

History
600 lines (493 loc) · 75.4 KB

dashboard-and-usage.md

File metadata and controls

600 lines (493 loc) · 75.4 KB

Dashboard Surfaces And Usage

Dashboard page contracts, usage accounting and request metrics, and the management settings that back individual dashboard surfaces. An already-issued, otherwise valid remembered session can Forget after a User-Agent change; access and resumption still require the original browser binding. Serving, authentication boundaries, and /api/* ownership are in GUI and management API. Model-price editing and estimate selection follow pricing.

UX boundary

The dashboard is a local control surface, not a separate service. It should reflect the same config and catalog invariants documented in this folder rather than inventing parallel state. The sign-in form offers opt-in Remember this device for direct HTTPS dashboards or loopback, storing an HttpOnly device cookie plus a separate proof in this site's origin-scoped localStorage; neither is the admin token or works alone, cookies span ports of one hostname so the proof binds the dashboard origin (still prefer a dedicated hostname), and storage failure reports Remember unavailable. Both mobile and sidebar Forget this device actions use logout to revoke the record and derived sessions immediately after successful publication and remove the proof, retaining it on failure for retry (directory-entry crash durability is best effort); remembered sessions cannot approve GitHub stars, reset-credit consumption, prompt-layer edits, Meta Muse sign-in, desktop compatibility changes or quota validation, which require a fresh local or paired session, and session-change events update both surfaces under GUI and management API.

Codex quota cards consume the display cache from src/codex/quota.ts. Partial refreshes remove omitted short tuples after their reset deadline; validated main-window absence also removes future-reset short tuples. Main-account hard-lock evidence and reset-notification history follow separate retention rules in OpenAI account modes. See permanent refusal auto-pause. Codex account panels expose no Spark quota toggle or setting and retain quota refresh, pause/resume, selection order, reset-credit confirmation and Advanced settings; account/provider quota DTOs suppress retired windows upstream of the generic quota renderer, under OpenAI quota scopes.

Dashboard surfaces

Dashboard localization uses the English gui/src/i18n/en.ts catalog as the complete key and placeholder contract. Every registered locale, including Vietnamese and Brazilian Portuguese (pt, HTML pt-BR), supplies the same keys; locale-specific Compatibility Lab, log-guard, routing, vision, status-code, and quota-formatting maps remain total rather than silently falling back to English. The Dashboard registers Qoder for export and managed integration under the Qoder settings ownership contract. T3 Code is registered the same way under the T3 Code settings ownership contract.

The Models catalog names three distinct delivery states. A successful management mutation confirms only that the catalog is saved on the hub. gui/src/api-targets.ts carries the local machine's catalogSyncedAt into gui/src/pages/Models.tsx as the time this client last fetched a catalog; the timestamp does not prove that fetch contains a later hub save. Runtime activation remains explicitly unverified because process age and catalog-file age are not client acknowledgement.

src/server/management/api-access.ts publishes an audio projection through the existing /api/keys response in src/server/management/oauth-account-routes.ts. URLs derive from the same advertised inference base as text APIs, with HTTP(S) mapped to WS(S). Configuration flags inspect enabled canonical providers only; they do not read credentials, inspect account health or prove entitlement. gui/src/pages/api-keys-utils.ts validates the same projection on network and cache reads. Missing or malformed audio metadata disables only audio controls.

Connections/API keys has separate Dictation and Live Voice sections in gui/src/components/apikeys-workspace/AudioApiPanel.tsx. Transient data keys never enter caches or generated samples. gui/src/audio-api-client.ts owns bounded uploads and a connection-only native voice probe; gui/src/api.ts sends uploads without management auth injection or 401 recovery. Voice readiness requires a nonterminal session acknowledgment with session.id, not merely socket open. Changing keys, inference metadata, API origin or leaving the active panel releases requests/sockets. Only allowlisted event types and localized error categories are displayed. Tests live in gui/tests/audio-api-client.test.ts, gui/tests/audio-api-panel.test.tsx, gui/tests/api-auth-memory.test.ts and tests/server/api-access-endpoints.test.ts.

The endpoints panel (gui/src/pages/api-keys-endpoints-panel.tsx) shows the base URL and models endpoint, then one card per public API from gui/src/pages/api-surface-cards.tsx: state, endpoint and the decision source (always served, explicit, inherited from Claude settings, or invalid and closed). The Messages card stays visible while closed, carries the toggle that calls PATCH /api/protocols/settings on the page's apiBase (machine or shared target) before reloading the keys payload, and links to #integrations/claude. parseApiSurfaces (gui/src/pages/api-keys-utils.ts) validates surfaces from the keys payload and the session cache; a server without it keeps the flat endpoint list gated on claudeCodeEnabled. Tests live in gui/tests/api-surface-cards.test.tsx.

The API page's request path preview is gui/src/components/protocols/ProtocolPlanPanel.tsx, placed after the endpoints section. It asks POST /api/protocols/plan through gui/src/protocol-api.ts, which validates the answer with the shared isProtocolPlanV1 and caches it per target, selector, sorted features and policy revision; a 404 from an older server turns the preview off without an error. The panel shows each candidate's path, delivery mode, fidelity, reasons and feature effects (FeatureDispositionList.tsx), and the features every eligible candidate guarantees apart from those only some keep. Delivery mode is not a verification verdict, so the panel shows no Lab badge and does not read ExternalModelRow.native. Tests live in gui/tests/protocol-api.test.ts and tests/server/protocol-routes.test.ts.

The same vocabulary appears on three more screens, each answering one question and each hiding quietly when the server predates its route:

  • Provider settings: gui/src/components/provider-workspace/ProviderProtocolPanel.tsx sits under the adapter field and reads GET /api/protocols?provider=<name> on the settings apiBase (fetchProtocolProviderSummary; a 404 or a body without provider hides it). It names the adapter as the upstream wire the provider receives, the decision source and the per-model overrides, and has no control of its own: the adapter field above it still saves through onUpdateProvider → PATCH /api/providers, and the panel only says what an unsaved choice would send. It is not an API exposure switch; those are the API page's cards.
  • Compatibility matrix: inbound and upstream protocol filters (gui/src/pages/compatibility-protocol-filter.tsx). The Lab subject list has no protocol, so while a filter is active gui/src/pages/compatibility-protocol-pairs.ts reads the listed subjects' details (at most 200, six at a time, cached per target) and maps their Lab identities with protocolFromLabProtocol. A subject whose pair is unknown is left out; a pair with no matching row reads "unverified, not failed", never failed or unsupported. The matrix shows Lab verdicts only; delivery mode stays on the path preview, so the two never share a badge.
  • Combo detail: gui/src/components/protocols/ComboProtocolPlan.tsx in the saved combo's config tab runs POST /api/protocols/plan on an explicit click for every feature the chosen client API can express, and renders the shared PlanResult: each target's path and feature effects, and the guaranteed/partial split. It reads the saved combo, and says so while edits are unsaved.

Deep links (gui/src/protocol-deep-links.ts) carry their target in the hash query, which resolveAppHashChange keeps only on #providers and #models/compatibility (QUERY_HASH_PATHS) and drops elsewhere. Each plan candidate links to #providers?provider=<name> (gui/src/pages/providers-deep-link.ts selects that provider and opens its Settings tab, and drops the query once another provider is chosen) and to #models/compatibility?inbound=…&upstream=…; a traced Logs row links to the compatibility pair it took. Each chip of the header quota strip (gui/src/components/quota-summary-bar/QuotaSummaryBar.tsx, one scrolling row with « / » paging) links to #providers?provider=<name>&tab=accounts, which opens that provider's Accounts tab through revealProviderAccounts instead; following the same link again re-dispatches hashchange so it re-applies. A transparent hit area above the fixed quota popover spans both its width and the visible chip width and bridges their measured gap, keeping details open during pointer travel. The popover is a named group with an account-management link; its text-only quota table describes the chip; focus stays open between its chip and link, and Escape restores focus to the chip. Links push history, the matrix replaces the entry when its filter is edited, and both targets re-read the hash on hashchange/popstate, so Back and Forward restore the prefilter. Tests live in gui/tests/provider-protocol-panel.test.tsx, gui/tests/compatibility-protocol-filter.test.tsx, gui/tests/protocol-deep-links.test.ts, gui/tests/providers-deep-link.test.tsx, gui/tests/quota-summary-bar.test.tsx and gui/tests/combo-protocol-plan.test.tsx; gui/tests/quota-summary-hover-browser.ts checks real pointer travel across the quota popover gap with production CSS.

The API workspace gives gui/src/components/section-tabs.tsx its mobile reading line so scroll-spy and the top-bar offset agree; other consumers keep their existing reading line. The section strip stays one row at every width.

Provider Overview consumes the existing shared add-provider-presets resource for sponsor presentation. matchingWorkspacePreset requires the configured id, adapter and normalized endpoint to match; a custom endpoint or absent sponsor metadata suppresses the introduction. ProviderSponsor keeps localized promotional copy and outbound HTTP(S) links separate from operator notes; its brand table maps each sponsor preset id (OrcaRouter, TokenLab) to a name and i18n copy, and a sponsor preset without a row renders nothing. Notes stay editable once in the main column; stats and account quota stay in the side column. Nothing here writes config or routes.

Provider marks remain a name-to-asset projection in gui/src/provider-icons.ts. The Crusoe preset maps to the self-hosted multicolor gui/public/provider-icons/crusoe.svg; the gradient is rendered as an image rather than flattened through the monochrome mask path.

The sidebar exposes eight rows (gui/src/nav-groups.ts NAV_GROUPS), in order: Dashboard, Connect, Codex, Providers, Models, Subagents, Usage & Logs, and Remote Link. Connect opens Integrations with the heading Connect; its tab strip orders Codex, Claude, Claude Desktop, and Grok Build before the remaining integrations. Claude is marked by NavGroup.embedded and has no sub-tabs and renders the Claude Code settings directly; #claude and #claude/code both open it and the former #claude/settings redirects to #claude/code. Claude Desktop is a separate Connect tab at #claude/desktop, containing the status row with Save / Save & apply, mode picker, and model families. Connect has no section switcher. Usage & Logs contains Usage (opened first), Logs & Debug, and Storage with the pill section switcher. The last row, Remote Link, groups Remote Link (#remote) and Remote Workspace (#remote-workspace, only when available) with the pill section switcher. Codex is the renamed Codex Set and retains #codex-set; preserved Claude URLs are #claude, #claude/code, and #claude/desktop. Account management lives only on Providers > Anthropic > Accounts; the former #claude/account bookmark redirects to #providers?provider=anthropic&tab=accounts. Legacy #integrations/claude and #integrations/claude/desktop still redirect to #claude/code and #claude/desktop. Startup (#startup) stays outside the sidebar. Several member pages are workspace shells rather than single forms, and the shell pattern is the part worth keeping stable:

Surface Shape
Providers Rail of configured providers plus a detail pane whose tabs are Overview, Models, Usage, then Accounts or API Keys when the provider has an auth surface, then Settings (gui/src/components/provider-workspace/ProviderDetails.tsx).
API keys Key table plus per-key detail (gui/src/components/apikeys-workspace/). The list payload carries masked prefixes only; each row deletes in place behind a two-step confirm, and clicking a key fetches the full value from POST /api/keys/reveal, which requires a current pairing or trusted Tailscale-identity session and answers no-store; automatic loopback sessions and raw admin tokens cannot reveal stored values, and a refusal there answers with the pairing surface — on the same-origin standalone transport the local pairing form, elsewhere the explanation alone — and requires a fresh reveal click after pairing. Displayed secrets and copy feedback are cleared on shared-session loss, pairing, inactive/hidden views, or a target change; stale asynchronous completions cannot restore them.
Storage Rail plus cleanup and trash detail (gui/src/components/storage-workspace/).
Subagents Featured-roster selection workspace (gui/src/components/subagents-workspace/).
Combos Rail, detail panel, and an add flow (gui/src/components/ComboWorkspace.tsx).
Add provider Catalog browser plus form and OAuth panes (gui/src/components/provider-catalog/, gui/src/components/AddProviderModal.tsx). The catalog browses four tabs — Accounts, Free, Local, Paid — where Local is a catalog-only bucket peeled out of bucketPresets after presetTier has classified; the workspace providerTier stays three-way, so the rail, the free-paid sort and the Free count still treat a local runtime as free. Search sits above the tabs and reaches every tab at once: while a query is live the list renders all four groups with headings and the strip becomes jump chips with counts rather than a tablist, because moving the selected tab would change the row kind under the user (a preset-select button becomes a login row). ArrowDown from the search input focuses the first enabled result action; if none is available, focus stays in the input. The tab strip wraps within narrow modals. Every nonempty note has a full-text button so narrow rows never hide content permanently; the native note dialog closes during teardown and restores focus to its trigger. Provider notes clamp to two lines and open in full in a stacked native <dialog> owned by AddProviderModal, which also owns the search text so its window Escape handler can unwind popup, then query, then dialog.
Codex accounts Account pool cards, add-account flow, switch and reset modals (gui/src/components/CodexAccountPool.tsx, gui/src/components/AddCodexAccountModal.tsx), plus the generic account-targeting picker opt-in on gui/src/pages/codex-set-multiauth.tsx. Add/delete/login completion is projected to one boolean before presentation; pending catalog work is a warning, not a failed account mutation. The main card's native-main device reauth (#3898) is owned by gui/src/components/use-main-device-reauth.ts: the dedicated /api/codex-auth/main/reauth-device namespace only — never the pool login route — with flowId-owned polling, an allowlisted verification URL, and no token fields accepted from payloads. The main-device reauth hook retains flow ownership from the Cancel click, while DELETE is unresolved and after retryable failure; polling normally continues. A concurrent GET HTTP error cannot expose a replacement login POST before DELETE settles. If a retryable DELETE failure races with a non-2xx GET while the flow is pending or committing, either response order preserves same-flow Cancel retry, restores the last server-provided device code, verification URL, and phase when needed, and keeps the existing poll cadence so a later terminal result remains observable. Outside same-flow cancellation ownership, a GET HTTP failure still stops polling without starting a second login POST. The cancellation-failure indication survives pending status updates until a trusted terminal result releases ownership. A successful DELETE with a terminal failed DTO releases it and uses the same closed failure-code mapping as polling; only succeeded notifies login completion. Unrecognized or nonterminal DTO status values remain retryable. A DELETE response with HTTP 404 and code unknown_flow releases the expired flow and shows the existing generic failure state so device re-login is available again; it claims neither login success nor confirmed cancellation. Confirmed cancellation also makes device re-login available. Start, polling and cancellation completions verify their controller or flow ownership after asynchronous response reads; replaced flows and unmounted hooks cannot update a newer flow or notify completion. Effect setup restores mounted state after the StrictMode development cleanup cycle.
Dashboard overview Overview, Providers, and Models tabs at the page level (gui/src/pages/Dashboard.tsx), the 30-day token and coverage stats in the overview head (gui/src/pages/dashboard-overview-head.tsx), and the effort-cap, injection, maintenance, sidecar, reasoning-retention, and memory panels below it (gui/src/pages/dashboard-overview-panels.tsx). The Reasoning retention controls (gui/src/components/ReasoningRetentionPanel.tsx) live in a collapsed Advanced settings disclosure at the end of Overview. They read and save reasoningRetention via GET/PUT /api/settings in src/server/management/config-routes.ts: only explicit enabled: true activates retention; absent, disabled, and legacy threshold-only settings remain off. The enable switch exposes numeric controls with defaults of 20% and 100000 tokens; disabling saves enabled: false, and null reset removes the setting and turns it off. Omission preserves it, validation precedes mutation, and save failure restores value and key presence. Copy warns that opt-in can preserve plaintext reasoning in local history/archive and that completed archives have no aggregate quota or expiry and require manual deletion. Loading disables writes, failures permit retry without losing edits, and stale responses cannot update a new API target. Its scope is the routed v1/v2 compaction contract; opaque output stays unchanged. tests/config/settings-reasoning-retention.test.ts and gui/tests/reasoning-retention-panel.test.tsx cover the API and controls. Inputs expose invalid state and reference the validation alert only for the failing field.

JEV setup and its Stats tab reuse these shells; omitted advanced quota fields survive editor saves without new GUI controls. Content-free v1 quota tier counts/selection follow loaded advisory quota; see providers/jev-decision.md.

The native-main reauth poller captures an immutable accepted flow id for queued callbacks. Its POST, GET and DELETE JSON reads retain API error codes, but non-2xx responses never become successful flow DTOs. The three local React Doctor response-body exceptions preserve that tested contract without disabling the rule for other calls.

Rail selection is component-local state today, so a reload returns to the workspace's default selection rather than the previously selected row. An OAuth ToS warning is shown before a login that requires acceptance (gui/src/components/OAuthTosWarningModal.tsx); oauthTosCopyKeys in gui/src/oauth-tos-risk.ts picks every key it renders, and Anthropic has its own subscription-connection title, conditions, API-key alternative, acknowledgement and continue copy. Each mount starts unchecked, and Cancel, Escape and the backdrop never start OAuth (gui/tests/oauth-tos-warning-modal.test.tsx). The Claude account-pool card states its usage conditions as static helper text with a closed details disclosure. Only a failed save is announced as an alert (role="alert"); a failed load replaces the status line with static text and disables the toggle. The enabled status line is chosen by strategy, so round-robin never claims to read usage, the threshold or the quota window (gui/tests/anthropic-pool-conditions.test.tsx). Provider-login polling follows the current continuation contract: device approval shows its code and verification link without a callback paste field; a later manual step replaces that hint and restores paste. Discovery reflects the current management principal.

The /#codex-auth add-account modal has a three-step manual-code UX contract on top of the existing OAuth polling API: submit request, waiting-for-login completion, and terminal success/failure. Once POST /api/codex-auth/login/code succeeds, the GUI must keep the input disabled, expose an aria-live status message that the code was accepted, and surface repeated login-status polling network failures as a visible warning instead of silently looking idle again.

Fixed provider redirect URIs keep their registered port. If that port is already in use, a login controller with onManualCodeInput enters manual-only mode: it must still publish the authorization URL and accept the final redirect URL or code through the same state/PKCE session. A controller without manual input must fail closed; it must not silently move a fixed redirect URI to another port.

The account-targeting control reads the effective flag from GET /api/settings; it must not infer state from the redacted config DTO or expose selector mappings. It renders no actionable off switch before hydration, serializes rapid clicks, rejects stale poll results that started before a mutation, and accepts the server-confirmed state after PUT. Product copy describes arbitrary public selectors and exact account binding only—there are no built-in Personal/Work roles. A pending catalog refresh keeps the saved state and renders fixed ocx sync guidance without server/account detail.

Usage accounting

OcxUsage.providerCredits preserves provider-reported credit spend in request and attempt rows through src/usage/log.ts normalization and ledger reloads. Missing readings stay absent, and zero is a measured value. Separate attempts add credits when usage is merged. The field is independent of token estimation (estimated describes tokens) and is never treated as USD or token usage. The human log projection in src/cli/observe.ts includes the persisted request ID as id=... for direct use with ocx logs get and ocx logs explain. Rows without an ID or with control characters in their ID omit it rather than rewriting the lookup key; JSON and JSONL keep the API payload. ocx logs history pages the same derived index the dashboard uses (GET /api/request-history) with a resumable nextCursor.

Upstream key account attribution

API-key attempts in src/usage/log.ts carry accountLogLabel as k plus 32 lowercase hex digits. src/codex/account-label.ts derives it from the first 128 bits of SHA-256 over JSON.stringify(["ocx-key-account-v1", providerName, entryId ?? null, reference]). reference is the configured value captured for the physical send, before environment or keychain resolution. The log contains the digest, not raw keys, references, or pool IDs. Existing Codex and OAuth label formats remain valid. Replacing a literal or reference changes identity; rotating the secret behind the same reference preserves the logical account.

src/usage/jev-stats.ts owns the parallel content-free JEV projection; its accumulator contract lives in providers/jev-decision.md.

src/providers/label.ts stamps only key authentication, including implicit custom-provider keys. src/server/request-log.ts commits identity at dispatch after queued selection changes, retains separate flat records when retries change keys, and isolates each record's raw usage from parent combo totals and adapter-loop aggregation. Reported failure usage is retained; missing usage and historical identities remain unknown. Native wire snapshots replace only the current physical response contribution, preserving prior sends on the same key without counting repeated inspections twice. A row with two or more attempts, combo or not, takes its request total from those attempts, so a key or account rotation is not reported as its last record alone; a single-attempt or locally answered row keeps its request-level usage. Consumers sum the flat attempts once and keep subscription quota observations separate from token or API-equivalent cost totals.

src/server/hub-usage.ts serves GET /v1/usage on hubs for an explicit configured data key. The authenticated key selects the aggregate; query parameters cannot select an API-key identity. Unscoped environment/admin credentials and loopback bypass are not admitted. The response projects only this client's numeric totals, provider/model/day rows and incomplete-history metadata through src/remote/hub-usage.ts; accounts, raw records and key IDs are omitted. Unknown fields are stripped at every object boundary and the serialized body is capped at 1 MiB.

Custom usage windows are immutable bounds on the streaming accumulator, applied before attribution and daily aggregation. The filtered cache includes both inclusive millisecond bounds in its identity and retains the existing ledger revision, overlay-version and timezone checks. At most four distinct filtered scans may run concurrently; identical requests share one scan and excess distinct work fails closed. Preset warming never consumes custom summaries. The response retains its preset range discriminator for compatibility and explicitly marks customWindow, since, and until; the chart uses the window's local calendar days with the existing 366-day cap. GUI custom reports bypass the held preset/session cache. Both dashboard and CLI reject a custom report unless the server echoes customWindow: true and the exact requested numeric since and until. An older daemon that silently returns a preset report cannot supply totals labelled with the requested custom interval.

Resetting a manual model price keeps the map, even when temporarily empty, through persistence reconciliation. This removes only the requested entry and preserves sibling rates independently written to disk. The Desktop sign-in preference likewise distinguishes saved from applied state: its pending flag survives cache refresh/remount until a successful sync confirms application.

Subagent fallback settings load independently of the main roster. Their failure disables only fallback controls and provides a retry; available fallback options come from that endpoint's availability list while already-configured stale values remain editable.

Subagents → Advanced uses the current API server's recommendation for Always proactive delegation (formerly Ultra mode). Enabling requires the native v2 flag, explicit v2 mode and a recommendation with nonblank string text and revision. Missing or malformed recommendations disable preset installation and restoration while existing custom hints remain editable and clearable. Restore changes only the editor draft; Save writes it. Recommendation-only refreshes preserve unsaved drafts. Switching API servers hides the previous hint and blocks mode writes until the new server's settings arrive.

An explicit multiAgentModeHintText write canonicalizes only the two byte-exact legacy OpenCodex presets; other valid custom text keeps its bytes. GET, unrelated PUTs and upgrades leave stored hints unchanged. null clears the hint, blank strings are rejected, and the existing native capability check still precedes writes. The text and revision recommendation is supplied independently of stored TOML and is not evidence of native runtime support.

Account quota discovery is capability-based. Cheap OAuth and provider-key lists include quotaMode (probe, passive, or unsupported) without contacting upstream quota APIs. GET /api/oauth/accounts?provider=...&quota=1 and GET /api/providers/keys?name=...&quota=1 enrich each supported credential separately; refresh=1 bypasses settled quota cache while joining a current same-identity read. OAuth readers use the named stored account; key readers use isolated per-key configuration, never active-key mutation or the provider-wide cache. Response projection rechecks key identity and exposes only quota/availability fields, not its internal identity guard. Passive observations retain their original timestamp and never trigger inference or token renewal. Unsupported, unobserved, failed and measured-zero readings remain distinct; multiple keys are not summed because they may share one upstream balance.

Provider details use one account-quota reading renderer for Overview, Usage and Accounts/API keys. Current-account usage sits below usage statistics; a known-mode active row is authoritative even when empty, so a newly selected passive account cannot inherit a previous account's cached report. Pool reports project only aggregation.currentAccount.quota with its own timestamp; missing or malformed aggregation stays unknown rather than using total capacity. Shared states include credits-only and measured-zero readings, unsupported, unobserved, explicit pending and unavailable-with-last-good. Forced account/key enrichment settles before its control reports a completed check, and provider-report waiters are bound to the exact refresh epoch.

Main-account WHAM refresh diagnostics are an ephemeral quotaRefresh outcome carried from fetchMainAccountInfoWhileOwned to the generation-checked account DTO and the opt-in CLI quota JSON. They are not persisted or consumed by admission/rotation. A private per-dispatch identity generation fences the diagnostic independently of ordinary quota metadata. Both snapshot and account DTO publication omit externally invalidated attempts; the generation itself is never serialized or stored in the quota cache. The CLI reconstructs the object using a fixed vocabulary and bounded numeric HTTP status, so an unexpected management response cannot add raw upstream material. An http_error may carry code only from CODEX_TERMINAL_AUTH_CODES (src/codex/quota-refresh-outcome.ts; e.g. token_invalidated after a plan change revokes the session), and a failed read keeps the main row's last-known plan. Reauth attribution uses current terminal-auth evidence behind the same generation fence; a transient HTTP401 diagnostic does not replace an existing refresh-failure cause.

Decision record: ADR-0078

src/usage/log.ts writes append-only JSONL to ~/.opencodex/usage.jsonl with file mode 0o600 inside an owner-only 0o700 directory. Consecutive appends reuse the directory and permission check for at most one second; the first append at or after that boundary attempts to reapply both modes, and an ENOENT append invalidates the cache and recreates the path immediately. This is a bounded, write-triggered repair of externally widened POSIX modes, not continuous filesystem monitoring or protection against another process changing the path again after the check. An opt-in shadow-call rewrite persists the bounded, redacted original helper model as shadowCallRewrittenFrom, so helper traffic remains identifiable after restart without storing request content or inferring a helper subtype from timing. A failed request persists closed failureStage and failureCause members on the attempt that ended it and on the logical row, derived once at addFinalRequestLog from facts that are themselves closed; errorCode and upstreamError carry upstream text and are deliberately not read there. The resend verdict they imply is never stored — /api/logs computes resendPermission at read time, so a row written by an older build cannot assert a permission the current tables refuse. An attempt also carries deliverySummary: adapter events, relayed frames, semantic bytes, side effects and terminal frames, counted where each event is delivered rather than where it is read, so the gap between the first two is the loss signal. Provider debug formats one ring line per finalized attempt from those counts and writes no second record. GET /api/usage?failures=1 groups failed rows by a versioned fingerprint over closed vocabularies only, rebuilt through the same cooperative scanner and inheriting its bounds, so deleting a ledger row removes it from the grouping. usageLedgerMaxBytes is unset by default; when set, an append that crosses it publishes the newest whole rows byte for byte through the shared atomic writer, refuses the rename unless the source is the exact revision that was copied, and then discards the Logs ring, the retained aggregates and the request-history index so no surface serves rows the ledger no longer has. That index (src/routing/history/) rebuilds a ledger of at least REQUEST_HISTORY_WORKER_REBUILD_MIN_BYTES (1 MiB) on a worker thread (#6990): rebuild-worker.ts, driven by rebuild-job.ts, ingests the ledger snapshot the refresh recorded into a private <index>.rebuild-<pid>-<n> file through the same ingest.ts the inline path uses, switches it to WAL and closes it; the serving thread then closes the old index and renames the build over it. Async readers wait for the build; the routing-time sync entry never does and throws RequestHistoryIndexBusyError, which health scoring reads as unknown evidence. Health scoring (src/routing/health.ts) counts physical sends only: a request answered locally or refused before any send is no evidence for any target, and a row's terminal status and close reason score its last physical send, so a 200 stream cut mid-flight after a failover counts against the target that dropped it (tests/server/health-scoring.test.ts). A close discards the build, rejects waiting readers with RequestHistoryIndexClosedError, and removes the build files only after the worker thread exits. For 30 s after a worker build fails (other than on a replaced ledger), async readers and manual rebuilds rebuild inline instead, while routing-time reads keep reporting busy. Ledger revisions in src/usage/log.ts capture device/inode identifiers from bigint descriptor stats as exact decimal strings; history metadata, ingestion, cooperative scans and retention compare them without narrowing to Numbers, preserving distinct NTFS identifiers above 2^53. An older index containing a rounded identifier fails the identity check and rebuilds once. src/usage/summary.ts turns that file into the /api/usage shape — totals, daily zero-filled grid, model and provider breakdowns, and measured / reported / unreported / unsupported / estimated counts. Each scope also aggregates end-to-end output throughput (#6309): measured output tokens and wall-clock durationMs are summed over attempts reporting both, exposing throughputTokensPerSec — a token-sum-over-duration-sum, never a mean of per-request rates. throughputSamples counts qualifying attempts (legacy rows without attempts contribute one); nonpositive or nonfinite tokens/timing are excluded. The Usage summary and Models/Providers tables display the rate and sample basis, or unavailable when none qualify. All three throughput displays use the same finite-number guard as their tooltip; malformed or nonfinite rates are unavailable rather than formatted. The management route scans the ledger from its beginning in fixed 1 MiB chunks on a cold rebuild, then retains compact numeric aggregate state and resumes at the last verified LF for ordinary appends. It does not retain the full input or a normalized object for every request, and neither the old byte window nor the parsed-entry cap can discard an earlier prefix before range and surface filtering. managementUsageMaxReadBytes remains a recognized compatibility setting for bounded legacy readers, but it is not an accuracy limit or tuning knob for GET /api/usage. A Codex-surface response includes an accounts breakdown keyed by stable non-PII accountLogLabel; cards join it to the management account DTO for 30-day tokens, API-equivalent cost and coverage. New main-pool rows use main; legacy bare openai rows remain ambiguous. A missing usage.jsonl returns a zeroed summary with 200 because a fresh install has no usage. Unmeasured requests remain distinct from measured zero through measured / reported / unreported / unsupported / estimated counts and their coverage totals. Cost estimates in src/usage/cost.ts price only reported or estimated usage, so a partly measured row is never given a confident, understated cost; per-row pricedRequests / unpricedRequests count a request once, unpriced when any attempt is unpriceable, while a usage-less attempt that made no physical send (sendCount === 0 or locallyAnswered) or was refused with an HTTP status >= 400 other than a 499 client cancel before any output (no streamAborted, no firstOutputMs) is a known zero, and a row with any measured attempt counts priced or unpriced, never unmetered, whatever its aggregate status. A usage-less attempt whose response began, aborted, was cancelled by the client, or has no error status may have been billed and stays unpriced. Normal dashboard pages use the document as their vertical scroller. In gui/src/styles.css, body horizontal overflow is clipped without creating a second scrollport; the mobile open-drawer state also locks document overflow. The shared sidebar stays viewport-height through the end of Codex and Claude account pages. gui/tests/shared-scroll-browser.ts checks the built entry and App styles with short/long content, both themes, desktop chrome, collapsed navigation and mobile widths. The Usage tab renders that shape and the main Dashboard shows its 30-day summary. The 200-entry in-memory requestLog is not the aggregation source; the JSONL ledger is. Usage table scrollports in gui/src/styles-usage-workspace.css contain absolute screen-reader captions so long tables do not extend the outer document beyond the report; gui/tests/usage-scroll-browser.ts measures that boundary and last-row reachability at desktop and mobile widths. Ledger read failures instead return 500 { error: "read_failed" }. Shared GUI usage admission reads that body before classifying HTTP failure and also rejects the legacy HTTP-200 envelope, so every shared cache retains its last valid report rather than fabricating zero totals.

Decision record: ADR-0106

A row also records the upstream cost of its logical request. logicalRequestId names the turn that a retry leg, a repair refetch and a combo child all belong to, and spend aggregates their physical sends: sends totals every attempt on the row, settled counts the sends whose attempt reached a terminal status, and unresolved holds the rest — an attempt abandoned in flight, or a budget charge no attempt row accounted for. Unresolved spend is never folded into settled, because an unexplained send is the quantity the record exists to expose. reserved and policyVersion report the request execution budget's final state, and moveReasons names every pool-binding move that discarded a warmed prompt-cache prefix. /api/usage totals these as sends, settledSends, unresolvedSends and spendRequests; per-attempt sendCount remains the accounting source, and attemptCount is the smaller number because retry layers re-send inside one attempt.

Cache detail is qualified by provenance rather than read as a measurement. cacheProvenance is observed, synthesized or unknown: strict-client normalization emits zero-default token-detail objects on every bridged wire, so a cached_tokens: 0 recovered from a parsed wire is a wire-compatibility artifact, and a row with no cache fields measured nothing at all. Only observed input tokens reach the cacheHitRate denominator, reported alongside it as cacheObservedInputTokens, and the summary counts the three provenances separately. A row written before the field existed is reconstructed from its own shape, so historical rows keep their previous reading. /api/logs marks a non-observed detail with cache_detail_missing on the cost estimate rather than pricing the turn as a measured uncached send.

A pool selection that produced no account reaches no auth context, so its cause is recorded on the request that failed for it: reasons are held per (thread, model lane) and consumed by that lane alone, because a thread holds one binding per lane and a thread-keyed reason lets one lane report a cause that fired on another. The failing row carries affinity: "cleared", its reason and errorCode: "codex_no_account"; no synthetic row is emitted, since /api/usage counts one row as one request. Affinity now reaches disk with the rest of the row — the field-by-field projection in addRequestLog did not name it, so a move survived only until the next restart.

Usage aggregation does not infer confirmed model identity merely from a requested selector. Model rows with saved unchanged default-provider route evidence carry hasUnresolvedRequestedModel: their tokens stay under the recorded serving provider, with an unresolved-request annotation. For those slash-containing selectors, a vendor-only inferred price is unavailable; exact provider and user prices remain eligible. Missing trace evidence is not reconstructed from today's configuration. Provider-detail model shares use that provider's token total, not the global total. Unknown reserved policy/ selectors are rejected before upstream dispatch; historical rows remain unchanged. Haiku 5.5 in src/usage/expected-prices.ts uses 0.10/0.50/0.01/0.125 USD per MTok (input/output/cache read/5m write), with all four rates multiplied by five above 100,000 raw prompt tokens; exactly 100,000 stays base. Exact provider/id bands include Cursor regular effort spellings and reference-priced preemptive surfaces; Kilo stays untiered. Sonnet 5.5 cache read is 0.10 (0.05x input), except Bedrock retains partner rates and Venice has its 1.25x exact overlay. Expected-price overlays are estimates, not billing reproductions: the Z.AI GLM rows (zai, zhipu-bigmodel, zhipu-bigmodel-coding, zhipu-bigmodel-responses) display the published z.ai USD list price on surfaces that actually bill by Coding Plan subscription or CNY-tiered domestic PAYG, and every such row is marked verified-derived so the estimate flag reaches the UI. Antigravity Claude Sonnet and Opus 5.5 base/tier overlays in src/usage/expected-prices.ts likewise use Anthropic reference prices with verified-derived, so even reported tokens retain an estimated-cost flag.

The management API retains the compact accumulator plus bounded query summaries; it never retains normalized per-request rows after a response. File identity changes, shrinkage, same-size metadata changes, pricing-overlay changes, and local-time-zone changes force a cold rebuild. Ordinary growth is treated as an append: the scanner verifies the previous LF and its trailing 64 KiB digest, then folds only the suffix into a cloned accumulator and publishes it after validation. Concurrent callers share that work. Cold rebuilds scan the whole ledger in fixed-size chunks and yield between bounded batches, so memory stays bounded and unrelated management requests remain serviceable even for a large existing log. The first read is proportional to ledger size; later refreshes hash the bounded retained window once and parse only the appended suffix, rather than rescanning the whole ledger. The Dashboard polls its 30-day usage independently once per minute, separate from five-second state polls. An unchanged retained snapshot reuses its verified region digest only for identical bounds; appends or trimming hash the returned region, preserving same-inode rewrite detection.

Decision record: ADR-0102

An oversized row is skipped inside the scanner bound without shortening identities. Accumulators keep normal rows plus usageIncomplete / usageIncompleteReason: "oversized_rows" on caches and rollups; append ORs the flag and a rebuild recalculates it. Invalid-row counts are not sticky, and absence of the flag is not completeness. GUI caches warn on Usage, Dashboard, provider and key views; CLI warns in human output only; most-used order save refuses an incomplete snapshot. Quota surfaces stay separate. Legacy truncation fields keep their meaning; read/mutation failures still fail closed.

usage.jsonl is an append-only runtime ledger. A manual in-place edit earlier than the trailing 64 KiB checkpoint followed by file growth is intentionally outside the incremental detector's contract: validating arbitrary historical rewrites on every refresh would require rereading the whole prefix. Replace or truncate the file, or restart the proxy, after manually changing historical rows so the next request performs a cold rebuild.

The wire fields historyTruncated, truncatedPrefixBytes, entriesTruncated, and entriesDropped remain in the response for compatibility with older GUI and CLI clients. A successful whole-ledger scan reports false, 0, false, and 0; clients must not interpret those fields as evidence that managementUsageMaxReadBytes was raised or that a bounded tail was selected.

Decision record: ADR-0079

Opt-in aggregate request metrics

metricsExport.enabled is default-off and fixed for one process lifetime. When enabled, src/server/index/serve-options.ts creates one src/server/request-metrics.ts owner before the shared fetch closure, so every listener spread records into the same bounded cells. Request logging calls the injected recorder once from addFinalRequestLog; the management route receives only a snapshot capability. There is no module-global active registry, timer, outbound connection, scrape-time log scan, or persistence. Restart creates a fresh owner, resets every counter/histogram, and changes opencodex_metrics_process_start_time_seconds. The opt-in owner also renders four Kiro quota gauges from fresh, identity-matched cached observations in src/providers/kiro-quota-metrics.ts. It emits at most 32 distinct opaque account labels and makes no scrape-time upstream call; missing, future-dated, expired, or reset-passed evidence emits no sample.

The label vocabularies are closed: protocol is responses, chat, messages, or unknown; result is completed, failed, incomplete, or aborted; recovery is one of the coarse classes listed in REQUEST_METRICS_RECOVERY_CLASSES, and cause is one of the shared failure causes in REQUEST_METRICS_FAILURE_CAUSES, which aliases the dictionary rather than copying it. Each is the roster the exporter itself iterates. The count is deliberately not restated here: it was written as eight, a bounded label value was added, and the documentation then contradicted the output it describes. A logical request increments once, physical sends sum the finalized attempt counts, and each distinct recovery kind already retained on an attempt contributes once to its coarse class. The Antigravity validation-refusal sibling resend records oauth-account-403, which Logs labels from the shared recovery roster. A Codex HTTP 402 account move records oauth-account-402, maps to quota-exhausted and the quota metric class, and has a localized Logs label; HTTP 429 remains a rate-limit recovery. opencodex_request_failures_total counts the cause the recorder derived and never re-derives one, and it labels a counter only: no histogram carries a cause. HTTP 200 never overrides a failed terminal event. Duration observes every valid finalized duration; TTFT observes only finite nonnegative first-output values, while opencodex_ttft_missing_total is the complementary denominator. The only account-specific metric label is the bounded Kiro opaque digest; no raw request, credential, account, provider, model, conversation, error, prompt, tool, body, header, or URL value enters a label or sample.

For diagnosing upstream-shape / usage-extraction issues run ocx debug usage on (or set OPENCODEX_USAGE_DEBUG=1 before start). The proxy then writes a rolling debug record per finalized request to ~/.opencodex/usage-debug.jsonl (mode 0o600, auto-trimmed to the most-recent 100 lines once it exceeds 200) with the upstream content-type, body kind (sse / json / other / none), a 2KB body sample, and the extracted usage. Off by default; the hot path is guarded so production stays untouched.

For diagnosing cache-read instability without capturing content, set OPENCODEX_CACHE_DEBUG=1 before start. src/usage/cache-diagnostic.ts then writes one record per finalized request to ~/.opencodex/cache-debug.jsonl (same 0o600 file, same 200-to-100 rolling bound) holding only presence booleans, counts, closed enums, the raw upstream cache counter before defaulting, and process-local HMAC equality tags for the prompt-cache key, allowlisted session headers, the account log label, and ordered instruction/tool/message blocks (capped at 128 per section, first divergent section/index only). The signing key is created at process start and never persisted, so tags compare values within one proxy process and never become a durable correlation key; no prompt text, tool name, raw identifier, or header value is recorded. Off by default.

Z.ai quota destination ownership

src/providers/quota/vendor-probes-key.ts uses one exact normalized-base mapping for both Z.ai quota eligibility and monitor selection. International root, coding Chat, Anthropic and Responses bases use api.z.ai with Bearer authentication. Existing BigModel CN root, coding Chat and Responses bases use open.bigmodel.cn with the raw key. Unsupported bases produce no probe; redirect refusal and quota parsing/cache semantics are unchanged.

Decision record: ADR-0096

Provider debug logging

Provider transport diagnostics (dropped SSE frames, adapter dial/stream events, etc.) are opt-in: ocx debug provider on / ocx debug provider off on the running proxy, the Debug-page toggle, or OCX_DEBUG=1 on the next start (legacy OCX_DEBUG_FRAMES still enables the same path). Lines use the [ocx:<adapter>:<event>] prefix, go to the proxy terminal, and are buffered for ocx debug provider logs / ocx debug provider logs -f. Usage JSONL tails with ocx debug usage logs [-f]. Separate from provider buffered logs above.

The shared Responses path follows the bounded multipart recovery contract; credential admission and retry policy remain unchanged.

Remote credentials and bounded sessions

Data keys authorize only the data matrix and authenticated catalog. Admin credentials authorize ordinary management and key rotation but cannot mint, exchange, or refresh a gui-session. Pairing grants are digest-only, origin-bound, one-use, capped at 128 live grants, burned after five grant failures, and source-limited after ten failures in ten minutes with at most 1,024 source buckets. POST /api/session/logout invalidates only the current origin/CSRF-authorized browser session; for a remembered device, a successful logout additionally forgets that device record and every session derived from it.

Model picker ordering settings

GET /api/subagent-models retains chosen, available, and catalogState, and adds routed-only pickerAvailable, saved pickerOrder, and nullable pickerOrderMode. available still includes saved disabled/missing roster choices; it is not the eligible-picker set. Bare aliases are excluded from the routed preset surface because a bare id activates complete Codex-picker ordering.

PUT accepts models and/or pickerOrder; pickerOrderMode requires pickerOrder and accepts alphabetical, provider, most-used, or null. Roster arrays keep their existing exact string values and five-slot cap. Picker arrays reject blank, duplicate or ineligible ids. Null/empty order clears order and mode; a nonempty order without a mode clears only the mode. Validation finishes before a synchronous live mutation/save. An unsupported future deletion-provenance format returns 409 for picker writes instead of losing clear intent. Deletion intent is staged separately and materialized as existing config rebase provenance, so failed persistence restores the touched fields without contaminating the live object's pending-deletion state. Absent fields are not copied back from a snapshot taken before discovery, preserving concurrent roster changes.

Only roster writes sync Claude agent definitions/auto-apply Desktop profiles. Picker writes converge the Codex catalog once and return its disposition. The Models UI owns a separate bounded picker data resource so failure cannot erase the ordinary model inventory; Apply publishes through the resource's generation fence, and Most used reads usage only on explicit Apply. Stored mode survives availability drift, while complete/native custom orders await explicit replacement.

The shared atomic replacement publisher also identifies explicit Remote Workspace file writes as remote-workspace. Remote Workspace uses a separate, explicitly enabled server surface with structural WebSocket callbacks and awaited per-server cleanup; its contract owns that integration and records its isolated owner and support limits.

Listener startup diagnostics follow the runtime lifecycle contract; malformed optional listener blocks follow config loading.

Chat helper admission in src/server/responses/core.ts follows the deferred stored-main contract: only a needed Direct OpenAI helper claims stored main, after terminal vision, routed vision and search exclusions.

Combo editor routing quota

gui/src/pages/Combos.tsx supplies all non-Combo management catalog rows to internal model selectors, including hidden models. Catalog visibility does not restrict Combo member or JEV decision-model selection and selecting a hidden model does not change its external visibility. Disabled-provider, decision-service and recursion exclusions remain unchanged; the v2 workspace cache avoids reusing v1 visibility-filtered candidates.

src/server/management/provider-routes.ts projects routingQuota after each quota read using the current provider configuration and scoped inference evidence. The DTO carries only state, observation time and an exclusive validUntil; cached display reports and private credential bindings are unchanged. Known states expire within 30 minutes; exhaustion may expire earlier when the runtime predicate clears at a reset boundary, accounting for other windows and persistent USD blockers.

gui/src/combo-workspace-data.ts accepts only this projection for quota-based Save/Create blocking. Missing, invalid or expired evidence is unknown. gui/src/pages/Combos.tsx wakes at the rendered expiry, including a deadline crossed before effects run, rechecks activation and visibility, and refreshes quota with Combo data while preserving drafts. Each successful quota snapshot also advances the observation clock, so a retained older row cannot defer evaluation of a fresh row.

Optional Codex transport-hint suppression is scoped to canonical Responses client output; its defaults and exclusions are owned by Responses transport.

The custom Responses provider add/settings form exposes preserveResponsesReasoningContent as a default-off checkbox with a plaintext forwarding/token-use note. It saves only explicit choices, resets/discards drafts with the saved row, and hides the control for curated presets, OAuth/forward and OpenAI-operated destinations. gui/src/responses-reasoning-replay.ts owns the UI eligibility check; the Responses serializer continues to own wire policy. The provider editor field policy exposes showThinkingSummary as a boolean provider option; it controls Responses summary defaults without a dashboard rendering change. See Google provider. It also exposes hideRawReasoning as a boolean option, which suppresses only the raw reasoning channel and leaves provider-authored summaries visible; a dashboard save keeps an omitted value for the same provider name even after a destination move, while PATCH accepts a boolean or null to clear it. The display contract is owned by Chat compatibility.

The same editor policy accepts the per-model inlineThinkTagModels string list. Its opt-in format contract is owned by Chat compatibility.

Paginated and migration-capable history follows the authoritative writer contract; this document adds no independent writer guarantee.

Codex pool settings and their consumers follow the reset-first ordering contract, including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings. Codex account DTOs and cards expose the routing-plan exclusion separately from credential health; the plan exclusion contract also governs CLI projection. Private pool credential metadata follows the quota-history publication identity contract; credential-only and account DTO projections omit it.

Claude replay carries Go conversation affinity privately to final dispatch; preliminary route selection does not inject Go-only headers.

The connected browser shell reuses SESSION_UNAVAILABLE_EVENT and its shared-session readiness state. Terminal 401 recovery failure exposes pairing without a restart instruction; a newer session or aborted request cannot publish an unavailable notice. Successful pairing changes dashboard resource revalidation dependencies, so retained failed stores are explicitly refreshed. Dashboard reads distinguish authentication, permission denial, request failure, invalid payload and transport failure; protected data is hidden for authentication/denial, while other failed refreshes label retained data as stale.

Cline journal Undo eligibility reads both native configuration files through the paired integration IO adapter; the integration contract defines recovery.

The existing dashboard file-client maps include Cline CLI and reuse its committed color mark. The export panel labels its download as a settings/catalog bundle; all locales explain that Undo restores both original files.

Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The catalog effort contract records the V1/compaction exemptions and caller-preservation boundary. Pool quota producers and account commands follow the bounded raw-observation contract, separate from the latest display snapshot and capacity estimates. The account history response can include a low-confidence effective capacity estimate; usage normalization retains local-answer provenance so local responses cannot supply samples. Account quota surfaces use safe probe diagnostics separately from quota validity, credential health and routing authority.

Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The Responses transport owner documents this boundary, and native Chat also honors explicit per-axis capability declarations; an initial JEV null choice separately strips caller effort.

Live sideband admission and its bounded upstream handshake follow the runtime contract; the ordinary Responses WebSocket exchange remains separate. Dashboard overview polling observes authorization failures independently of stalled or rejected peer requests, cancels remaining child requests after a decisive result, and exposes resource-level deadline failures without rewriting them as authentication failures.

The explicit model-capability contract preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Exact model input declarations now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The Combos page sends non-image members as the request-only visionSidecarTargets field on PUT /api/combos whenever the Image switch is enabled, so text-only enrollment needs no manual provider edit. The GUI classifies members from the authoritative visionSidecarConsumer flag served on /api/models rows, so a member covered only by legacy noVisionModels keeps its sidecar classification after reload. Members whose modalities are unknown or have no text input keep the switch unavailable and are named in the hint; a member later removed from the combo keeps its provider declaration until cleared in the provider editor.

The raw provider editor round-trips autoReviewModel and autoReviewModelOverrides through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical openai rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly selectedModels; POST keeps the exact-key comparison. Canonical openai still rejects allowPrivateNetwork, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See reviewer projection. Stored Direct substitution follows the credential identity contract: both synchronous and asynchronous materializers discard the caller account header before applying the stored credential; ordinary native Direct passthrough is unchanged.

Shared response-log retention and native SSE inspection pacing follow the bounded inspection contract; other subsystem behavior remains unchanged.

Native steering retains fixed phase deadlines and reconciled replay output; see the steering stability contract.

Native steering generation overrides, explicit public-API eligibility and the consent-gated wire probe follow the shared control contract; this owner does not change routing or execute diagnostic tools.

compactionRouting is a persisted configuration setting. Its model and optional effort follow the Responses trigger contract. Dashboard Overview provides labeled model, trigger, source-scope and effort selectors and warns which provider (or combo targets, via GET /api/combos) receives the full conversation. The source-scope picker edits sourceModels as exact model or provider/* selectors, keeps saved selectors missing from the catalog visible, and refuses an empty selection because the schema would drop the override. The model checklist is searchable and renders at most 300 matches without dropping hidden selections. GET /api/settings returns the override or null; PUT /api/settings accepts a validated object or null to clear it; a failed save restores live settings and deletion provenance while the dashboard keeps the draft for retry.

src/server/gui-static.ts serves the dashboard from gui/dist, with OPENCODEX_GUI_DIST taking priority. A standalone binary resolves the copied directory beside ocx, then the desktop bundle's resource directory (../Resources on macOS, ../lib/OpenCodex on Linux), so a runtime the desktop shell did not start still serves it. Runtime package metadata comes from the bundled src/lib/package-version.ts manifest import so compiled binaries do not read a source-tree package.json.

Quota-reset notifications

src/quota/ implements the opt-in quotaResetNotify section. With the section absent, or enabled without a sink, no reset is detected or delivered and no new baseline is created. Cleanup can still write when an existing baseline is forgotten: src/quota/reset-observer.ts runs forgetQuotaBaseline without a sink. Startup still arms one unref'd poller interval (src/server/background-lifecycle.ts) whose tick is a no-op until the section is enabled, so enabling it takes effect without a restart.

  • src/quota/window-mapping.ts maps provider and Codex quota snapshots to one neutral window list, and src/quota/reset-detector.ts compares two consecutive observations of a window and emits at most one reset event, classified as scheduled or surprise.
  • src/quota/reset-observer.ts is the only place a snapshot becomes a delivered notification. It never throws into the quota write that triggered it. src/codex/quota.ts and src/providers/quota.ts reach it only through a lazy import, because both are statically reachable from src/server/responses/core.ts and a static edge would load the subsystem into every install.
  • src/quota/reset-seen-store.ts keeps a durable already-notified ledger. The observer claims a reset there before dispatch, so a reset is attempted at most once, across restarts and racing observers; a failed delivery is not retried. src/quota/reset-sinks.ts delivers to a webhook or a local command; each sink is best-effort and isolated from the other.
  • src/quota/reset-poller.ts is an opt-in idle refresh (default 15 minutes, floor 10). Without it, quota reports are fetched only when the dashboard or CLI asks, and an overnight reset goes unobserved. src/quota/reset-activation.ts installs the sink independently of the poller, so pollSeconds: 0 observes live traffic only.

Usage history and model identity

src/server/request-log-terminal-status.ts owns the diagnostics shared by request contexts and final rows in src/server/request-log.ts. It provisionally maps bare upstream error events with recognized rate-limit classes to 429 and explicit overload to 503; an unknown code takes the 5xx its Codex WebSocket wrapper states (top-level integer status or status_code, not both, read by codexWsWrapperStatus in src/server/responses/codex-ws-wire.ts, as the native client does), otherwise it remains 502; a wrapper 4xx is not adopted because it would move quota or credential routing, and policy refusals retain precedence (#6740). Genuine completed, failed, or incomplete terminals supersede provisional status before account-health recording. Final rows keep the first upstreamErrorType (closed provider-class vocabulary), upstreamErrorCode (nested envelopes, or a known bare class) and upstreamRequestId within the 128-character token allowlist, and drop any value that repeats a credential: src/server/configured-credentials.ts reads every configured provider key, pool key, header value and OAuth-store token when checking (observe-only and quiet; rebuilt when the bytes of config.json or auth.json change (every check reads both stores and compares a content digest, never inode/size/mtime, so a same-length rewrite inside one timestamp tick or a timestamp-preserving writer is still seen), when a keychain secret is replaced in this process, or when a referenced env value changes; an incomplete set is retried after 30 s; upstreamDiagnosticLogFields takes one snapshot per projected row, only once a field is populated), and src/lib/outbound-credential-registry.ts records final HTTP (sendWithConnectionPolicy) and Codex WebSocket headers in a strict tier (credential-named, any length) and a best-effort tier (8+ characters, never disabling), split into components. The check runs at capture, in warnings and at projection; an incomplete configured source (until it recovers) or an evicted strict value (until restart) makes all three fields fail closed; the event discriminator is not an error class. It preserves upstream servedModel independently of route-derived resolvedModel; src/usage/log.ts persists it with wireModel. The Logs model column and detail view compare servedModel with wireModel ?? model. recordObservedServedModel in src/usage/log.ts refuses the client's own selector echoed in response.model (Anthropic routes keep anthropic/<model> there), and modelIdentityLogFields drops the same echo from older rows on read, so neither draws a false reroute. An absent upstream model stays absent; the tooltip retains all available model identities. Historical Codex openai, chatgpt and openai-multi main labels collapse for reporting; configured provider names ending in -main remain separate. Native Messages request and attempt requestedEffort labels follow the native Messages logging contract.

Rows also carry the observed protocol path (protocolTrace), persisted in usage.jsonl and re-validated on read; the Logs list shows it as a text badge, the detail dialog as a section, and src/server/request-log-filter.ts owns the /api/logs query filters including protocolMode; its single-pass query applies provider, conversation, model, account, protocol mode and status before tail, then reports the pre-pagination count alongside the offset/limit page. src/server/request-log-filter.ts accepts the full provider/model label shown by ocx logs and bare model IDs, including IDs containing slashes. Qualified failover matches bind provider/model from the same attempt; matching occurs before the row cap and pagination. Protocol Paths owns its derivation.

Conversation searches in the dashboard, /api/logs and ocx logs filter --conversation accept codex://threads/<id> links up to 512 characters, including query/fragment metadata, trailing slashes and surrounding whitespace. The server matcher in src/server/request-log-conversation.ts and its browser mirror in gui/src/log-conversation-id.ts try both the extracted single-segment id and the original trimmed query, preserving raw-id and whole-literal-URI digest matching. The prefix is case-insensitive; the id retains its case. Malformed or longer links remain plain queries subject to the existing 4096-character digest-input bound. Persistence is unchanged. Rows may also carry genStartMs and lastOutputMs, an observed request-relative pair recorded by src/server/request-log-generation-window.ts from the Responses and Anthropic SSE taps: the first output item or content block (reasoning included) to the last output delta. They are proxy observation times, not provider-internal token timing, and src/usage/log.ts keeps them only as a pair of nonnegative finite values with lastOutputMs >= genStartMs. decodeTokPerSecondResult in src/server/management/shared.ts prefers that window when the pair is valid and otherwise falls back to the post-TTFT window (durationMs - firstOutputMs); both keep the one-second minimum, the ttft_missing / decode_window_too_short unavailable reasons and estimated: true, so a short measured window is still an estimate rather than a provider decode benchmark. The end-to-end tok/s column is unchanged, attempts carry no generation window of their own, and the request-history view does not present one. Successful decode DTOs additionally expose timingBasis: "generation-window" | "legacy-post-visible-output", derived at read time only. Logs labels the method “Generation window” or “After visible output” in the list and attempt table and uses “Output rate during generation (est.)” or “Output rate after first visible output (est.)” in request details. Missing or unfamiliar bases show “Timing unknown” and “Output rate (est.; timing method unknown)” in details rather than inferring a method. The detail grid shows a full-width muted caption explaining the method only when the decode rate is available. Both numerators include reported reasoning output tokens, so the two estimates are not directly comparable. Historical JSONL and end-to-end rates are unchanged.

Request-history selectors longer than 130 characters persist as a prefix plus a digest of the complete selector; exact-match filtering uses the same idempotent encoding. The derived index rebuilds when its projection version changes and encodes older raw-selector rows from canonical JSONL so exact filters still find them. CLI access-key usage is unavailable without an ISO-8601 UTC attribution timestamp, rather than a measured zero or never-used key.

Kimi Coding K3/K3.1 Pro (and moonshot kimi-k3.1-pro*) price rows are API-reference estimates at the 5-minute cache-write rate, not Code Plan billing or quota. The three Coding presets have explicit price namespaces. The retargeted kimi-for-coding alias stays unpriced until a verified K2.8 price or a user modelCosts override exists; the retired K2.7 mapping is not reused. Request, attempt and combo estimates remain unknown rather than zero or partial totals. User prices take precedence and existing unknown-price and unknown-cap policies still govern cost evidence and routing.

The Models app-server status read is owned by its API-base/restart effect, not the picker tab; switching to Combos preserves a pending read and its existing stale-state banner.

Anthropic Fast pricing applies a 2x list-price multiplier only when the response confirms usage.speed: "fast"; an absent echo or standard-speed downgrade retains standard pricing. tests/usage/usage-anthropic-fast-pricing.test.ts pins that distinction. The request-metrics recovery label anthropic-fast-downgrade projects to fast_downgrade, separate from reasoning-effort effort_downgrade.

Cursor Claude Fast pricing applies the published Fast tuples to Opus 4.8, Opus 5 and Opus 5.5. Explicit -fast model IDs use the Fast tuple directly; a Cursor variant tier outcome applies the same 2x multiplier to a base model estimate. Opus 4.7 remains standard-priced because its upstream Fast mode is unavailable. Configured model prices retain precedence over compiled rows. Main-account protection copy uses the effective short/long thresholds from the server projection. The main account card and setting render an amber advisory for possible usage outside opencodex, including when the admission policy is off; warning detection and expiry belong to main-account observations.