Repository navigation
Spark model-specific 5h header window is stored as account-level short quota on Pro accounts #4122
Description
Client or integration
OpenCodex dashboard
Area
Authentication and account pool
Summary
Two quota observation paths write into the same per-account snapshot and disagree about what a 5-hour window means.
- The WHAM path (parseUsageQuota, src/codex/quota.ts) treats a Pro account correctly: the main rate_limit.primary_window is weekly-only, and the GPT-5.3-Codex-Spark 5h/weekly windows from additional_rate_limits (metered_feature codex_bengalfox) are stored only in customWindows.
- The response-header path (parseUpstreamQuotaHeaders via applyAccountQuotaFromUpstreamHeaders, called from quota-auto-refresh.ts, responses/core.ts, responses/compact.ts) has no model context. When the routed model is Spark, the upstream response headers describe the model-specific 5h limit as the primary window, and the parser stores it as the account-level shortPercent / shortWindowSeconds.
Result: in a pool of Pro accounts, only the account that recently served Spark traffic shows an account-level "5h" quota bar; the others show none. Verified live against backend-api/wham/usage for three Pro accounts on the same pool — all three return an identical structure (primary window 604800s weekly, secondary null, Spark 5h+weekly in additional_rate_limits), yet only the Spark-serving account carries account-level shortPercent 4 with shortWindowSeconds 18000, and that tuple matches the Spark 5h window exactly (same percent, same reset_at). Expected: model-specific limit readings must not occupy the account-level short-window slot, so Pro accounts with identical upstream limits display identically.
Reproduction
- Configure a Codex account pool with two or more Pro accounts; pin/select account A.
- Route a request for model gpt-5.3-codex-spark through account A with non-zero usage.
- The upstream response headers carry the Spark governing limit as primary (x-codex-primary-window-minutes: 300); applyAccountQuotaFromUpstreamHeaders stores it as account A's account-level short window.
- Compare with account B, which never served Spark: no account-level short window. The dashboard shows a "5h" quota only for account A, while wham/usage for A and B is structurally identical (weekly-only primary plus Spark additional windows).
A deterministic regression test can replay synthetic headers through applyAccountQuotaFromUpstreamHeaders — for example x-codex-primary-used-percent: 4, x-codex-primary-window-minutes: 300, x-codex-secondary-used-percent: 21, x-codex-secondary-window-minutes: 10080 — and assert the account-level slot is written with no model attribution, without credentials or upstream requests.
Version
2.49.0
Operating system
macOS 27.0
Provider and model
openai (ChatGPT Codex backend, account pool) / gpt-5.3-codex-spark
Logs or error output
wham/usage per account (three Pro accounts, same pool, same minute):
A: primary_window 604800s/21% additional GPT-5.3-Codex-Spark 5h 4% -> cache: shortPercent 4, shortWindowSeconds 18000
B: primary_window 604800s/29% additional GPT-5.3-Codex-Spark 5h 0% -> cache: no short window
C: primary_window 604800s/24% additional GPT-5.3-Codex-Spark 5h 0% -> cache: no short window
A.shortResetAt == A.customWindows["GPT-5.3-Codex-Spark 5h"].resetAt (exact match)
Screenshots and supporting files
No screenshots; the state above is read from codex-quota-cache.json and live wham/usage responses. Related: #4007 fixed the inverse transition (header updates clearing Spark customWindows); this issue is the same collision in the other direction (Spark header readings creating an account-level window).
Suggested fix path:
- Thread the routed model (or a model-limit hint) from the header-path callers that know it (responses/core.ts, responses/compact.ts) into applyAccountQuotaFromUpstreamHeaders; when the routed model belongs to a known model-specific limit family (codex_bengalfox / GPT-5.3-Codex-Spark), store the short primary window under customWindows instead of the account-level shortPercent.
- Alternatively or additionally, reconcile against the cached customWindows: if a GPT-5.3-Codex-Spark 5h entry with the same resetAt already exists, attribute the header primary reading to that model window.
- Refresh the stale comment in parseUpstreamQuotaHeaders ("Pro stays weekly-only") to describe the model-specific case.
- Regression test beside the existing quota tests: Spark-model header updates must not create account-level shortPercent, while a genuine account-level 5h primary on a non-Spark model still must.
Redacted configuration
{
"providers": { "openai": { "codexAccountMode": "pool", "authMode": "forward" } },
"activeCodexAccountPinned": "<account A>",
"showCodexSparkQuota": false
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
Ingwannu commentedon Sep 9, 2026
Confirmed the model-attribution gap on dev
57077ca326:parseUpstreamQuotaHeadersclassifies any explicit sub-day primary window into short*, andapplyAccountQuotaFromUpstreamHeadersreceives no routed-model identity. The callers in quota-auto-refresh and Responses/compact therefore cannot distinguish an account limit from a Spark-specific observation at this write boundary.This must be fixed in stored quota/policy attribution, not just hidden in the card. Thread the final model/limit identity while preserving account-generation and main-writer fences; keep genuine non-Spark short windows and independently observed account weekly limits. Do not infer scope from matching percentages/reset times alone. #4123 is related but covers a separate WHAM-payload path; both deserve separate regression fixtures, and fixing the header writer alone will not prove the WHAM mirror case fixed.
lidge-jun commentedon Sep 9, 2026
Fixed by #4128 (merged into dev as 91db6c2, head b214258). Spark-model response headers now file the model-specific 5h window under customWindows instead of the account-level short slot; genuine account-level 5h on non-Spark models unchanged. Note: short tuples polluted before this fix clear on restart, disk-cache TTL, or the next genuine short observation. Follow-ups recorded from review: Spark-aware exhaustion routing from customWindows; derive the Spark window label from the model id if a future Spark rename ships.
fix(codex): attribute Spark 5h header windows to the model limit, not…
cb8010d6 commentedon Sep 9, 2026
Implemented in #4137.
The patch follows the confirmed producer boundary rather than inferring ownership from matching values: HTTP, WebSocket, compact-retry, and warmup writers now carry the resolved quota scope. A Spark sub-day primary updates the exact
GPT-5.3-Codex-Spark 5hcustom window, while the ordinary secondary weekly window, genuine shared 5-hour observations, other custom windows, and writer/generation fences remain intact.For comparison, Wangnov/codex-meter#6 also separates
additional_rate_limitsentries usingmetered_feature: codex_bengalfox, but its presentation-only rule drops every top-level sub-day primary. The OpenCodeX fix is narrower because live Team WHAM data confirms that a top-level 18,000-second primary can be a genuine ordinary account limit.docs(devlog): move the closed Spark short-quota unit to _fin
fix(codex): stop carrying an elapsed account-level short window
fix(codex): attribute Spark 5h header windows to the model limit, not…
docs(devlog): move the closed Spark short-quota unit to _fin
fix(codex): stop carrying an elapsed account-level short window