Skip to content

Spark model-specific 5h header window is stored as account-level short quota on Pro accounts #4122

Closed

Description

@lidge-jun

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

  1. Configure a Codex account pool with two or more Pro accounts; pin/select account A.
  2. Route a request for model gpt-5.3-codex-spark through account A with non-zero usage.
  3. 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.
  4. 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:

  1. 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.
  2. 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.
  3. Refresh the stale comment in parseUpstreamQuotaHeaders ("Pro stays weekly-only") to describe the model-specific case.
  4. 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

  1. Ingwannu commented on Sep 9, 2026

    @Ingwannu
    Owner

    Confirmed the model-attribution gap on dev 57077ca326: parseUpstreamQuotaHeaders classifies any explicit sub-day primary window into short*, and applyAccountQuotaFromUpstreamHeaders receives 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.

  2. lidge-jun commented on Sep 9, 2026

    @lidge-jun
    OwnerAuthor

    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.

  3. added a commit that references this issue on Sep 9, 2026
    b214258
  4. cb8010d6 commented on Sep 9, 2026

    @cb8010d6
    Contributor

    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 5h custom 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_limits entries using metered_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.

  5. added 3 commits that reference this issue on Sep 17, 2026
    e0c6a96
    f722b17
    1f12231
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions