Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs-site/src/content/docs/getting-started/how-it-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ account before the request is forwarded upstream. The rule is intentionally spli
coalesces simultaneous windows into one request, and durably persists both reset timestamps to
prevent duplicate work after restarts. Paused accounts and accounts
requiring reauthentication are skipped. Activation captures successful response quota headers;
opted-in idle accounts also refresh stale quota metadata at most once every five minutes,
without needing an open dashboard. Observed reset boundaries are retained across restarts
known reset times are checked locally each minute without periodic quota queries, even when
the cached usage is old or the proxy restarts. Only missing reset times need a metadata query
after the five-minute freshness guard. Unresolved discovery and failed activations retry after
5, 10, 20, 40, then at most every 60 minutes; these retry delays reset on proxy restart.
Successful response headers seed the next window without an extra query when available.
Dashboard refreshes and optional reset-notification polling remain independent.
Observed reset boundaries are retained across restarts
until completed, so a moving idle-window timestamp cannot erase a pending activation.
Metadata refresh uses the existing bounded authentication recovery; an inference 401 marks
the rejected credential for reauthentication instead of repeatedly spending retries on it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ Codex 使用 OpenAI **Responses API**。opencodex 接收通过 HTTP 与 Server-S
已报告的 5 小时及每周窗口;新添加账号不会自动启用。在 Pool 模式下,窗口到期后会通过对应账号
发送最小化、不保存的请求,并消耗少量额度;同时到期的窗口合并为一次请求。暂停、需要重新认证
的账号会被跳过,主账号硬锁限制也会得到遵守。成功响应的额度头会更新缓存;已启用且符合条件的
空闲账号还会每隔至少 5 分钟刷新过期的额度元数据,无需保持仪表盘打开。已观察到的到期时间会保留
空闲账号已有重置时间时,每分钟仅在本地检查是否到期,不会因缓存过期或代理重启而定期查询额度。
只有缺少重置时间时,才在五分钟新鲜度保护后补查。持续缺失信息或激活失败时,重试间隔依次为
5、10、20、40、60 分钟,并以 60 分钟封顶;重试间隔在代理重启后重新计算。
成功响应头提供下一轮时间时无需额外查询。仪表盘刷新及可选的重置通知轮询仍独立运行。
已观察到的到期时间会保留
至激活完成,重启或后续查询的时间变化不会丢失待处理窗口。元数据查询复用现有的有次数限制的认证
恢复逻辑;推理请求返回 401 时,被拒绝的凭据会标记为需要重新认证。失败日志仅记录不透明账号标签
和安全的状态原因。该功能独立于为传入请求选择账号的路由逻辑。
Expand Down
6 changes: 4 additions & 2 deletions src/codex/quota-auto-refresh-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
/** Completed/due markers use epoch milliseconds; persisted legacy markers may use seconds. */
export type CodexQuotaAutoRefreshWindows = { fiveHour?: number; weekly?: number };

export type CodexQuotaRetry = { after: number; delay: number };

export const completedByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
export const retryAfterByAccount = new Map<string, number>();
export const retryAfterByAccount = new Map<string, CodexQuotaRetry>();
export const scheduledByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
export const quotaRefreshAfterByAccount = new Map<string, number>();
export const quotaRefreshAfterByAccount = new Map<string, CodexQuotaRetry>();

