Skip to content

fix(billing): settle tiered retries with final group - #6518

Merged
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
NexusAgentX:fix/tiered-final-group-billing
Jul 31, 2026
Merged

fix(billing): settle tiered retries with final group#6518
Calcium-Ion merged 1 commit into
QuantumNous:mainfrom
NexusAgentX:fix/tiered-final-group-billing

Conversation

@faithleysath

@faithleysath faithleysath commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

AI-assisted contribution. 修复 tiered_exprauto 跨组重试后仍按首次分组倍率结算的问题(#6480)。

每次渠道选定后,代码会同步 BillingSnapshot 中随路由变化的组倍率和预估额度:

  • 重试切到更贵分组时,调用现有 BillingSession.Reserve,在发送请求前补足该组的预扣;
  • 初始免费分组重试到付费分组时,在发送请求前建立 BillingSession;
  • 切到更便宜或免费分组时不提前退款,仍按最终实际 usage 结算差额;
  • 表达式和请求输入保持冻结,只有最终路由相关的组计费状态更新。

这使成功请求按最终选中的分组倍率结算,并保留失败重试的退款和预扣语义。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 提交者已确认本 PR 的描述、实现和测试结果;本 PR 为 AI-assisted 贡献,不声明为纯人工撰写。
  • 非重复提交: 已检查现有 Issue 和 PR;关联 tiered_expr 在 auto 跨组重试后仍按首次分组倍率结算 #6480,未发现已有对应修复 PR。
  • Bug fix 说明: 问题可在 tiered_expr + auto 跨组重试的纯计费路径复现,关联 tiered_expr 在 auto 跨组重试后仍按首次分组倍率结算 #6480
  • 变更理解: 仅刷新最终路由相关的 group ratio/estimated quota,并复用现有 BillingSession 预扣和结算机制。
  • 范围聚焦: 未包含前端功能、schema 迁移或其他计费模式改动。
  • 本地验证: 已运行完整 Go 测试和前端生产构建。
  • 安全合规: 未包含凭据或配置变更。

📸 运行证明 / Proof of Work

cd web && bun install --frozen-lockfile
DISABLE_ESLINT_PLUGIN=true VITE_REACT_APP_VERSION="$(cat ../VERSION)" bun run build
go test ./...

新增回归覆盖:

  • 更贵最终分组会在发送前提高预扣;
  • 免费首组重试到付费组会建立 BillingSession;
  • 更贵、免费最终分组均按最终倍率结算。

Summary by CodeRabbit

  • Bug Fixes

    • Improved tiered billing during relay retries by refreshing billing calculations for the ultimately selected group.
    • Ensured quota reservations and pre-consumption adjustments remain accurate when retries switch between free and paid groups.
    • Improved error handling when updated quota requirements cannot be applied.
  • Documentation

    • Clarified tiered billing, retry, snapshot, reservation, and settlement behavior.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

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

Changes

Tiered billing retry settlement

Layer / File(s) Summary
Billing snapshot semantics
pkg/billingexpr/types.go, pkg/billingexpr/expr.md, relay/common/relay_info.go
Documentation clarifies frozen request fields, refreshed group-dependent fields, free-to-paid retry billing, and final-group settlement.
Selected-group billing preparation
service/tiered_settle.go, service/tiered_settle_test.go
Refreshes group ratios and estimated quota, performs pre-consumption or reservation updates, maps failures to API errors, and tests paid/free retry scenarios.
Retry-loop integration
controller/relay.go
Invokes tiered billing preparation after selecting a retryable channel and exits the retry loop when preparation fails.

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
Loading

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

A rabbit hops where retries run,
Refreshing rates from group to group in the sun.
Quotas reserve, then settle right,
Free or paid, the totals align bright.
“Nibble the bug!” the bunny sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes refresh tiered billing state on retry, use the final group for reservation/settlement, and add regression tests for #6480.
Out of Scope Changes check ✅ Passed Changes are focused on tiered billing retry/settlement and supporting docs/tests, with no unrelated functional scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: tiered billing retries now settle using the final selected group.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Calcium-Ion
Calcium-Ion merged commit df43f80 into QuantumNous:main Jul 31, 2026
2 checks passed
l1i1 pushed a commit to l1i1/new-api that referenced this pull request Jul 31, 2026
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)
Calcium-Ion added a commit that referenced this pull request Aug 1, 2026
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.
Calcium-Ion added a commit that referenced this pull request Aug 1, 2026
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.
kimberxu pushed a commit to kimberxu/new-api that referenced this pull request Aug 1, 2026
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
0401lucky pushed a commit to 0401lucky/new-api that referenced this pull request Aug 2, 2026
0401lucky pushed a commit to 0401lucky/new-api that referenced this pull request Aug 2, 2026
)

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.
bigfish9 added a commit to lanlingxiawu/new-api-er that referenced this pull request Aug 3, 2026
合并上游 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,故本次未采纳,需单独改造。
dreamlx added a commit to dreamlx/new-api that referenced this pull request Aug 3, 2026
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>
speedxcc pushed a commit to speedxcc/new-api-speed that referenced this pull request Aug 4, 2026
合并官方上游 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)
nsuanningmeng added a commit to nsuanningmeng/LemonHub that referenced this pull request Aug 6, 2026
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.
yuqiyi pushed a commit to yuqiyi/new-api that referenced this pull request Aug 16, 2026
* 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
refeiner pushed a commit to wuqiang44444444/new-api that referenced this pull request Aug 17, 2026
refeiner pushed a commit to wuqiang44444444/new-api that referenced this pull request Aug 17, 2026
)

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.
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
)

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tiered_expr 在 auto 跨组重试后仍按首次分组倍率结算

2 participants