fix(billing): settle tiered retries with final group - #6518
Conversation
WalkthroughTiered billing now refreshes group-dependent snapshot fields after auto-group selection, adjusts pre-consumption or reservations, and settles using the final group. The relay retry loop invokes this preparation before marking the channel used or reading the request body. ChangesTiered billing retry settlement
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Relay
participant PrepareTieredBillingForSelectedGroup
participant BillingSettler
participant TryTieredSettle
Relay->>PrepareTieredBillingForSelectedGroup: selected group and RelayInfo
PrepareTieredBillingForSelectedGroup->>BillingSettler: PreConsumeBilling or Reserve updated quota
BillingSettler-->>PrepareTieredBillingForSelectedGroup: updated billing state
Relay->>TryTieredSettle: final usage and refreshed snapshot
TryTieredSettle->>BillingSettler: settle captured snapshot
BillingSettler-->>TryTieredSettle: settlement quota and result
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Follow-up to QuantumNous#6518 (issue QuantumNous#6480) addressing three review findings: - Wallet Reserve top-up now checks the balance before decrementing: DecreaseUserQuota never fails on insufficient balance (it drives the wallet negative), so a retry landing on a more expensive group now returns insufficient_user_quota (403, skip retry) like the initial pre-consume, while genuine DB errors keep query/update error codes. - PriceData.FreeModel is cleared when a retry switches from a free group to a paid one, keeping it consistent with the billing session created at that point. - getChannel refreshes GroupRatioInfo only after channel selection succeeds, and the retry loop records the channel in use_channel before PrepareTieredBillingForSelectedGroup can fail. (cherry picked from commit dd924e5)
Follow-up to #6518 (issue #6480) addressing three review findings: - Document and lock in arrears semantics for the wallet Reserve top-up: when an auto-group retry lands on a more expensive group, the full reservation delta is deducted unconditionally (balance may go negative), mirroring settlement, so the logged pre-consumed quota always reconciles with the actual balance movement. Genuine DB errors still fail the attempt with update_data_error. Subscription funding keeps its insufficient-quota behavior: subscriptions enforce a hard used<=total cap and do not support arrears. - PriceData.FreeModel is cleared when a retry switches from a free group to a paid one, keeping it consistent with the billing session created at that point. - getChannel refreshes GroupRatioInfo only after channel selection succeeds, and the retry loop records the channel in use_channel before PrepareTieredBillingForSelectedGroup can fail.
Follow-up to #6518 (issue #6480) addressing three review findings: - Document and lock in arrears semantics for the wallet Reserve top-up: when an auto-group retry lands on a more expensive group, the full reservation delta is deducted unconditionally (balance may go negative), mirroring settlement, so the logged pre-consumed quota always reconciles with the actual balance movement. Genuine DB errors still fail the attempt with update_data_error. Subscription funding keeps its insufficient-quota behavior: subscriptions enforce a hard used<=total cap and do not support arrears. - PriceData.FreeModel is cleared when a retry switches from a free group to a paid one, keeping it consistent with the billing session created at that point. - getChannel refreshes GroupRatioInfo only after channel selection succeeds, and the retry loop records the channel in use_channel before PrepareTieredBillingForSelectedGroup can fail.
Upstream 2026-07-31..2026-08-01: deepseek responses api (QuantumNous#6562), zstd request decompression (QuantumNous#6545), log stream status (QuantumNous#6558), tiered retry billing settlement (QuantumNous#6518, QuantumNous#6570), OAuth opener fix (QuantumNous#6425), multipart image edit fix (QuantumNous#6559), public header nav style (QuantumNous#6557). Conflict resolution: - controller/relay.go: keep per-channel rate limit check (custom) and upstream PrepareTieredBillingForSelectedGroup call - relay/common/relay_info.go: keep RequestDebugSnapshot (custom) and upstream TieredBillingSnapshot comment
) Follow-up to QuantumNous#6518 (issue QuantumNous#6480) addressing three review findings: - Document and lock in arrears semantics for the wallet Reserve top-up: when an auto-group retry lands on a more expensive group, the full reservation delta is deducted unconditionally (balance may go negative), mirroring settlement, so the logged pre-consumed quota always reconciles with the actual balance movement. Genuine DB errors still fail the attempt with update_data_error. Subscription funding keeps its insufficient-quota behavior: subscriptions enforce a hard used<=total cap and do not support arrears. - PriceData.FreeModel is cleared when a retry switches from a free group to a paid one, keeping it consistent with the billing session created at that point. - getChannel refreshes GroupRatioInfo only after channel selection succeeds, and the retry loop records the channel in use_channel before PrepareTieredBillingForSelectedGroup can fail.
合并上游 16 个提交,主要是 token Auto 分组 (QuantumNous#6590)、deepseek responses API (QuantumNous#6562)、Bedrock 客户端断开取消 (QuantumNous#6589)、分层重试计费加固 (QuantumNous#6518/QuantumNous#6570)、 zstd 请求解压 (QuantumNous#6545)、OIDC 自定义显示名 (QuantumNous#6012)、日志暴露 stream_status (QuantumNous#6558)。 33 处冲突的处理: - 11 个 legacy channel adaptor:上游删除 panic 之后的死代码以配合新增的 go vet CI,本仓早已把整段 panic 换成返回 ErrLegacyAdaptorNotImplemented, 已达成同一目的且不会让中继链路 panic,保留本仓实现。 - relay-aws.go 流式循环:两侧改动正交,合并保留 —— 上游的 ctx.Done() 取消 分支(客户端断开时不再空转上游),加本仓的 finalizeClaudeOnError(提前 返回时补发流终止符,否则 Claude 格式调用方会一直挂着)。 - 7 个 locale:两侧各自插入相邻 key,按字母序归并;上游 24 个新 key 与本仓 1278 个 fork key 全部保留,逐一核对无丢失。 - keys / oauth / profile 前端:上游 Auto 分组为主体,叠加本仓改动; account-bindings-tab 与 oauth/$provider 保留本仓实现,理由见下。 测试取舍: - 不引入 model/token_auto_groups_cache_test.go:它依赖上游的 truncateTables, 该助手全局清表,与本仓行级清理原则冲突(共享库里有开发数据)。 - 不携带上游 controller/token_test.go:其迁移兼容测试会替换 model.DB 并在 cleanup 里关闭,后续用 harness 连接的测试会拿到已关闭的句柄。按既有约定, token_auto_groups_test.go 需要的四个助手放进 zz_upstream_test_shims_test.go, 其中 openTokenControllerTestDB 增加了 model.DB/LOG_DB 的成对保存还原。 本仓原 token_test.go 的 controller 行为测试迁到 gen_ctrl_token_handlers_test.go,与上游文件名脱钩以免再冲突。 - 三处断言随上游行为更新:stream_status 现对日志所有者可见;deepseek 的 ConvertOpenAIResponsesRequest 已实现不再返回错误;计费路径判定收紧为 「标记之外还需带对应 usage payload」,并补了三个反向用例锁住该语义。 遗留:oauth 绑定回调仍用 window.opener 判定 bind/login,上游 QuantumNous#6425 已改为 sessionStorage 标记 + state 比对。该修复要求 popup 先以 about:blank 打开再打 标记,本仓四个内置 provider 走 window.open(url) 直开,直接套用会让绑定永远 判成 login,故本次未采纳,需单独改造。
Sync upstream/main (rc.11 → rc.23, 46 commits) into main. Key upstream changes: QuantumNous#6369 relaykit module extraction (dto/types/service/relayconvert moved to relaykit/ submodule with require+replace), QuantumNous#6518/QuantumNous#6570 tiered retry billing, QuantumNous#6558 stream status exposed to log owners, QuantumNous#6562 DeepSeek responses API, QuantumNous#6590 auto group, QuantumNous#6545 zstd decompression, per-channel HTTP transport controls. Conflicts resolved: - constant/api_type.go, constant/channel.go: keep LH channel numbering (OspreyAI=58/HappyHorse=59/Seedance=60/AdvancedCustom=61 via reserved placeholders), append upstream Sub2API=62/NewAPI=63 before Dummy. - relay/common/relay_info_test.go: union imports (LH common2/constant/gin + upstream convmeta/relaykit-types/assert), keep both test sets. LH-only packages (ospreyai/happyhorse/seedance/task variants) had stale host dto/types imports after relaykit extraction — migrated to relaykit/dto and relaykit/types (task-family TaskError kept on host dto via taskdto alias, matching upstream doubao pattern). Co-Authored-By: Claude <noreply@anthropic.com>
合并官方上游 Calcium-Ion/new-api main 分支的 10 个新 commit: - Feat/auto group (QuantumNous#6590) - fix(aws): cancel Bedrock requests on client disconnect (QuantumNous#6589) - fix(billing): harden tiered retry group-switch billing (QuantumNous#6570) - fix(billing): settle tiered retries with final group (QuantumNous#6518) - feat: deepseek responses api (QuantumNous#6562) - fix(oauth): stop treating a foreign window.opener as a bind flow (QuantumNous#6425) - fix(relay): preserve multipart image edits for New API channels (QuantumNous#6559) - feat(logs): expose stream status to log owners (QuantumNous#6558) - feat: support zstd request decompression (QuantumNous#6545) - style: use text-sm for public header nav links (QuantumNous#6557) 冲突解决: model/option.go 的 switch case 区域,双方各自新增了选项注册 (我方 GroupPassThrough + 官方 MaxTokenAutoGroups),取并集保留两者。 其余文件均自动合并成功。 验证: go build + go test 全过(relay/helper、middleware、controller)
Sync 71 upstream commits (rc.21 -> rc.23). Major upstream changes adopted: - stateless-token dashboard auth (sessions removed); fork site scoping re-grafted fail-closed via model.GetUserSiteId in setDashboardAuthContext - relaykit module extraction; fork billing guards, error override, and O(N^2) DoS fix re-applied inside relaykit paths - web/default -> web frontend move; web/classic theme deleted (fork's classic-only edits verified ported to the new tree before deletion) - auto-group (QuantumNous#6590), per-channel HTTP transport, tool pricing, zstd, New API channel, tiered-retry billing fixes (QuantumNous#6518/QuantumNous#6570) Fork features preserved and verified: multi-site scoping, tickets, captcha suite (per-request verification now, no session memo), email campaigns, request-body recording, epay three-path settlement, quota saturation invariants, model-redirect hiding, token multi-group priority. Notable resolutions: - 76 fork-only frontend files missed by rename detection were manually relocated from web/default/src to web/src (tickets/referral/site-admin/ sub-site-management/contact/affiliate + routes + locale extras) - altcha dependency restored in web/package.json - getModelListGroups adopts upstream empty-on-filtered-snapshot semantics - relayconvert goldens regenerated for fork's BillingUsage json:"-" privacy - upstream tests adapted to fork signatures (site scope args, SSRF-off TestMain in controller) Semantic changes accepted from upstream: external identity binding is now platform-global (was per-site); captcha verification is per-request. go build/vet/test green in root and relaykit modules.
* v1.0.0-rc.24: (117 commits) CI: enhance release synchronization workflow with optional file syncing fix: 修复兑换码额度精度损失 (QuantumNous#6685) feat(rate-limit): add user critical rate limit middleware for access token and aff transfer routes fix: test Claude/Gemini endpoints with native request format (QuantumNous#6698) feat(channels): refine fetched model categorization (QuantumNous#6632) Merge commit from fork refactor(relay): move replay metadata onto request bodies fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset (QuantumNous#6249) Feat/auto group (QuantumNous#6590) fix(aws): cancel Bedrock requests on client disconnect (QuantumNous#6589) fix(billing): harden tiered retry group-switch billing (QuantumNous#6570) fix(billing): settle tiered retries with final group (QuantumNous#6518) feat: deepseek responses api (QuantumNous#6562) style: use text-sm for public header nav links to match other nav components (QuantumNous#6557) fix(oauth): stop treating a foreign window.opener as a bind flow (QuantumNous#6425) fix(relay): preserve multipart image edits for New API channels (QuantumNous#6559) feat(logs): expose stream status to log owners (QuantumNous#6558) feat: support zstd request decompression (QuantumNous#6545) fix: preserve Qwen thinking_budget passthrough (QuantumNous#5836) feat(oidc): 支持自定义 OIDC 登录显示名称 (QuantumNous#6012) ... # Conflicts: # service/text_quota.go # web/src/features/models/components/drawers/model-mutate-drawer.tsx # web/src/features/pricing/components/model-details.tsx # web/src/features/pricing/lib/price.ts
) Follow-up to QuantumNous#6518 (issue QuantumNous#6480) addressing three review findings: - Document and lock in arrears semantics for the wallet Reserve top-up: when an auto-group retry lands on a more expensive group, the full reservation delta is deducted unconditionally (balance may go negative), mirroring settlement, so the logged pre-consumed quota always reconciles with the actual balance movement. Genuine DB errors still fail the attempt with update_data_error. Subscription funding keeps its insufficient-quota behavior: subscriptions enforce a hard used<=total cap and do not support arrears. - PriceData.FreeModel is cleared when a retry switches from a free group to a paid one, keeping it consistent with the billing session created at that point. - getChannel refreshes GroupRatioInfo only after channel selection succeeds, and the retry loop records the channel in use_channel before PrepareTieredBillingForSelectedGroup can fail.
) Follow-up to QuantumNous#6518 (issue QuantumNous#6480) addressing three review findings: - Document and lock in arrears semantics for the wallet Reserve top-up: when an auto-group retry lands on a more expensive group, the full reservation delta is deducted unconditionally (balance may go negative), mirroring settlement, so the logged pre-consumed quota always reconciles with the actual balance movement. Genuine DB errors still fail the attempt with update_data_error. Subscription funding keeps its insufficient-quota behavior: subscriptions enforce a hard used<=total cap and do not support arrears. - PriceData.FreeModel is cleared when a retry switches from a free group to a paid one, keeping it consistent with the billing session created at that point. - getChannel refreshes GroupRatioInfo only after channel selection succeeds, and the retry loop records the channel in use_channel before PrepareTieredBillingForSelectedGroup can fail.
📝 变更描述 / Description
AI-assisted contribution. 修复
tiered_expr在auto跨组重试后仍按首次分组倍率结算的问题(#6480)。每次渠道选定后,代码会同步
BillingSnapshot中随路由变化的组倍率和预估额度:BillingSession.Reserve,在发送请求前补足该组的预扣;这使成功请求按最终选中的分组倍率结算,并保留失败重试的退款和预扣语义。
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
tiered_expr + auto跨组重试的纯计费路径复现,关联 tiered_expr 在 auto 跨组重试后仍按首次分组倍率结算 #6480。📸 运行证明 / Proof of Work
新增回归覆盖:
Summary by CodeRabbit
Bug Fixes
Documentation