Explorar o código

fix(billing): correct tiered billing settlement and edge cases

- quota.go: add missing SettleBilling call in PostWssConsumeQuota
- text_quota.go: gate InjectTieredBillingInfo on tieredBillingApplied bool
  instead of tieredResult != nil, so fallback billing still logs metadata
- price.go: remove quotaBeforeGroup == 0 from freeModel condition to avoid
  bypassing settlement for output-only expressions
- tiered_settle.go: split cc/cc1h subtraction using UsageSemantic to
  distinguish OpenAI vs Claude cache creation token formats
- pricing.go: only set BillingMode when a non-empty expression exists
- useModelPricingEditorState.js: only write billing_mode when
  finalBillingExpr is non-empty
CaIon hai 2 semanas
pai
achega
3e5f2ee1d6

+ 2 - 2
model/pricing.go

@@ -323,8 +323,8 @@ func updatePricing() {
 			pricing.AudioCompletionRatio = &audioCompletionRatio
 		}
 		if billingMode := billing_setting.GetBillingMode(model); billingMode == "tiered_expr" {
-			pricing.BillingMode = billingMode
-			if expr, ok := billing_setting.GetBillingExpr(model); ok {
+			if expr, ok := billing_setting.GetBillingExpr(model); ok && expr != "" {
+				pricing.BillingMode = billingMode
 				pricing.BillingExpr = expr
 			}
 		}

+ 1 - 1
relay/helper/price.go

@@ -269,7 +269,7 @@ func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptT
 
 	freeModel := false
 	if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
-		if groupRatioInfo.GroupRatio == 0 || quotaBeforeGroup == 0 {
+		if groupRatioInfo.GroupRatio == 0 {
 			preConsumedQuota = 0
 			freeModel = true
 		}

+ 4 - 0
service/quota.go

@@ -226,6 +226,10 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
 		model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
 	}
 
+	if err := SettleBilling(ctx, relayInfo, quota); err != nil {
+		logger.LogError(ctx, "error settling billing: "+err.Error())
+	}
+
 	logModel := modelName
 	if extraContent != "" {
 		logContent += ", " + extraContent

+ 3 - 1
service/text_quota.go

@@ -330,6 +330,7 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us
 	summary := calculateTextQuotaSummary(ctx, relayInfo, usage)
 
 	var tieredResult *billingexpr.TieredResult
+	tieredBillingApplied := false
 	if originUsage != nil {
 		var tieredUsedVars map[string]bool
 		if snap := relayInfo.TieredBillingSnapshot; snap != nil {
@@ -337,6 +338,7 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us
 		}
 		tieredOk, tieredQuota, tieredRes := TryTieredSettle(relayInfo, BuildTieredTokenParams(usage, summary.IsClaudeUsageSemantic, tieredUsedVars))
 		if tieredOk {
+			tieredBillingApplied = true
 			tieredResult = tieredRes
 			summary.Quota = composeTieredTextQuota(relayInfo, summary, tieredQuota, tieredRes)
 		}
@@ -451,7 +453,7 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us
 		// prompt/cache fields here, otherwise old upstream payloads may be double-counted.
 		other["input_tokens_total"] = usage.InputTokens
 	}
-	if tieredResult != nil {
+	if tieredBillingApplied {
 		InjectTieredBillingInfo(other, relayInfo, tieredResult)
 	}
 

+ 14 - 5
service/tiered_settle.go

@@ -22,8 +22,14 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
 	p := float64(usage.PromptTokens)
 	c := float64(usage.CompletionTokens)
 	cr := float64(usage.PromptTokensDetails.CachedTokens)
-	ccTotal := float64(usage.PromptTokensDetails.CachedCreationTokens)
-	cc1h := float64(usage.ClaudeCacheCreation1hTokens)
+	cc5m := float64(usage.PromptTokensDetails.CachedCreationTokens)
+	cc1h := float64(0)
+
+	if usage.UsageSemantic == "anthropic" {
+		cc1h = float64(usage.ClaudeCacheCreation1hTokens)
+		cc5m = float64(usage.ClaudeCacheCreation5mTokens)
+	}
+
 	img := float64(usage.PromptTokensDetails.ImageTokens)
 	ai := float64(usage.PromptTokensDetails.AudioTokens)
 	imgO := float64(usage.CompletionTokenDetails.ImageTokens)
@@ -33,8 +39,11 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
 		if usedVars["cr"] {
 			p -= cr
 		}
-		if usedVars["cc"] || usedVars["cc1h"] {
-			p -= ccTotal
+		if usedVars["cc"] {
+			p -= cc5m
+		}
+		if usedVars["cc1h"] {
+			p -= cc1h
 		}
 		if usedVars["img"] {
 			p -= img
@@ -61,7 +70,7 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
 		P:    p,
 		C:    c,
 		CR:   cr,
-		CC:   ccTotal - cc1h,
+		CC:   cc5m,
 		CC1h: cc1h,
 		Img:  img,
 		ImgO: imgO,

+ 1 - 1
web/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js

@@ -1041,12 +1041,12 @@ export function useModelPricingEditorState({
 
       for (const model of models) {
         if (model.billingMode === 'tiered_expr') {
-          tieredOutput['billing_setting.billing_mode'][model.name] = 'tiered_expr';
           const finalBillingExpr = combineBillingExpr(
             model.billingExpr,
             model.requestRuleExpr,
           );
           if (finalBillingExpr) {
+            tieredOutput['billing_setting.billing_mode'][model.name] = 'tiered_expr';
             tieredOutput['billing_setting.billing_expr'][model.name] = finalBillingExpr;
           }
         }