Skip to content

feat(codex): Sync and display credit balances - #823

Merged
tbphp merged 4 commits into
mainfrom
tbphp/codex-credit-balance
Oct 5, 2026
Merged

tbphp merged 4 commits into
mainfrom
tbphp/codex-credit-balance

Conversation

@tbphp

@tbphp tbphp commented Oct 4, 2026

Copy link
Copy Markdown
Owner

关联 Issue / Related Issue

Closes #822

变更内容 / Change Content

  • Bug 修复 / Bug fix
  • 新功能 / New feature
  • 其他改动 / Other changes

Read Codex credit balances from the existing account quota response and refresh them passively from HTTP response headers, for streaming and non-streaming requests, and WebSocket codex.rate_limits events. Credit-only observations also update the stored balance without requiring a quota window.

Persist zero balances while hiding zero and missing balances in the UI. Show positive balances or unlimited credits beside the plan badge in the modern frontend and beside reset credits in the classic frontend. Credit balances remain independent from quota windows and reset credits; values from different observation sources are never added together.

Preserve credit observation timestamps when later responses omit credit data, and retain the existing identity and concurrency checks against stale writes. Existing snapshots remain compatible through optional JSON fields; no database migration, SDK upgrade, or additional upstream request is required.

Validation: make check passed. Regression coverage includes legacy snapshots, missing and zero balances, credit-only HTTP and WebSocket observations, empty quota arrays, and stale observation timestamps. Live upstream requests confirmed that HTTP headers and WebSocket quota events provide credit balances.

自查清单 / Checklist

  • 我已运行 make check,或在说明中写明无法运行的原因和未验证范围。 / I ran make check, or documented why it could not run and what remains unverified.
  • 本 PR 范围聚焦,未包含无关改动。 / This PR is focused and contains no unrelated changes.
  • 我已更新必要的公开文档或发布说明。 / I updated any required public documentation or release notes. (Not applicable; no public documentation changes are required.)
  • 我已确认提交、日志和测试数据不包含敏感信息。 / I confirmed that commits, logs, and fixtures contain no sensitive data.
  • 如适用,我已说明兼容性或数据迁移影响。 / Where applicable, I documented compatibility or data-migration impact.

Copilot AI balanced review requested due to automatic review settings October 4, 2026 14:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@tbphp tbphp self-assigned this Oct 4, 2026
@tbphp tbphp added the enhancement New feature or request label Oct 4, 2026
@tbphp tbphp added this to the v2.0.0 milestone Oct 4, 2026
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

本次变更为 Codex 凭据点数增加观测、快照保存和前端展示。后端从配额数据、HTTP 响应头及 WebSocket 事件提取点数,并将其纳入被动观测和快照合并。classic 与 modern 前端解析点数摘要,并显示余额或无限额度状态。

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 20df2

Some later-arriving credit updates can be missed, leaving an older balance displayed. The impact is bounded, but the timestamp handling should be corrected or explicitly accepted before merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 20df2

Credit balances remain behind existing credential access controls. No new privilege or unauthenticated access path was identified. Risk is low, with remaining uncertainty around observation ordering and recovery after process interruption.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated new data-integrity exposure is a credential’s stored credit summary and its existing views. The inspected propagation does not establish new execution authority or cross-credential write reachability.

Trust Boundaries and Controls

  • observed — Passive writes retain credential-generation validation, per-credential mutation serialization and compare-and-set predicates. Presentation rejects stored observations whose identity fingerprint differs from the current credential.
  • observed — Upstream balances pass finite-number validation with a 128-character limit and reach the new card displays through formatted text interpolation, rather than an HTML interpretation sink.

Resilience and Maintainability Implications

  • observed — Database persistence precedes pending acknowledgement and runtime quota projection. A recovery routine can reload persisted observations using current identity fingerprints, but end-to-end recovery after interruption between these steps remains unverified.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 23 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了主要变更:同步并展示 Codex 额度余额。
