Skip to content

fix(subscription): 修复被动额度契约缺失与来源误归属 - #575

Merged
tbphp merged 5 commits into
mainfrom
fix/quota-source-ui-contract
Sep 6, 2026
Merged

fix(subscription): 修复被动额度契约缺失与来源误归属#575
tbphp merged 5 commits into
mainfrom
fix/quota-source-ui-contract

Conversation

@tbphp

@tbphp tbphp commented Sep 6, 2026

Copy link
Copy Markdown
Owner

关联 Issue / Related Issue

Follow-up to #574#574 上线后暴露两个问题:

  1. 额度同步接口返回 code: 0state: fresh,但账号卡片提示"无法同步账号信息"。
  2. 请求 Spark 等专属额度成功后,账号普通 7d 额度被误刷成满额(回到 fix(subscription): 按来源和周期匹配 Codex 被动额度窗口 #574 之前的错误行为)。

变更内容 / Change Content

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

问题 1(前端契约遗漏)#574 新增后端 quota_windows[].source_id,遗漏前端 DTO 和严格字段白名单,导致 projectQuotaWindow() 拒绝成功响应并抛出 InvalidResponseError。仅补 CredentialQuotaWindowDtoquotaWindowFields 接受可选 source_id,沿用现有 projectString() 校验。

问题 2(被动额度来源归属错误)X-Codex-Primary/Secondary-* 这组无命名空间的响应头,语义是"本次请求实际计费到的额度",不是固定的普通账号额度。请求 Spark 时该组装的是 Spark 自身的窗口;#574 把这组无条件标成 source_id=codex,导致来源+周期精确匹配到了错误目标(Spark 7d 覆盖普通 7d)。

修复为按 X-Codex-Active-Limit 判定该组归属:

  • premium/codex → 普通账号额度
  • 指向专属额度(如 codex_bengalfox)→ 改绑到该来源
  • 与该来源已有命名空间窗口同周期才是真重复,按 (来源, 周期) 逐窗口去重,避免误删另一周期的正常刷新
  • 缺少该头且响应含其他来源 → 按副本丢弃,沿用旧行为

不硬编码 Spark 模型映射,不新增依赖、数据库表,不修改 429、路由或调度。

测试 / Validation

新增/修改:

  • 前端契约:source_id 存在时保留,不存在时兼容旧响应,其他未知字段仍拒绝。
  • 后端来源归属:Active-Limit 指向普通/专属/未知来源、缺省时的窗口分配;专属额度与通用组同周期去重、不同周期均保留;现场复现用例(7d 用尽后请求 Spark,验证普通 7d 不被刷满)。

已执行:

  • make check 全部通过(含 -race、CPA、数据库合同、windows-encryption-acl)。
  • git --no-pager diff --check 通过。

兼容性与人工验收

无数据库或配置迁移,不需要删除或重新导入账号。部署须包含重新构建后的前端。

人工验收:

  1. 刷新页面并点击"刷新额度",卡片正常显示普通 Codex 7d 和 Spark 独立窗口,不再误报同步失败。
  2. 普通 7d 用尽后请求一次 Spark 模型,刷新卡片,确认普通 7d 仍显示用尽,不被 Spark 数据覆盖。

自查清单 / Checklist

  • 已执行 make check 并全部通过。
  • 范围聚焦于本次两处缺陷,未修改 429、路由或调度。
  • 必要的修复说明和部署要求已写入本 PR。
  • 不包含真实凭据或账号私密数据。
  • 已说明兼容性及无需数据迁移。

修复 #574 新增 source_id 后前端严格投影拒绝成功响应的问题。补齐可选 DTO 字段、白名单及字符串投影,保持其他未知字段校验,不修改后端额度匹配或限流行为。

新增前后端额度字段契约回归测试,避免后端新增字段却遗漏前端白名单和 DTO。
Copilot AI lite review requested due to automatic review settings September 6, 2026 01:49
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Team

Run ID: 16140406-250e-439e-891b-524ea2dd1897

📥 Commits

Reviewing files that changed from the base of the PR and between 3e107d7 and 6330356.

📒 Files selected for processing (2)
  • internal/subscription/providers/codex/observation.go
  • internal/subscription/providers/codex/passive_quota_active_limit_test.go

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Codex 被动配额窗口现在根据命名空间和 X-Codex-Active-Limit 确定 SourceID。系统会丢弃无法安全归属的通用窗口,并避免其与独立命名空间窗口竞争。新增测试覆盖来源匹配和窗口刷新行为。前端配额窗口 DTO 与投影支持可选的 source_id 字段。

Merge Risk: ⚪ Minimal · up to 63303

Codex passive quota windows now retain their source identity across backend and frontend handling, preventing metered quota updates from overwriting account quotas. The supplied coverage shows source-specific refreshes and period-aware deduplication behave as intended, with no material merge-readiness risk remaining.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了本次变更的两个核心问题:修复被动额度契约缺失和额度来源误归属。标题简洁、明确,并与代码改动一致。
Description check ✅ Passed PR 描述完整填写了关联 Issue、变更内容、测试验证、兼容性说明、人工验收和自查清单。描述明确说明了两个缺陷、修复方案、验证范围及部署要求。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

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.

🟡 Changes recommended

The new Go contract test reflects the provider snapshot struct rather than the Control API response type, which can miss API-only fields and weaken the intended frontend/backend contract guarantee.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a frontend/backend contract mismatch that caused the UI to show “sync failed” even when the quota sync API returned success (code: 0, state: fresh): the backend added quota_windows[].source_id, but the frontend DTO and strict whitelist rejected it during projection.

Changes:

  • Allow source_id in the quota window whitelist and project it into the parsed quota window DTO.
  • Add source_id?: string to CredentialQuotaWindowDto so the Control API types match the response.
  • Add a Go “frontend contract” test to ensure future backend quota window field additions require updating the frontend whitelist/DTO together.
File summaries
File Description
web/src/app/resources/credentials.ts Adds source_id to the strict allowed-field list and projects it into quota window parsing.
web/src/api/control/types.ts Extends CredentialQuotaWindowDto with optional source_id.
internal/webui/credential_quota_contract_test.go Adds a contract test intended to keep frontend whitelist/DTO aligned with backend quota window JSON fields.
Review details

Suppressed comments (1)

internal/webui/credential_quota_contract_test.go:31

  • After switching the import to internal/control, reflect the actual serialized quota window type (control.ObservationQuotaWindow) instead of the provider snapshot struct, so the test covers the real web contract (including observed_usage).
	windowType := reflect.TypeOf(providerobservation.QuotaWindow{})
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +3 to +12
import (
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"

providerobservation "gpt-load/internal/subscription/providers/observation"
)
上游的 X-Codex-Primary/Secondary-* 报告的是本次请求实际计费到的额度,
而不是固定的普通账号额度。请求 Spark 等专属额度时这一组装的是该额度的
副本,按 codex 收下会用它覆盖同周期的普通窗口,把用尽的 7d 刷成满额。

改为由 X-Codex-Active-Limit 决定该组归属:premium/codex 归普通额度,
指向专属额度时改绑到该来源,上游已单独报告该来源则让位避免歧义。
缺少该头且响应里已有别的来源时按副本丢弃。
@tbphp tbphp self-assigned this Sep 6, 2026
@tbphp tbphp added the bug Something isn't working label Sep 6, 2026
@tbphp tbphp added this to the v2.0.0 milestone Sep 6, 2026
@tbphp
tbphp requested a lite review from Copilot September 6, 2026 02:37

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.

🔵 Needs a closer look

It includes behavior-changing backend quota parsing logic (beyond the described frontend DTO fix), so it needs careful human review of scope, correctness, and rollout impact.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines 110 to +114
// passiveQuotaNamespaceFields are the header suffixes that belong to a limit
// namespace as a whole rather than to one of its windows.
var passiveQuotaNamespaceFields = []string{"limit-reached", "limit-name", "allowed"}
// namespace as a whole rather than to one of its windows. active-limit only
// ever appears on the response as a whole, which parses as the empty namespace.
var passiveQuotaNamespaceFields = []string{"limit-reached", "limit-name", "allowed", "active-limit"}

@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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Team

Run ID: 5ca080a8-3e54-46e9-a511-b781222bd7cf

📥 Commits

Reviewing files that changed from the base of the PR and between 1dc1757 and ca5e554.

📒 Files selected for processing (6)
  • internal/subscription/passive_quota_source_flush_test.go
  • internal/subscription/passive_quota_source_test.go
  • internal/subscription/providers/codex/observation.go
  • internal/subscription/providers/codex/observation_test.go
  • internal/subscription/providers/codex/passive_quota_active_limit_test.go
  • internal/subscription/providers/codex/quota_source_test.go

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread internal/subscription/providers/codex/observation.go Outdated
Active-Limit 指向专属额度时,通用组只有与独立命名空间来源和周期都相同
的窗口才是重复报告。原先只要该来源存在任意窗口就丢掉整组,会漏掉另一
个周期的刷新。改为解析后按来源加周期逐窗口去重。
@tbphp tbphp changed the title fix(ui): 修复额度同步成功却提示失败的 source_id 契约遗漏 fix(subscription): 修复被动额度契约缺失与来源误归属 Sep 6, 2026
通用组内部的 primary 与 secondary 是两份数据而非互为副本,周期相同时
原实现让它们互相判定为重复并双双丢弃。去重范围收窄为通用窗口与独立
命名空间窗口之间。
@tbphp
tbphp merged commit 4a99ab1 into main Sep 6, 2026
11 checks passed
@tbphp
tbphp deleted the fix/quota-source-ui-contract branch September 6, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants