Custom providers can show quota warnings from the local ChatGPT account

Open 💬 2 comments Opened Jul 19, 2026 by MeroZemory

Summary

When Codex is configured with a custom model provider whose requests use credentials/account separate from the local ChatGPT login, proactive quota notices can still be driven by the local ChatGPT account.

For example, Codex may show:

Heads up, you have less than 10% of your weekly limit left.

even though the active custom provider is routing the request through a different account with sufficient quota.

Reproduction

  1. Log Codex in to a ChatGPT account so that ~/.codex/auth.json contains local account state.
  2. Configure and select a custom model provider with requires_openai_auth = false.
  3. Have that provider send inference requests using separate credentials/account.
  4. Let the locally logged-in ChatGPT account approach its rate limit while the account actually selected by the provider still has sufficient quota.
  5. Run a model turn through the custom provider.

The TUI can show a proactive low-quota warning based on the local account rather than the account that served the turn.

Expected behavior

  • Local ChatGPT account snapshots must not drive proactive quota warnings when the active provider does not require OpenAI authentication.
  • Rate-limit metadata returned with the active model turn should still drive accurate warnings for the account/provider that actually served the request.
  • OpenAI-specific lower-cost model-switch nudges should not be shown for custom providers.
  • Correct warnings should not require an additional quota API request on every turn; Codex should use metadata already present on the successful turn response.

Root cause

The notice path treats local AccountUsage snapshots and turn-derived RollingUpdate snapshots equivalently. Their provenance matters:

  • AccountUsage represents the locally logged-in ChatGPT account.
  • RollingUpdate comes from the active model turn response.

For a custom provider, only the latter can describe the quota state of the provider/account that actually served the request.

Validated patch

I prepared and tested a minimal patch that scopes notices by snapshot provenance and provider authentication behavior:

Added tests:

  • custom_provider_ignores_local_account_rate_limit_notices
  • custom_provider_warns_from_turn_rate_limits_without_model_nudge

Verification:

  • Targeted tests: 2/2 passed
  • Full codex-tui: 3,145/3,146 passed
  • just fix -p codex-tui: passed
  • just fmt: passed

The one full-suite failure is an unrelated existing Windows-only pet image assertion:

pets::tests::kitty_local_file_pet_image_uses_file_reference_without_inline_payload

That test uses absence of the literal Base64 substring cG5n as a proxy for ensuring the bytes b"png" were not sent inline. However, the local-file protocol intentionally Base64-encodes the canonical file path, and a Windows path ending in frame.png can legitimately contain the same substring depending on Base64 alignment. The output still uses Kitty's file-transfer mode (t=f), and this patch does not touch the pet/image code.

PR availability

The repository currently limits opening pull requests to collaborators, so I could not open the PR directly. The compare link above is ready for a maintainer/collaborator to use.

View original on GitHub ↗

2 Comments

MeroZemory · 1 month ago

Closing as not reproducible as filed.

On a closer read of the current source, /status and the startup rate-limit prefetch only fetch the local ChatGPT account's limits when should_prefetch_rate_limits() is true, i.e. config.model_provider.requires_openai_auth && has_chatgpt_account (tui/src/chatwidget/rate_limits.rs; gate applied in slash_dispatch.rs for /status and in app.rs for the startup prefetch, and covered by the test status_command_renders_immediately_without_rate_limit_refresh). So a session whose active provider is a custom provider with requires_openai_auth = false never fetches or displays the local account's limits to begin with.

The behavior I originally observed turned out to be a resumed session: codex resume restores the thread's saved model_provider (here openai) unless an explicit override is passed (intended behavior, #32746). The session was therefore actually running as openai against the local ChatGPT account, so the warning correctly reflected that account — not a custom provider mislabeling.

Apologies for the noise. If a genuinely reachable case exists — e.g. a stale AccountUsage snapshot surviving a mid-session provider switch, where the /status rows have no provenance gate — I'll open a fresh issue with a concrete reproduction and, if warranted, a narrowly scoped patch (the OpenAI-specific model-switch nudge suppression for custom providers was the only part of my draft with independent value).

MeroZemory · 1 month ago

Reopening with a corrected analysis. My earlier close was overstated: "a custom-provider session never fetches or displays the local account's limits" holds only for a fresh custom-provider launch, not after an in-process transition from an OpenAI-authenticated context.

Why the originally-filed (pure custom-provider) repro does not reproduce

account_state() reads the auth manager only when requires_openai_auth, returning None otherwise (model-provider/src/provider.rs:285). So on a fresh launch with a custom provider (requires_openai_auth = false), has_chatgpt_account / has_codex_backend_auth are both false, and /status, the startup prefetch, and /usage all skip the local-account fetch (/status + startup gate on should_prefetch_rate_limits = requires_openai_auth && has_chatgpt_account; /usage gates on has_codex_backend_auth).

The actually-reachable path (provider transition)

When a thread is forked/resumed in-process, the new widget is built with the new config but copies the previous widget's account flags (tui/src/app.rs:739-747: has_chatgpt_account: self.chat_widget.has_chatgpt_account(), has_codex_backend_auth: self.chat_widget.has_codex_backend_auth()). After switching OpenAI → custom provider and forking/resuming without restarting, those flags stay stale-true. Then /usage (gated on has_codex_backend_auth) and the reset-credit flow (OpenRateLimitResetCredits, ungated — tui/src/app/event_dispatch.rs:981) call fetch_account_rate_limits, which reads the local ChatGPT account regardless of the active provider (app-server/src/request_processors/account_processor.rs). The resulting AccountUsage snapshot, pre-patch, fired should_warn_about_rate_limit_usage = is_codex_limit && !has_workspace_credits — a local-account warning under a custom active provider.

Corrected reproduction

  1. Launch codex on OpenAI with a ChatGPT login (account flags become true).
  2. Switch config to a custom provider with requires_openai_auth = false.
  3. Fork/resume a thread without restarting the TUI (new config, stale account flags).
  4. /usage -> "Redeem usage limit reset".
  5. Pre-patch: a high-usage local-account snapshot emits the quota warning despite the custom active provider.

Fix outline

  • e2060ea87 gates the warning/nudge on RollingUpdate || requires_openai_auth, suppressing the misleading warning (the reported symptom).
  • Root cause is the stale account-flag copy on fork/resume (app.rs:739-747). Recomputing has_chatgpt_account / has_codex_backend_auth from the new provider on fork/resume (instead of copying the previous widget's) would also keep /status and /usage consistent, not just the warning.

Filing as analysis-only per CONTRIBUTING (no unsolicited PR). Happy to expand the reproduction or root-cause detail.