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
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ ocx login anthropic
실제 사용량을 다시 확인하며, 조회 실패나 잘못된 수치는 차단을 풀지 않습니다.
일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다.

새로운 유효한 WHAM 사용량 응답 한 건에서 1차 창의 기간이 **24시간 이상**으로 명시되고,
2차·3차 창이 명시적 `null`이거나 그 기간도 24시간 이상으로 명시되면 이전 5h 수치를 대체합니다.
파서의 단기·장기 구분 기준을 따르므로 주간·월간뿐 아니라 하루짜리 창도 해당합니다.
현재 창에는 동일한 99% 기준을 적용합니다. 이 판단은 응답 한 건의 정보에 의존하며 연속 관측을
요구하지 않습니다. 2차·3차 필드가 생략되었거나, 1차 창의 기간을 모르거나, 응답 헤더만 일부
도착한 경우에는 이전 차단을 해제하지 않습니다.

저장되는 옵션은 OpenCodex의 `config.json`에 있는 `"codexMainAccountHardLock": true`이며,
기본값은 꺼짐입니다. 식별된 메인 계정의 새 요청을 막는 기능이지 마지막 1%를 예약하는 기능은
아닙니다. 진행 중 요청, 식별되지 않은 키링 계정, 프록시 밖 요청은 사용량을 더 쓸 수 있습니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ not erase an already measured blocking tuple. A predicted reset time alone does
While blocked, the existing once-per-minute background cycle checks fresh owned usage; failed or
invalid readings retain the block. Other pause, reauthentication, and upstream limits remain independent.

Protection treats one fresh valid WHAM usage response as a replacement for the old 5h reading when
its primary window explicitly lasts **at least 24 hours** and secondary/tertiary windows are explicitly `null`
or also explicitly last at least 24 hours. This follows the parser's short/long boundary, so a
one-day window qualifies as well as weekly/monthly windows. The current window still uses the same
99% threshold. This relies on the single reported snapshot; repeated observations are not required.
Omitted secondary/tertiary fields, an unknown primary duration, or partial response headers cannot clear a previous block.

The persisted option is `"codexMainAccountHardLock": true` in OpenCodex's `config.json`; it is off
by default. This protects new requests using the identified main account, not the last 1% itself:
already-running requests, unmatched caller-owned keyring credentials, and traffic outside the
Expand Down
50 changes: 43 additions & 7 deletions src/codex/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type QuotaDiskFile = {
};

type MainPolicyQuota = { identityKey: string; quota: StoredAccountQuota };
/** Fresh WHAM topology proof is consumed by the merge, never retained in a cache or DTO. */
type MainPolicyQuotaObservation = Omit<StoredAccountQuota, "updatedAt"> & { shortWindowAbsent?: true };
let mainPolicyQuota: MainPolicyQuota | null = null;
let diskHydrated = false;
let persistTimer: ReturnType<typeof setTimeout> | null = null;
Expand Down Expand Up @@ -198,6 +200,12 @@ function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): bo
&& seconds >= MONTHLY_WINDOW_MIN_SECONDS;
}

/** Same 24h short/long boundary as the parser; this includes a declared one-day window. */
function isExplicitLongWindow(window: WhamUsageWindow | null | undefined): boolean {
const seconds = window?.limit_window_seconds;
return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= WEEKLY_WINDOW_MIN_SECONDS;
}

function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean {
const minutes = windowMinutes_(windowMinutes);
return minutes !== undefined && minutes >= MONTHLY_WINDOW_MIN_MINUTES;
Expand Down Expand Up @@ -266,12 +274,17 @@ function snapshotHasCustom(quota: Omit<StoredAccountQuota, "updatedAt">): boolea
function snapshotHasUsage(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota);
}
/**
* Publish parsed display quota and separately validated main-policy evidence after writer checks.
* A null policy observation retains only the matching main identity's previous evidence;
* transient replacement markers are consumed during merging and never enter stored snapshots.
*/
export function setAccountQuotaFromParsed(
accountId: string,
quota: Omit<StoredAccountQuota, "updatedAt"> | null,
writerGeneration = captureConfigGeneration(),
mainWriter?: MainQuotaWriter,
policyQuota: Omit<StoredAccountQuota, "updatedAt"> | null = quota,
policyQuota: MainPolicyQuotaObservation | null = quota,
historyEvidence?: QuotaObservationEvidence,
): void {
quota = withoutRetiredCodexQuota(quota);
Expand Down Expand Up @@ -312,9 +325,13 @@ export function setAccountQuotaFromParsed(
}
}

