feat(quota): surface credit balance and pool overview on dashboard#582
Open
icebear0828 wants to merge 3 commits into
Open
feat(quota): surface credit balance and pool overview on dashboard#582icebear0828 wants to merge 3 commits into
icebear0828 wants to merge 3 commits into
Conversation
CodexUsageResponse.credits was previously typed as `unknown` and dropped on the floor by toQuota(). For Plus accounts that doesn't matter (has_credits=false, balance=0), but for Pro / Pay-As-You-Go and team accounts the balance is the only signal the operator has to track spend without leaving the dashboard. Phase 1 surfaces the data that's already on the wire from the existing /codex/usage warmup polls — no new upstream traffic, no new auth scope, no risk to account standing: * Type CodexUsageResponse.credits / spend_control / rate_limit_reached_type properly and carry credits through toQuota() into a new CodexQuota.credits slot. balance is parsed from upstream's decimal string into a number; malformed payloads return null defensively rather than NaN. * updateCachedQuota() now preserves previously known credits when the incoming quota lacks them. The passive header-driven path (rateLimitToQuota in proxy-rate-limit) doesn't carry credit info — without this merge every /codex/responses response would wipe the balance set by the warmup. * Config schema adds usage_stats.credits_per_usd (default 25 ⇒ 1000 credits = $40 per the public rate card). Set to 0 to suppress USD rendering. Currently consumed by the dashboard as a hard-coded constant matching the default; a follow-up will wire it through admin/general-settings. * AccountCard renders a Credit Balance row only for accounts where has_credits=true or unlimited=true. Plus accounts render exactly as before. Overage-limit-reached accounts get a red flag. * PoolOverview is a new card above AccountList on the main page, showing active vs. quota-exhausted counts, the sum of credits + USD across accounts with a real credit pool, and the account with the highest secondary used_percent + its reset time. Tests cover the credits parser (5), the credits-preserve merge (2), the format helpers (11), the pool aggregation logic (7), and the new config default (1). No new tests for the React tree itself — the project's web .test.tsx files are dead today because jsdom isn't installed; restoring that lives in a follow-up. i18n: 8 new strings in zh + en.
…refresh GET /auth/accounts/:id/quota called /codex/usage on every invocation but returned the result without touching pool.cachedQuota. As a result the dashboard's per-account "Refresh" button (which POSTed /refresh — token refresh only — and never hit /quota) had no path to surface upstream window resets. After OpenAI does a promo / window grant that resets secondary_window.used_percent to 0, the proxy keeps showing the pre-reset percentage until a real proxied /codex/responses request comes in and passively updates cachedQuota via x-codex-* headers. That same path also never surfaces the credits block (Pro / PAYG accounts), since x-codex-credits-* headers are not parsed today — only /codex/usage body carries credits, and only via toQuota(). Persist toQuota(usage) into pool.updateCachedQuota right after the upstream fetch in the route handler, and chain the dashboard's "Refresh" button so it hits /refresh (token + status) followed by /quota (cachedQuota write-back). Result: clicking Refresh on a card after a quota reset now immediately reflects the upstream state on screen, and Credit Balance rows populate for Pro / PAYG accounts on first refresh without waiting for traffic. Discovered while validating PR #582 — three Plus accounts that the proxy was reporting at 98–100% secondary_used were actually at 0% upstream after a recent promo refresh. After this fix, /quota on each restored the truth.
Owner
Author
|
type 修复 + credits 保留合并思路对路,三点小修建议:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of the "Plus / Pro account spend visibility" track. Goal was originally "show Plus 订阅到期日 on the dashboard" — investigation (this same conversation) confirmed that data point is not exposed by any endpoint reachable from the Codex Desktop OAuth scope. JWT
/authonly haschatgpt_plan_type,/codex/usagehas only rate windows + credits,/backend-api/mecarries profile but no subscription dates,/backend-api/wham/analytics/daily-workspace-usage-counts(the Codex Cloud Settings → Analytics endpoint) returns per-day/week credit usage but no renewal date.What we can surface today, with zero new upstream traffic, is the credit accounting block that's already in
/codex/usagewarmup responses. The block was being typed asunknownand dropped on the floor. For Plus accounts it's all zeros (no credit pool) but for Pro / Pay-As-You-Go / Team accounts the balance is the only signal an operator has to track spend without leaving the dashboard.Changes
Backend
src/proxy/codex-types.ts— typeCodexUsageCredits,CodexUsageSpendControl,CodexUsageRateLimitReachedType(previouslyunknown). Re-exported throughcodex-api.ts.src/auth/types.ts— newCodexQuotaCreditsshape onCodexQuota.credits.balanceis parsed into a number once at ingest; the rest of the proxy works in numeric.src/auth/quota-utils.ts—toQuota()carries credits through, with defensive null on malformed payloads (rather than producing NaN).src/auth/account-registry.ts—updateCachedQuota()preserves previously known credits when the incoming quota lacks them. The passive header-driven path (rateLimitToQuotainproxy-rate-limit) doesn't carry credit info — without this merge every/codex/responsesresponse would wipe the balance set by the warmup. Behavior-equivalent for paths that always set credits.src/config-schema.ts— newusage_stats.credits_per_usd(default 25 ⇒ 1000 credits = $40, per the public rate card). Set to 0 to suppress USD rendering.Frontend
shared/types.ts—AccountQuota.creditsmirrors backend shape.shared/utils/format.ts—formatCredits/creditsToUsd/formatUsdhelpers.shared/i18n/translations.ts— 8 new strings (zh + en):creditsBalance,creditsUnlimited,creditsOverageReached,poolOverview,poolActiveAccounts,poolExhaustedAccounts,poolTotalCredits,poolTopUsage.web/src/components/AccountCard.tsx— new Credit Balance row rendered only whenhas_credits=true || unlimited=true. Plus accounts render exactly as before. Overage-limit flag goes red.web/src/components/PoolOverview.tsx(new) — card above AccountList on the main page. Shows active vs. quota-exhausted counts, sum of credits + USD across accounts with a real credit pool, and the account with highest secondary used_percent + its reset time.web/src/App.tsx— wirePoolOverviewaboveAccountListon the default tab.Known follow-ups (separate PRs)
credits_per_usdis currently hard-coded in the dashboard (DEFAULT_CREDITS_PER_USD = 25inAccountCard.tsxandPoolOverview.tsx) matching the schema default. Wiring it throughadmin/general-settingsso an operator'sdata/local.yamloverride actually takes effect on the UI is a follow-up.web/src/components/*.test.tsxfiles are currently dead becausejsdomisn't installed; adding the devDep + restoring the tests is out of scope here. Pool aggregation logic is covered bytests/unit/web/pool-overview-stats.test.tsinstead (pure function, runs under node).wham/analytics/daily-workspace-usage-countsendpoint, with bar charts on a dedicated drawer) is intentionally not included. The endpoint works (probed + confirmed in the same conversation) but is poll-only — each call is ~1 extra/backend-apirequest per account. Will be opt-in / on-demand in a follow-up to keep risk profile clean.Test Plan
npx vitest run tests/unit/auth/quota-utils.test.ts— 13 pass (5 new for credits)npx vitest run tests/unit/auth/account-pool-quota.test.ts— 21 pass (2 new for credits-preserve merge)npx vitest run shared/utils/__tests__/format-credits.test.ts— 11 pass (new file)npx vitest run tests/unit/web/pool-overview-stats.test.ts— 7 pass (new file)npx vitest run tests/unit/config-schema.test.ts— 15 pass (1 new for credits_per_usd default)npx vitest run— 2279 pass, 1 skipped, 0 failures across full suitenpx tsc --noEmit— cleannpm run build— clean web bundle, 267 kB → 66 kB gzipped (+8 kB vs base)http://localhost:8080/: confirmPoolOverviewcard renders above the account list (since current pool has only Plus accounts withhas_credits=false, the Credit Balance row and "Total Credits" tile correctly stay hidden; active/exhausted counts and "Highest Weekly Usage" should appear)Notes
The cache-related concern about Phase 1 changing upstream traffic: this PR ships zero new upstream calls. Credits flow through the existing
/codex/usagewarmup that runs at import / on manual "refresh" button click. No background poll, no new endpoint, no new auth scope. The cache hit rate sampling we did earlier in the same investigation showedinstructionsare already stable (cchstrip works), so the existing cache hit rate (~97% steady-state) is unaffected.