Commit Graph
11 Commits
Author SHA1 Message Date
CaIon ccd535ef8e fix: harden concurrent quota and status updates 2026-08-11 22:03:47 +08:00
Calcium-Ion cfaba1dd67 fix(billing): harden tiered retry group-switch billing (#6570)
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.
2026-08-01 09:35:51 +08:00
Calcium-Ion 86ac0f7745 refactor: extract protocol conversion layer into standalone relaykit module (#6369)
* test(relayconvert): add golden snapshot matrix and relaykit boundary guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.

* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.

* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged

* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports

* refactor(dto): drop gin and logger dependencies

Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.

* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged

* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch

* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)

Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged

* build(docker): copy relaykit/go.mod before go mod download

The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.

* fix: address relaykit extraction regressions

* fix: address relaykit review regressions

* docs: document Meta nil receiver contract

* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

* fix: make Gemini safety settings read path race-free
2026-07-27 15:56:21 +08:00
CaIon f6c260437c feat(subscription): add support for wallet overflow and downgrade group functionality in subscription plans 2026-06-18 16:44:12 +08:00
CaIon 6bde1a9c8d Merge origin/main into nightly
Resolve conflicts:
- .gitignore: keep nightly additions (.test, skills-lock.json)
- relay/helper/price.go: keep both billingexpr and model imports
- en.json / zh-CN.json: keep nightly's superset of i18n entries
- service/billing_session.go: add missing 3rd arg to DecreaseUserQuota
- en.json / zh-CN.json: deduplicate 129+320 duplicate i18n keys
2026-04-23 21:37:03 +08:00
CaIon 91ed4e196a feat: implement tiered billing expression evaluation and related functionality
- Added support for tiered billing expressions in the billing system.
- Introduced new types and functions for handling billing expressions, including caching and execution.
- Updated existing billing logic to accommodate tiered billing scenarios.
- Enhanced request handling to support incoming billing expression requests.
- Added tests for tiered billing functionality to ensure correctness.
2026-03-17 16:59:25 +08:00
CaIon ba25ba88fe refactor(task): extract billing and polling logic from controller to service layer
Restructure the task relay system for better separation of concerns:
- Extract task billing into service/task_billing.go with unified settlement flow
- Move task polling loop from controller to service/task_polling.go (supports Suno + video platforms)
- Split RelayTask into fetch/submit paths with dedicated retry logic (taskSubmitWithRetry)
- Add TaskDto, TaskResponse generics, and FetchReq to dto/task.go
- Add taskcommon/helpers.go for shared task adaptor utilities
- Remove controller/task_video.go (logic consolidated into service layer)
- Update all task adaptors (ali, doubao, gemini, hailuo, jimeng, kling, sora, suno, vertex, vidu)
- Simplify frontend task logs to use new TaskDto response format
2026-02-22 16:00:27 +08:00
t0ng7u 743b9278c4 🛠️ fix: billing session error handling for subscription-first fallback.
Aligns the error variable types in the subscription-first path so that quota fallback checks use the correct NewAPIError.
This prevents build failures and preserves the intended wallet fallback when subscription pre-consume returns an insufficient quota error.
2026-02-07 01:03:49 +08:00
t0ng7u 4fd8d033cd chore: Improve subscription billing fallback and UI states
Add a lightweight active-subscription check to skip subscription pre-consume when none exist, reducing unnecessary transactions and locks. In the subscription UI, disable subscription-first options when no active plan is available, show the effective fallback to wallet with a clear notice, and distinguish “invalidated” from “expired” states. Update i18n strings across supported locales to reflect the new messages and status labels.
2026-02-07 00:57:36 +08:00
CaIon 0a8055286b fix: 修复 BillingSession 多个边界问题
- Settle 部分失败保护:新增 fundingSettled 标记,资金来源提交后
  令牌调整失败不再导致 Refund 误退已结算的资金
- 订阅多扣费修复:trySubscription 传 subConsume 而非 preConsumedQuota
  给 preConsume,保证三者(amount/preConsume/FinalPreConsumedQuota)一致
- 令牌回滚错误记录:preConsume 中 funding 失败时令牌回滚错误不再丢弃
- 移除钱包路径死代码:用户额度不足的 strings.Contains 匹配不可能命中
- WalletFunding.Refund 不重试:IncreaseUserQuota 非幂等,重试会多退
2026-02-06 23:41:51 +08:00
CaIon 116004fd44 refactor: 抽象统一计费会话 BillingSession
将散落在多个文件中的预扣费/结算/退款逻辑抽象为统一的 BillingSession 生命周期管理:

- 新增 BillingSettler 接口 (relay/common/billing.go) 避免循环引用
- 新增 FundingSource 接口 + WalletFunding / SubscriptionFunding 实现 (service/funding_source.go)
- 新增 BillingSession 封装预扣/结算/退款原子操作 (service/billing_session.go)
- 新增 SettleBilling 统一结算辅助函数,替换各 handler 中的 quotaDelta 模式
- 重写 PreConsumeBilling 为 BillingSession 工厂入口
- controller/relay.go 退款守卫改用 BillingSession.Refund()

修复的 Bug:
- 令牌额度泄漏:PreConsumeTokenQuota 成功但 DecreaseUserQuota 失败时未回滚
- 订阅退款遗漏:FinalPreConsumedQuota=0 但 SubscriptionPreConsumed>0 时跳过退款
- 订阅多扣费:subConsume 强制为 1 但 FinalPreConsumedQuota 不同步
- 退款路径不统一:钱包/订阅退款逻辑现统一由 FundingSource.Refund 分派
2026-02-06 23:14:25 +08:00