/** One partial-window merge contract for legacy quota and identity-bound policy evidence. */
/**
* Merge a partial observation into the legacy or identity-bound policy snapshot.
* Policy mode retains omitted blocking short usage unless this observation authorizes replacement;
* the returned snapshot contains quota fields only, without the transient replacement marker.
*/
function mergeAccountQuota(
quota: Omit<StoredAccountQuota, "updatedAt">,
quota: MainPolicyQuotaObservation,
existing: StoredAccountQuota | undefined,
updatedAt: number,
policyEvidence = false,
Expand Down Expand Up @@ -376,7 +393,7 @@ function mergeAccountQuota(
}
if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt;
if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds;
} else {
} else if (!policyEvidence || quota.shortWindowAbsent !== true) {
// Unknown usage is not a lower reading. Retain the entire known tuple: pairing
// its percentage with new metadata would silently extend or shorten its reset.
// An elapsed reset is the exception. It describes a window that has already rolled over,
Expand Down Expand Up @@ -791,13 +808,32 @@ function filterMainPolicyMonthlyQuota(
return hasKnownQuotaValue(filtered) || filtered.resetCredits !== undefined ? filtered : null;
}

/** Ordinary main policy rejects an entire message containing any invalid numeric window. */
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
/**
* Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages.
* Mark a valid primary of at least 24h as replacement evidence only when both other windows
* are explicitly null or at least 24h. A null result supplies no usable policy observation.
*/
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null {
const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null;
return filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type));
const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type));
const [primary, secondary, tertiary] = windows;
// WHAM explicitly reports absent windows as null; omissions cannot prove replacement.
// Policy trusts one complete snapshot only when every non-null window is >=24h.
// Headers never supply this proof, and reset time alone still cannot release a block.
if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary)
&& (secondary === null || isExplicitLongWindow(secondary))
&& (tertiary === null || isExplicitLongWindow(tertiary))) {
return { ...quota, shortWindowAbsent: true };
}
return quota;
}

/**
* Normalize WHAM windows into the display snapshot, preserving declared short-window shape.
* Finite percentages are clamped for compatibility; policy callers must validate raw readings
* separately. Return null when neither a quota value/window nor reset credits are available.
*/
export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number"
? data.rate_limit_reset_credits.available_count
Expand Down
19 changes: 16 additions & 3 deletions structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,10 @@ This stops partial weekly/Spark or credits-only refreshes from renewing obsolete
5h rows through the cache-wide `updatedAt` timestamp. Plan labels do not suppress real windows.

The separately retained main-policy snapshot preserves omitted blocking short evidence even after
its reset clock passes. Credits-only, weekly-only, and metadata-only updates cannot remove an
its reset clock passes. Credits-only, partial weekly-only, and metadata-only updates cannot remove an
existing blocking short usage reading or release its hard lock; a fresh short reading can replace
it. Expired non-blocking short evidence is dropped, so it cannot take priority over a fresh blocking
weekly reading.
it. A validated long-primary WHAM snapshot can also retire the short tuple as described below.
Expired non-blocking short evidence is dropped, so it cannot take priority over a fresh blocking weekly reading.

The Codex writer explicitly asks `src/quota/reset-observer.ts` to retain an absent short window
in `src/quota/reset-seen-store.ts`, with its original observation time. Detection compares only
Expand All @@ -301,6 +301,19 @@ release the block. Policy validation precedes legacy clamping. Supplementary mon
become the fallback governing window without a monthly-only plan or explicit primary-monthly evidence.
Previously unobserved usage is unknown, not fabricated headroom.

