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

This commit is contained in:
吴天一
2026-07-31 19:16:46 +08:00
committed by GitHub
parent 9724ef1b24
commit df43f80153
6 changed files with 209 additions and 8 deletions
+57 -1
View File
@@ -1,9 +1,13 @@
package service
import (
"net/http"
"github.com/QuantumNous/new-api/pkg/billingexpr"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/gin-gonic/gin"
)
// TieredResultWrapper wraps billingexpr.TieredResult for use at the service layer.
@@ -90,8 +94,60 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
}
}
func refreshTieredBillingGroup(relayInfo *relaycommon.RelayInfo) (*billingexpr.BillingSnapshot, error) {
if relayInfo == nil {
return nil, nil
}
snap := relayInfo.TieredBillingSnapshot
if snap == nil || snap.BillingMode != "tiered_expr" {
return nil, nil
}
groupRatio := relayInfo.PriceData.GroupRatioInfo.GroupRatio
if snap.GroupRatio == groupRatio {
return snap, nil
}
estimatedQuotaAfterGroup := snap.EstimatedQuotaBeforeGroup * groupRatio
estimatedQuota, err := billingexpr.QuotaRoundStrict(estimatedQuotaAfterGroup)
if err != nil {
return nil, err
}
snap.GroupRatio = groupRatio
snap.EstimatedQuotaAfterGroup = estimatedQuota
return snap, nil
}
// PrepareTieredBillingForSelectedGroup refreshes routing-dependent billing
// state before an upstream attempt. An existing session reserves any higher
// estimate before sending. If the initial group was free and skipped
// pre-consume, switching to a paid group creates the session at that point.
func PrepareTieredBillingForSelectedGroup(c *gin.Context, relayInfo *relaycommon.RelayInfo) *types.NewAPIError {
snap, err := refreshTieredBillingGroup(relayInfo)
if err != nil {
return types.NewErrorWithStatusCode(
err,
types.ErrorCodeModelPriceError,
http.StatusBadRequest,
types.ErrOptionWithSkipRetry(),
)
}
if snap == nil || snap.GroupRatio == 0 {
return nil
}
if relayInfo.Billing == nil {
return PreConsumeBilling(c, snap.EstimatedQuotaAfterGroup, relayInfo)
}
if err := relayInfo.Billing.Reserve(snap.EstimatedQuotaAfterGroup); err != nil {
return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
}
relayInfo.FinalPreConsumedQuota = relayInfo.Billing.GetPreConsumedQuota()
return nil
}
// TryTieredSettle checks if the request uses tiered_expr billing and, if so,
// computes the actual quota using the frozen BillingSnapshot. Returns:
// computes the actual quota using the captured BillingSnapshot. Returns:
// - ok=true, quota, result when tiered billing applies
// - ok=false, 0, nil when it doesn't (caller should fall through to existing logic)
func TryTieredSettle(relayInfo *relaycommon.RelayInfo, params billingexpr.TokenParams) (ok bool, quota int, result *billingexpr.TieredResult) {