Usage/quota queries frequently showed spurious "query failed" states that
manual refresh could not clear (#3820). Root cause: all transport-level
failures were folded into Ok(success:false), so react-query's retry never
fired and the failure body poisoned the cache as regular data.
Backend:
- balance/coding_plan/subscription services now return Err for send
failures and body-read failures (read body via bytes() before
serde_json::from_slice; reqwest's json() wraps read errors as Decode,
making error-kind checks on it dead code). Auth/4xx/parse errors stay
Ok(success:false) and surface immediately.
- Script path maps transient AppError keys (request_failed /
read_response_failed) to Err; Volcengine adds a Transient call variant.
- Command layer skips snapshot persistence, usage-cache-updated emit and
tray refresh on Err so the cache bridge cannot overwrite retained data.
- Expired-credential retry propagates transient errors instead of
rewriting them as "OAuth token has expired".
Frontend:
- resolveDisplayUsage generalized to subscription quotas; 5th param is now
an options object with a `rejected` flag: stale success data retained by
react-query across rejections is re-anchored to dataUpdatedAt and expires
through the same 10-minute keep-last-good window instead of being shown
indefinitely (a total outage used to mask longer than a single 5xx).
- useSubscriptionQuota / useCodexOauthQuota gain keep-last-good with
per-scope snapshot reset; rejected queries with no displayable value
synthesize a failure placeholder carrying the real error message so
footers keep rendering a retry entry point.
- HTTP 429 is classified transient (retry-later) alongside 5xx.
- UsageScriptModal surfaces string rejections via extractErrorMessage.
Tests: 6 backend behavior tests drive real HTTP against a local listener
to pin the Err/Ok channel semantics; frontend suite extends
keepLastGoodUsage coverage (405 passing).
- Add `get_codex_oauth_models` Tauri command reusing the managed OAuth
access token to hit `chatgpt.com/backend-api/codex/models`; HTTP and
multi-shape JSON parsing live in `services::codex_oauth_models` so the
command stays thin.
- Unify the Claude form's "fetch models" button across normal / Copilot /
Codex OAuth presets, drop the auto-load effect for Copilot in favor of
explicit clicks, and guard against stale responses with a requestId ref.
- Add Vitest coverage for both Copilot and Codex OAuth paths asserting no
request on mount and the correct account id on click; add Rust unit
tests for the four model-list payload shapes.
Codex OAuth (ChatGPT Plus/Pro) providers previously fell through to the
default UsageFooter branch and showed no quota at all, while Copilot and
official Codex providers already had a wham/usage-backed quota footer.
This wires up the same five-hour / seven-day tier badges for codex_oauth
provider cards by reusing the existing query_codex_quota function and
SubscriptionQuotaFooter rendering, parameterized to keep both the CLI
credential path ("codex") and the cc-switch managed OAuth path
("codex_oauth") working from a single source of truth.
- Parameterize services::subscription::query_codex_quota with tool_label
and expired_message; promote SubscriptionQuota constructors to
pub(crate). The CLI path keeps its existing "codex" label and the
"re-login with Codex CLI" message; the new path passes "codex_oauth"
and a cc-switch-specific re-login hint.
- Add a new get_codex_oauth_quota Tauri command in commands/codex_oauth.rs
that resolves the ChatGPT account (explicit binding > default account
> not_found), pulls a valid access_token from CodexOAuthManager
(auto-refresh handled), and delegates to query_codex_quota.
- Extract SubscriptionQuotaFooter's render body into a pure
SubscriptionQuotaView component (props: quota / loading / refetch /
appIdForExpiredHint / inline). The existing SubscriptionQuotaFooter
becomes a thin wrapper with identical props and behavior, so
CopilotQuotaFooter and the official Claude/Codex/Gemini paths are
untouched. This avoids duplicating ~280 lines of five-state rendering.
- Add CodexOauthQuotaFooter, a 38-line wrapper that calls the new
useCodexOauthQuota hook and forwards to SubscriptionQuotaView.
- ProviderCard inserts an isCodexOauth branch between isCopilot and
isOfficial, keyed off PROVIDER_TYPES.CODEX_OAUTH (newly added to
config/constants.ts to centralize the previously scattered string).
- Frontend hook caches per (codex_oauth, accountId) so multiple cards
bound to the same ChatGPT account share one fetch via react-query
dedup; cards bound to different accounts get independent fetches.
- No new i18n keys: existing subscription.fiveHour / sevenDay / expired /
refresh / queryFailed / expiredHint are reused.
Adds a new managed OAuth provider that lets Claude Code route requests
through a user's ChatGPT Plus/Pro subscription via the chatgpt.com
backend-api/codex endpoint.
- CodexOAuthManager: OpenAI Device Code flow with multi-account support,
JWT-based account identification, and automatic access_token refresh.
- Reuses the generic managed-auth command surface (auth_start_login,
auth_poll_for_account, etc.) via provider dispatch in commands/auth.rs.
- ClaudeAdapter detects codex_oauth providers, forces the base URL to
the ChatGPT backend, pins api_format to openai_responses, and emits
Authorization + originator headers; the forwarder injects the dynamic
access_token and ChatGPT-Account-Id per request.
- transform_responses gains an is_codex_oauth path that aligns the body
with OpenAI's codex-rs ResponsesApiRequest contract: sets store:false,
appends reasoning.encrypted_content to include, strips max_output_tokens
/ temperature / top_p, injects default instructions/tools/parallel_tool_calls,
and forces stream:true. Covered by 9 new unit tests plus regression
guards for the non-Codex path.
- Stream check reuses the same transform flag so detection matches the
production request shape.
- Frontend adds CodexOAuthSection + useCodexOauth hook, integrates it
into ClaudeFormFields / ProviderForm / AuthCenterPanel, ships a new
"Codex (ChatGPT Plus/Pro)" preset, and adds zh/en/ja i18n strings.