A single fresh valid WHAM response with an explicitly long primary window can replace an obsolete
short-window tuple when secondary and tertiary windows are explicitly null or also explicitly long.
Long means **at least 24 hours**, matching the parser's short/long discriminator; a one-day primary
qualifies, not only a seven-day or monthly window. The policy trusts that one reported topology;
it does not require repeated observations or independently confirm upstream window completeness.
Omitted secondary/tertiary fields, an unknown primary duration, partial headers, or invalid usage cannot prove that the
short window disappeared. Replacement proof belongs only to that observation and is never persisted;
the resulting weekly/monthly window still blocks at 99%. This prevents old short-window exhaustion
from surviving indefinitely on a now weekly/monthly account. Coverage lives in
`tests/codex-integration/main-quota-evidence-validation.test.ts`,
`tests/codex-integration/main-quota-provenance.test.ts`, and
`tests/codex-integration/main-account-hard-lock-recovery.test.ts`.

The policy reads a separately retained identity-tagged quota snapshot, so the legacy rotation
cache's six-hour expiry does not silently release a known block. A confirmed account transition
invalidates old evidence. Request-owned bearers are matched only against a credential and effective
Expand Down
20 changes: 20 additions & 0 deletions tests/codex-integration/main-account-hard-lock-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ let previousHome: string | undefined;
let previousCodexHome: string | undefined;
let previousFetch: typeof fetch;

/** Build the minimal proxy configuration with main-account hard-lock recovery enabled. */
function config(): OcxConfig {
return { port: 10100, defaultProvider: "openai", providers: {}, codexMainAccountHardLock: true };
}

/** Encode synthetic account and expiry claims for the fixture; this is not a signed credential. */
function bearer(expired = false): string {
const payload = Buffer.from(JSON.stringify({
exp: Math.floor(Date.now() / 1000) + (expired ? -120 : 86_400),
Expand All @@ -42,13 +44,15 @@ function bearer(expired = false): string {
return `header.${payload}.signature`;
}

/** Write fixture credentials into the isolated home and reconcile the active main identity. */
function writeMain(expired = false): void {
writeFileSync(join(home, "auth.json"), JSON.stringify({ tokens: {
access_token: bearer(expired), refresh_token: "fixture-refresh", account_id: accountId,
} }));
reconcileMainCodexAccountRuntimeState();
}

/** Seed a 99% short-window block for the observed fixture identity, even though its reset elapsed. */
function block(): void {
const writer = captureMainQuotaWriter(accountId);
if (!writer) throw new Error("Fixture identity must be observed");
Expand All @@ -61,6 +65,10 @@ function usage(percent = 0): Response {
} });
}

/**
* Stub recovery HTTP calls, requiring a known metadata/token URL and an active native-main drain.
* Return the captured URL list so tests can verify the requests made by background recovery.
*/
function fetchWith(handler: (url: string, init?: RequestInit) => Promise<Response>) {
const calls: string[] = [];
globalThis.fetch = Object.assign(async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
Expand Down Expand Up @@ -123,6 +131,18 @@ afterEach(async () => {
});

describe("main hard-lock background recovery", () => {
test("owned metadata recovery replaces an obsolete short block with the current weekly window", async () => {
const calls = fetchWith(async () => Response.json({ plan_type: "pro", rate_limit: {
primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, tertiary_window: null,
} }));
await runMainAccountHardLockRecovery(config());
expect(calls).toEqual([whamUrl]);
expect(getMainAccountHardLockStatus(config())).toEqual({ enabled: true, state: "ready" });
expect(getMainPolicyQuota()?.shortPercent).toBeUndefined();
expect(getMainPolicyQuota()?.weeklyPercent).toBe(35);
expect(getNativeMainProfileRequestCount()).toBe(0);
});

test("existing sweep hook forces fresh WHAM past cache/reset without adding a timer", async () => {
let percent = 99;
const calls = fetchWith(async () => usage(percent));
Expand Down
Loading
Loading