Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,8 @@ threshold. The Desktop/main account keeps its separate 98% hard lock.
Set `codexPool.lowQuotaProtection` in configuration to pause accounts, record a log-and-API
alert, or both; see [routing configuration](/reference/configuration/routing/#codex-pool-low-quota-protection).
A pause takes effect for the next selection immediately, while saving it to disk is deferred.
Pool quota responses can authorize a pause only with valid usage and a captured credential
that is still current. A response without that proof may refresh displayed usage without pausing an account.
Check this server’s authenticated `GET /api/codex-auth/low-quota-events` history for `logged`
alerts or save failures. Manual resume remains in force for the current quota episode. The
default alert reaches only the log and API; it does not produce a desktop or OS notification.
Expand Down
3 changes: 2 additions & 1 deletion src/codex/auth-api/pool-quota-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ export async function commitPoolQuotaResponse(
}
const validPolicyObservation = isValidWhamHistoryObservation(data);
setAccountQuotaFromParsed(accountId, quota, writerGeneration, undefined, validPolicyObservation ? quota : null,
ctx.poolWriter && validPolicyObservation ? { writer: ctx.poolWriter, observedAt, source: "wham", raw: quota } : undefined);
ctx.poolWriter && validPolicyObservation ? { writer: ctx.poolWriter, observedAt, source: "wham", raw: quota } : undefined,
true);
return {
quota: getAccountQuota(accountId),
needsReauth: false,
Expand Down
2 changes: 1 addition & 1 deletion src/codex/quota-auto-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async function warmAccount(config: OcxConfig, accountId: string): Promise<void |
try {
await warmCodexAccount({ ...token, onCompleted: headers => {
if (isCodexAccountGenerationLive(accountId, token.generation)) {
applyAccountQuotaFromUpstreamHeaders(accountId, headers, writerGeneration, undefined, { poolWriter });
applyAccountQuotaFromUpstreamHeaders(accountId, headers, writerGeneration, undefined, { poolWriter, poolResponse: true });
}
} });
} catch (error) {
Expand Down
16 changes: 12 additions & 4 deletions src/codex/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ export function setAccountQuotaFromParsed(
mainWriter?: MainQuotaWriter,
policyQuota: MainPolicyQuotaObservation | null = accountId === MAIN_CODEX_ACCOUNT_ID ? quota : null,
historyEvidence?: QuotaObservationEvidence,
// Pool responses carry their credential provenance separately from the optional history
// writer: when capture failed before dispatch, an absent writer must fail closed instead
// of reading as a writer-free legacy/login observation.
poolRequest = false,
): void {
quota = withoutRetiredCodexQuota(quota);
policyQuota = withoutRetiredCodexQuota(policyQuota);
Expand All @@ -298,7 +302,10 @@ export function setAccountQuotaFromParsed(
hydrateAccountQuotasFromDisk();
const legacyExisting = accountQuota.get(accountId);
const updatedAt = Date.now();
if (historyEvidence && historyEvidence.writer.accountId === accountId && isPoolQuotaWriterLive(historyEvidence.writer)) {
const livePoolEvidence = historyEvidence
? historyEvidence.writer.accountId === accountId && isPoolQuotaWriterLive(historyEvidence.writer)
: !poolRequest;
if (historyEvidence && livePoolEvidence) {
quotaHistory.append(historyEvidence.writer, { observedAt: historyEvidence.observedAt, source: historyEvidence.source,
credentialGeneration: historyEvidence.writer.credentialGeneration, windows: historyWindows(historyEvidence.raw),
}, updatedAt);
Expand All @@ -323,7 +330,7 @@ export function setAccountQuotaFromParsed(
schedulePersistAccountQuotas();
// Credits carry the previous usage tuple; they must not refresh its observation clock.
if (!(quota.resetCredits !== undefined && !snapshotHasUsage(quota))) {
if (!isMain && policyQuota) observeCodexLowQuota(accountId, policyQuota);
if (!isMain && policyQuota && livePoolEvidence) observeCodexLowQuota(accountId, policyQuota);
notifyCodexQuotaSnapshot(accountId, next);
}
}
Expand Down Expand Up @@ -581,7 +588,7 @@ export function applyAccountQuotaFromUpstreamHeaders(
headers: Headers,
writerGeneration = captureConfigGeneration(),
mainWriter?: MainQuotaWriter,
options?: { modelId?: string; poolWriter?: PoolQuotaWriter },
options?: { modelId?: string; poolWriter?: PoolQuotaWriter; poolResponse?: boolean },
): void {
const quota = parseUpstreamQuotaHeaders(headers, options);
if (!quota) return;
Expand All @@ -591,7 +598,8 @@ export function applyAccountQuotaFromUpstreamHeaders(
const validHistory = !["x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent"]
.some(name => isInvalidPolicyUsagePercent(headers.get(name)));
setAccountQuotaFromParsed(accountId, quota, writerGeneration, mainWriter, policyQuota,
options?.poolWriter && validHistory ? { writer: options.poolWriter, observedAt: Date.now(), source: "response-header", raw: quota } : undefined);
options?.poolWriter && validHistory ? { writer: options.poolWriter, observedAt: Date.now(), source: "response-header", raw: quota } : undefined,
options?.poolResponse === true);
}

export function updateAccountQuota(
Expand Down
5 changes: 3 additions & 2 deletions src/server/responses/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,8 @@ export async function handleResponsesCompact(
upstream.headers,
authCtx.writerGeneration,
authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
{ modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined },
{ modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined,
poolResponse: authCtx.kind === "pool" },
);
}
recordCompactPoolOutcome(authCtx, upstream.status, {
Expand Down Expand Up @@ -1328,7 +1329,7 @@ export async function handleResponsesCompact(
if (outcomeCtx.kind === "pool") {
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/quota");
applyAccountQuotaFromUpstreamHeaders(outcomeCtx.accountId, upstream.headers, outcomeCtx.writerGeneration,
undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter });
undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter, poolResponse: true });
}
const retryAfter = upstream.headers.get("retry-after");
const resetAt = [
Expand Down
6 changes: 4 additions & 2 deletions src/server/responses/core-codex-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxPro
const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
return headers => {
if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined });
applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined,
poolResponse: authCtx.kind === "pool" });
};
}

Expand Down Expand Up @@ -717,7 +718,8 @@ export async function retryCodexPoolOnAlternateAccount(
firstResponse.headers,
firstAuthCtx.writerGeneration,
firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
{ modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined },
{ modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined,
poolResponse: firstAuthCtx.kind === "pool" },
);
}
const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
Expand Down
3 changes: 2 additions & 1 deletion src/server/responses/passthrough-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ export async function deliverPassthroughResponse(
if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
applyAccountQuotaFromUpstreamHeaders(admissionState.authCtx.accountId, upstreamResponse.headers,
admissionState.authCtx.writerGeneration, admissionState.authCtx.kind === "main-pool" ? admissionState.authCtx.mainQuotaWriter : undefined,
{ modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined });
{ modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined,
poolResponse: admissionState.authCtx.kind === "pool" });
}
if (terminalBodyWillRecord) {
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
Expand Down
2 changes: 1 addition & 1 deletion structure/providers/openai-accounts.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ The credits module beside `src/codex/quota-types.ts` retains validated WHAM cred

## Low-quota protection

`src/codex/quota.ts` sends accepted usage observations through `src/codex/low-quota-observer.ts`; credits-only updates never replay carried usage into protection, and pool WHAM/header observations reach the policy only after raw percentages pass validation; clamped display bars cannot authorize a pause. The pool-account-only [configuration policy](../config.md#codex-pool-low-quota-protection) pauses live selection immediately, defers a bounded config save, and deduplicates account/window notices. Manual resume is respected across every qualifying window already active for that account until recovery or a new reset episode. Native main keeps its separate 98% hard lock. Each server registration owns a bounded status ledger; its authenticated management route exposes only its own account ids. The default alert is a log line plus a `logged` event, with no OS notification.
`src/codex/quota.ts` sends accepted usage observations through `src/codex/low-quota-observer.ts`; credits-only updates never replay carried usage into protection, and pool WHAM/header observations reach the policy only after raw percentages pass validation and the captured credential writer is still live; clamped display bars and responses from retired credentials cannot authorize a pause. The pool-account-only [configuration policy](../config.md#codex-pool-low-quota-protection) pauses live selection immediately, defers a bounded config save, and deduplicates account/window notices. Manual resume is respected across every qualifying window already active for that account until recovery or a new reset episode. Native main keeps its separate 98% hard lock. Each server registration owns a bounded status ledger; its authenticated management route exposes only its own account ids. The default alert is a log line plus a `logged` event, with no OS notification. A pool response that could not capture a writer at dispatch (credential already replaced, or the store unreadable) fails closed the same way: its snapshot still refreshes the display cache, but it never authorizes a policy pause. Writer-free legacy and login paths are explicitly distinct from that pool contract and keep their existing observation behavior.

## Bounded pool quota observations

Expand Down
Loading
Loading