Description check ✅ Passed 描述包含关联 Issue、变更内容、验证结果和自查清单,并说明兼容性影响及文档更新不适用。
Linked Issues check ✅ Passed [#822] 要求在凭据页显示点数,并在同步额度后更新余额。NormalizeQuota 从现有额度响应读取 credits,并将摘要保存在额度快照中;classic 前端在重置额度旁显示正余额或不限量状态,modern 前端在套餐徽章旁显示。改动还覆盖 HTTP 与 WebSocket 被动更新,并加入零值、缺失值及过期观测回归测试。
Out of Scope Changes check ✅ Passed 改动集中在 Codex 点数读取、观测快照持久化、被动更新、前端展示及相关测试和本地化。未发现与 [#822] 无关的变更。
Full details: Docstring Coverage

Explanation

Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 23 files. (2 skipped: 2 unsupported.)

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
internal/subscription/passive_quota_flush.go-151-168 (1)

151-168: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

在刷新边界按点数时间独立合并。

flushOnePassiveQuotaObservationLocked 会在 observation.ObservedAtMS <= row.ObservedAtMS 时直接丢弃整份样本。mergePassiveQuotaSamples 还会再次按快照总时间跳过样本,并把点数时间与总时间比较。这样,窗口观测时间较新但没有点数,或已有窗口使总时间较新时,后到的点数样本即使拥有更新的 CreditSummary.ObservedAtMS 也不会写入。最终 snapshot_json.credits 保持缺失或旧余额。

请在刷新边界分别校验窗口时间和点数时间,并在点数合并中与已有 credits.observed_at_ms 比较。点数单独合并时必须保留较新的快照总时间。

Suggested fix
@@
-		if row.ObservedAtMS != nil && observation.ObservedAtMS <= *row.ObservedAtMS {
-			// An observation at least as new -- typically a manual refresh --
-			// was persisted while this sample sat pending. Writing it now would
-			// rewind observed_at_ms and overwrite newer quota values with older
-			// ones. The tie is included on purpose: a passive header captured
-			// just before an active refresh that completed within the same
-			// millisecond truncates to the same value, and the active result is
-			// the authoritative one. The CAS below only catches writes that land
-			// after this read.
-			manager.passiveQuota.ack(observation.CredentialID, observation.Version)
-			return nil
-		}
 		merge, observedAtMS, mergeErr := mergePassiveQuotaSamples(row.SnapshotJSON, row.ObservedAtMS, observation)
@@
-	var observedAtMS int64
+	var observedAtMS int64
+	if storedAtMS != nil {
+		observedAtMS = *storedAtMS
+	}
 	samples := [2]*PassiveQuotaSample{
@@
-		if sample == nil || (storedAtMS != nil && sample.ObservedAtMS <= *storedAtMS) {
+		if sample == nil {
 			continue
 		}
+		windows := sample.Windows
+		if storedAtMS != nil && sample.ObservedAtMS <= *storedAtMS {
+			windows = nil
+		}
 		credits := sample.Credits
-		if credits != nil && credits.ObservedAtMS != nil && storedAtMS != nil && *credits.ObservedAtMS <= *storedAtMS {
-			credits = nil
+		if len(windows) == 0 && credits == nil {
+			continue
 		}
-		merged, err := mergePassiveQuotaSnapshot(result.Encoded, sample.Windows, credits)
+		merged, err := mergePassiveQuotaSnapshot(result.Encoded, windows, credits)
@@
-		if merged.Matched {
+		if merged.Matched && sample.ObservedAtMS > observedAtMS {
 			observedAtMS = sample.ObservedAtMS
 		}
@@
-		if patch.Balance != "" {
-			next.Balance = patch.Balance
-			next.ObservedAtMS = cloneInt64(patch.ObservedAtMS)
-		}
-		if patch.HasCredits != nil {
-			next.HasCredits = patch.HasCredits
-		}
-		if patch.Unlimited != nil {
-			next.Unlimited = patch.Unlimited
-			if *patch.Unlimited {
+		newer := previous == nil || previous.ObservedAtMS == nil ||
+			patch.ObservedAtMS == nil || *patch.ObservedAtMS > *previous.ObservedAtMS
+		if newer {
+			if patch.Balance != "" {
+				next.Balance = patch.Balance
 				next.ObservedAtMS = cloneInt64(patch.ObservedAtMS)
 			}
+			if patch.HasCredits != nil {
+				next.HasCredits = patch.HasCredits
+			}
+			if patch.Unlimited != nil {
+				next.Unlimited = patch.Unlimited
+				if *patch.Unlimited {
+					next.ObservedAtMS = cloneInt64(patch.ObservedAtMS)
+				}
+			}
+			outcome.Matched = true
 		}
-		outcome.Matched = true

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 84bc6a09-9e69-4789-b920-e3b789ad874f
📥 Commits

Reviewing files that changed from the base of the PR and between 65c6509 and 20df2bc.

📒 Files selected for processing (25)
  • internal/control/classic_api_contract_test.go
  • internal/control/credential_credits_test.go
  • internal/control/credential_observations.go
  • internal/execution/cpa/adapter.go
  • internal/execution/cpa/adapter_test.go
  • internal/execution/cpa/codex_provider.go
  • internal/execution/cpa/credits_test.go
  • internal/execution/cpa/provider.go
  • internal/execution/cpa/websocket.go
  • internal/subscription/passive_credits_test.go
  • internal/subscription/passive_quota.go
  • internal/subscription/passive_quota_flush.go
  • internal/subscription/providers/codex/credits.go
  • internal/subscription/providers/codex/credits_test.go
  • internal/subscription/providers/codex/observation.go
  • internal/subscription/providers/observation/snapshot.go
  • web/src/frontends/classic/api/control/types.ts
  • web/src/frontends/classic/app/resources/credentials.ts
  • web/src/frontends/classic/features/groups/credentials/SubscriptionAccountCard.vue
  • web/src/frontends/classic/i18n/locales/en-US/group.ts
  • web/src/frontends/classic/i18n/locales/ja-JP/group.ts
  • web/src/frontends/classic/i18n/locales/zh-CN/group.ts
  • web/src/frontends/modern/api/credential-observation.ts
  • web/src/frontends/modern/features/groups/SubscriptionCredentialCard.vue
  • web/src/frontends/modern/i18n/locales/credential-cards.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

@tbphp

tbphp commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Addressed the credit ordering issue from the review and the discussion in adbc0c7.

Pending accumulation and persistence now compare credits against their own observation time, retain newer windows, and keep the overall snapshot time monotonic. Active refreshes without credit data retain only the credit timestamp, preventing earlier passive responses from restoring a cleared balance. Existing identity and CAS protections remain in place.

Regression tests cover late credits before and after a window flush, missing and zero balances, active refreshes, timestamp ties, and legacy snapshots. make check passed.

For the docstring advisory: This repository does not enforce an 80% docstring threshold; the new exported credit parsers are documented.

@tbphp
tbphp merged commit 49d1bf6 into main Oct 5, 2026
12 checks passed
@tbphp
tbphp deleted the tbphp/codex-credit-balance branch October 5, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

希望增加凭据的点数(Credit)查询与展示

2 participants