/** Drop every activation record when its account is removed. */
export function forgetCodexQuotaAutoRefreshAccount(accountId: string): void {
Expand Down
37 changes: 29 additions & 8 deletions src/codex/quota-auto-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./
import {
completedByAccount, retryAfterByAccount, scheduledByAccount, quotaRefreshAfterByAccount,
resetCodexQuotaAutoRefreshStateForTests,
type CodexQuotaAutoRefreshWindows,
type CodexQuotaAutoRefreshWindows, type CodexQuotaRetry,
} from "./quota-auto-refresh-state";
export type { CodexQuotaAutoRefreshWindows } from "./quota-auto-refresh-state";
export { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";

export const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
const RETRY_MS = 5 * 60_000;
const MAX_RETRY_MS = 60 * 60_000;
const CONCURRENCY = 4;

export interface CodexQuotaAutoRefreshStatus {
Expand All @@ -51,6 +52,21 @@ export interface CodexQuotaAutoRefreshRunDeps {

let inFlight: Promise<void> | null = null;

/** Back off unsuccessful discovery/activation without adding another timer. */
function deferRetry(retries: Map<string, CodexQuotaRetry>, accountId: string, now: number): number {
const delay = Math.min((retries.get(accountId)?.delay ?? RETRY_MS / 2) * 2, MAX_RETRY_MS);
retries.set(accountId, { after: now + delay, delay });
return delay;
}

/** Every enabled window needs a retained, uncompleted deadline, not fresh usage percentages. */
function hasScheduledWindows(config: OcxConfig, accountId: string): boolean {
const setting = config.codexQuotaAutoRefresh?.[accountId];
const scheduled = scheduledByAccount.get(accountId);
return (!setting?.fiveHour || scheduled?.fiveHour !== undefined)
&& (!setting?.weekly || scheduled?.weekly !== undefined);
}

/** Report upstream window availability separately from persisted spending intent. */
export function codexQuotaAutoRefreshStatus(
config: OcxConfig,
Expand Down Expand Up @@ -309,14 +325,19 @@ export async function runCodexQuotaAutoRefresh(
// Capture before WHAM can move an idle window's reset into the future.
rememberWindows(config, accountId, quotaFor(accountId));
const quota = quotaFor(accountId);
if ((!quota || now - quota.updatedAt >= RETRY_MS)
&& (quotaRefreshAfterByAccount.get(accountId) ?? 0) <= now) {
quotaRefreshAfterByAccount.set(accountId, now + RETRY_MS);
try { await refresh(config, accountId); } catch { /* Retry metadata at the bounded cadence. */ }
// A known deadline remains actionable even when its usage snapshot is old.
// Only discover missing windows; never poll merely to keep percentages fresh.
if (hasScheduledWindows(config, accountId)) {
quotaRefreshAfterByAccount.delete(accountId);
} else if ((!quota || now - quota.updatedAt >= RETRY_MS)
&& (quotaRefreshAfterByAccount.get(accountId)?.after ?? 0) <= now) {
deferRetry(quotaRefreshAfterByAccount, accountId, now);
try { await refresh(config, accountId); } catch { /* Retry missing metadata with backoff. */ }
}
if (!eligible(accountId)) return;
rememberWindows(config, accountId, quotaFor(accountId));
if ((retryAfterByAccount.get(accountId) ?? 0) > now) return;
if (hasScheduledWindows(config, accountId)) quotaRefreshAfterByAccount.delete(accountId);
if ((retryAfterByAccount.get(accountId)?.after ?? 0) > now) return;
const windows = dueCodexQuotaAutoRefreshWindows(config, accountId, quotaFor(accountId), now);
if (!windows) return;
try {
Expand All @@ -327,11 +348,11 @@ export async function runCodexQuotaAutoRefresh(
persist(config, accountId, completed);
rememberWindows(config, accountId, quotaFor(accountId));
} catch (error) {
retryAfterByAccount.set(accountId, now + RETRY_MS);
const delay = deferRetry(retryAfterByAccount, accountId, now);
const account = config.codexAccounts?.find(candidate => candidate.id === accountId);
const label = account ? codexAccountLogLabel(account) : "main";
console.warn(`[codex-quota-auto-refresh] ${label}: ${codexWarmupFailureReason(error)}; ${
isAccountNeedsReauth(accountId) ? "reauthentication required" : "retry in five minutes"
isAccountNeedsReauth(accountId) ? "reauthentication required" : `retry in ${delay / 60_000} minutes`
}`);
}
}));
Expand Down
8 changes: 4 additions & 4 deletions structure/catalog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Model Catalog

Activation-owned metadata discovery no longer refreshes known deadlines merely because quota snapshots age. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).

Native result continuations and function-result injection follow [the mode-specific result and control contract](transports/streaming-health.md#experimental-native-function-result-injection); this surface does not infer upstream support or alter its defaults.
Explicit Codex CLI installation observation supplies no selected-runtime proof to catalog discovery or publication. See the [read-only observation contract](runtime.md#explicit-codex-cli-installation-observation).

Expand Down Expand Up @@ -593,8 +595,6 @@ Subagent account previews and live routing share the [priority failback](provide

Startup and explicit catalog synchronization in `src/codex/sync.ts` refresh the optional
`src/providers/reasoning-metadata.ts` effort snapshot for supported destinations before catalog
gathering. Each sync waits at most two seconds for a fresh or shared fetch, then continues with
the existing snapshot; the fetch retains its own abort deadline. Routed effort reads in
gathering. Each sync waits at most two seconds for a fresh or shared fetch, then continues with the existing snapshot; the fetch retains its own abort deadline. Routed effort reads in
`src/reasoning-effort.ts` use a snapshot immediately and request a best-effort background refresh
only when an existing snapshot answers with an expired ladder. Missing or corrupt snapshots do
not fetch on the request path; catalog sync owns their bootstrap.
only when an existing snapshot answers with an expired ladder. Missing or corrupt snapshots do not fetch on the request path; catalog sync owns their bootstrap.
2 changes: 2 additions & 0 deletions structure/codex-home.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Codex Home

Quota activation restores deadlines from OpenCodex settings; its retry backoff remains process-local. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).

Catalog HTTP acquisition follows the [proxy-routing contract](catalog.md#remote-catalog-http-proxy-routing).

A lock in the Codex credential store is governed by [descriptor identity and age](catalog.md#accounts-namespaces-and-pool-rotation), so the mere presence of its filename is neither acquisition nor release authority. Failed path-identity probes leave the lock for stale recovery and preserve the refresh callback outcome. Cooperating lock metadata changes serialize through the existing SQLite mutation transaction; release keeps the descriptor open through identity comparison and any unlink, then closes it. Failed metadata writes remove only a matching owned path after successful coordination; unknown identity, failed probes or unavailable coordination retain the path for stale recovery. Async refresh work holds no metadata transaction.
Expand Down
6 changes: 3 additions & 3 deletions structure/config.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Config Surface

Quota activation reuses the existing next-reset fields without adding a polling configuration key. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).

Native function-result injection follows [the separate opt-in control contract](transports/streaming-health.md#experimental-native-function-result-injection); this surface does not infer upstream support or alter its defaults.

Native steering follows [the shared WebSocket contract](transports/streaming-health.md#experimental-native-mid-turn-steering); this surface's defaults remain unchanged.
Expand Down Expand Up @@ -589,9 +591,7 @@ being treated as a text model by one and an image target by the other.
malformed persisted value is off. `src/config/schema/config-schema.ts` degrades a malformed hand edit
to absence so an optional monitoring typo cannot discard providers or credentials. The live-write
boundary runs `metricsExportConfigError` in `src/config/diagnostics.ts` before the degrading schema,
so wrong types and unknown nested fields are rejected rather than silently saved. Activation is read
when the server process creates its serve options and therefore requires restart; it adds no setting
to the live `/api/settings` mutation surface.
so wrong types and unknown nested fields are rejected rather than silently saved. Activation is read when the server process creates its serve options and therefore requires restart; it adds no setting to the live `/api/settings` mutation surface.

`apiSurfaces` and `protocols` on `src/types/config.ts` are parsed by `src/protocols/settings.ts` only; [Protocol Paths](data-planes/protocol-paths.md#settings) owns their schema handling, meaning and the one writer (`PATCH /api/protocols/settings`), including why closing Messages also writes `claudeCode.enabled` through `commitClaudeCodeBlock` (`src/claude/claude-code-block.ts`, the sentinel-stamping block writer every management route uses).

Expand Down
2 changes: 2 additions & 0 deletions structure/gui-and-management-api.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# GUI And Management API

Automatic activation retains its existing settings controls; dashboard quota queries remain independent. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).

The companion settings contract in `src/companion/` persists menu-bar and widget display
preferences, while `src/server/management/companion-routes.ts` exposes those settings and the
usage timeline assembled by `src/usage/timeline.ts` to local clients. Query, filter-echo and
Expand Down
2 changes: 2 additions & 0 deletions structure/ops/docs-and-release.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Docs And Release

The activation scheduling contract is covered by `tests/codex-integration/codex-quota-auto-refresh.test.ts`, including restart recovery and bounded retries. See the [quota activation contract](../providers/openai-tiers.md#public-provider-contract).

Automatic package-tree restart holds a releasable data-plane drain until its scheduled
service-home check succeeds. A veto releases that fence; a committed shutdown uses the
permanent drain latch.
Expand Down
13 changes: 9 additions & 4 deletions structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,17 @@ existing minimal non-stored warmup through that exact account once the timestamp
field-patches the completed timestamp. The next observed reset boundary is also retained in
`nextFiveHourResetAt` / `nextWeeklyResetAt` until completed; later idle-window metadata cannot
postpone it. Successful warmups publish quota headers under the captured credential/identity fence.
For opted-in accounts only, stale metadata is refreshed at most once per five minutes through
the existing WHAM recovery path, independently of dashboard traffic or reset notifications.
Known deadlines suppress activation-owned WHAM queries regardless of snapshot age, including
when only persisted deadlines survive a restart. Missing enabled-window deadlines use the existing
WHAM recovery path after the five-minute freshness guard; unresolved discovery backs off from
five minutes to an hour (5, 10, 20, 40, 60 minutes). Passive headers can satisfy discovery without
a query. Completed warmups seed the next deadlines from response headers; missing next-window
headers use the same discovery path. Retry delays are process-local; deadlines remain durable.
Dashboard queries and reset-notification polling are separate owners and retain their behavior.
Inference 401s quarantine the rejected credential; failures log an opaque label and safe reason.
Paused or reauthentication-required
accounts are skipped, simultaneous 5-hour/weekly resets share one warmup, transient failures retry
after five minutes, and account deletion removes its setting and completion markers.
accounts are skipped, simultaneous 5-hour/weekly resets share one warmup, transient activation failures
back off from five minutes to an hour, and account deletion removes settings and retry/completion state.
Main-account hard-lock also gates these billable warmups. A policy/identity skip changes neither
completion markers nor retry delay; quota reads remain available. Main refresh completes before
shared credential ownership, then prepared credentials and restrictions are rechecked. Lifecycle
Expand Down
6 changes: 3 additions & 3 deletions structure/runtime.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Runtime

The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).

## Resolved static model policy

`src/router.ts` attaches one frozen `ResolvedModelPolicy` to every `RouteResult`. Policy/combo
Expand Down Expand Up @@ -589,9 +591,7 @@ an unreadable current record is unknown, and a valid address is probed even when
PID is gone. Lease delegation is passed only to stop and recovery children, never package
manager children. A replacement refusal passes through owner-aware recovery: only the same CLI
owner revives the stopped runtime; foreign ownership stays transferred and unknown ownership
remains a reported recovery requirement. Dashboard restart delegates the lease token to its repair child. Direct
start holds the same lease through bind plus PID and runtime-address publication. If listener
rollback cannot prove the socket closed, the process retains its lease until exit.
remains a reported recovery requirement. Dashboard restart delegates the lease token to its repair child. Direct start holds the same lease through bind plus PID and runtime-address publication. If listener rollback cannot prove the socket closed, the process retains its lease until exit.
The registration is never deleted; `ocx service install` releases the marker only after the
registration succeeds.

Expand Down
2 changes: 2 additions & 0 deletions structure/subagents.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Subagents And Multi-Agent Surface

Subagent quota priming remains separate from automatic activation scheduling. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).

Native result continuations and function-result injection follow [the mode-specific result and control contract](transports/streaming-health.md#experimental-native-function-result-injection); this surface does not infer upstream support or alter its defaults.
Explicit Codex CLI installation observation does not attest the runtime used by a subagent or change agent selection. See the [read-only observation contract](runtime.md#explicit-codex-cli-installation-observation).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,13 @@ afterEach(async () => {
});

describe("quota auto-refresh native-main admission", () => {
test("stale metadata prepares an expired main token before WHAM and activation", async () => {
test.each([false, true])("expired main token is prepared before activation (missing deadline: %s)", async missingDeadline => {
const cfg = config();
writeMain(bearer(true));
const cached = getAccountQuota(MAIN);
if (!cached) throw new Error("Expected cached main quota");
cached.updatedAt = now - 300_000;
if (missingDeadline) delete cached.shortResetAt;
const fresh = bearer();
const calls = installFetch(async (url, init) => {
if (url === tokenUrl) {
Expand All @@ -157,7 +158,7 @@ describe("quota auto-refresh native-main admission", () => {
return completedResponse();
});
await runCodexQuotaAutoRefresh(cfg, now, { persistCompleted: recordMarkers });
expect(calls).toEqual([tokenUrl, whamUrl, responsesUrl]);
expect(calls).toEqual(missingDeadline ? [tokenUrl, whamUrl, responsesUrl] : [tokenUrl, responsesUrl]);
expect(isAccountNeedsReauth(MAIN)).toBe(false);
expect(cfg.codexQuotaAutoRefresh?.[MAIN]?.lastFiveHourResetAt).toBe(RESET_MILLISECONDS);
expect(getNativeMainProfileRequestCount()).toBe(0);
Expand Down
Loading
Loading