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
This commit is contained in:
CaIon
2026-04-24 00:33:54 +08:00
parent 8eeae00737
commit 3e5f2ee1d6
6 changed files with 25 additions and 10 deletions
+2 -2
View File
@@ -323,8 +323,8 @@ func updatePricing() {
pricing.AudioCompletionRatio = &audioCompletionRatio pricing.AudioCompletionRatio = &audioCompletionRatio
} }
if billingMode := billing_setting.GetBillingMode(model); billingMode == "tiered_expr" { if billingMode := billing_setting.GetBillingMode(model); billingMode == "tiered_expr" {
pricing.BillingMode = billingMode if expr, ok := billing_setting.GetBillingExpr(model); ok && expr != "" {
if expr, ok := billing_setting.GetBillingExpr(model); ok { pricing.BillingMode = billingMode
pricing.BillingExpr = expr pricing.BillingExpr = expr
} }
} }
+1 -1
View File
@@ -269,7 +269,7 @@ func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptT
freeModel := false freeModel := false
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume { if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
if groupRatioInfo.GroupRatio == 0 || quotaBeforeGroup == 0 { if groupRatioInfo.GroupRatio == 0 {
preConsumedQuota = 0 preConsumedQuota = 0
freeModel = true freeModel = true
} }
+4
View File
@@ -226,6 +226,10 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota) model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
} }
if err := SettleBilling(ctx, relayInfo, quota); err != nil {
logger.LogError(ctx, "error settling billing: "+err.Error())
}
logModel := modelName logModel := modelName
if extraContent != "" { if extraContent != "" {
logContent += ", " + extraContent logContent += ", " + extraContent
+3 -1
View File
@@ -330,6 +330,7 @@ func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us
summary := calculateTextQuotaSummary(ctx, relayInfo, usage) summary := calculateTextQuotaSummary(ctx, relayInfo, usage)
var tieredResult *billingexpr.TieredResult var tieredResult *billingexpr.TieredResult
tieredBillingApplied := false
if originUsage != nil { if originUsage != nil {
var tieredUsedVars map[string]bool var tieredUsedVars map[string]bool
if snap := relayInfo.TieredBillingSnapshot; snap != nil { 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)) tieredOk, tieredQuota, tieredRes := TryTieredSettle(relayInfo, BuildTieredTokenParams(usage, summary.IsClaudeUsageSemantic, tieredUsedVars))
if tieredOk { if tieredOk {
tieredBillingApplied = true
tieredResult = tieredRes tieredResult = tieredRes
summary.Quota = composeTieredTextQuota(relayInfo, summary, tieredQuota, 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. // prompt/cache fields here, otherwise old upstream payloads may be double-counted.
other["input_tokens_total"] = usage.InputTokens other["input_tokens_total"] = usage.InputTokens
} }
if tieredResult != nil { if tieredBillingApplied {
InjectTieredBillingInfo(other, relayInfo, tieredResult) InjectTieredBillingInfo(other, relayInfo, tieredResult)
} }
+14 -5
View File
@@ -22,8 +22,14 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
p := float64(usage.PromptTokens) p := float64(usage.PromptTokens)
c := float64(usage.CompletionTokens) c := float64(usage.CompletionTokens)
cr := float64(usage.PromptTokensDetails.CachedTokens) cr := float64(usage.PromptTokensDetails.CachedTokens)
ccTotal := float64(usage.PromptTokensDetails.CachedCreationTokens) cc5m := float64(usage.PromptTokensDetails.CachedCreationTokens)
cc1h := float64(usage.ClaudeCacheCreation1hTokens) cc1h := float64(0)
if usage.UsageSemantic == "anthropic" {
cc1h = float64(usage.ClaudeCacheCreation1hTokens)
cc5m = float64(usage.ClaudeCacheCreation5mTokens)
}
img := float64(usage.PromptTokensDetails.ImageTokens) img := float64(usage.PromptTokensDetails.ImageTokens)
ai := float64(usage.PromptTokensDetails.AudioTokens) ai := float64(usage.PromptTokensDetails.AudioTokens)
imgO := float64(usage.CompletionTokenDetails.ImageTokens) imgO := float64(usage.CompletionTokenDetails.ImageTokens)
@@ -33,8 +39,11 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
if usedVars["cr"] { if usedVars["cr"] {
p -= cr p -= cr
} }
if usedVars["cc"] || usedVars["cc1h"] { if usedVars["cc"] {
p -= ccTotal p -= cc5m
}
if usedVars["cc1h"] {
p -= cc1h
} }
if usedVars["img"] { if usedVars["img"] {
p -= img p -= img
@@ -61,7 +70,7 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
P: p, P: p,
C: c, C: c,
CR: cr, CR: cr,
CC: ccTotal - cc1h, CC: cc5m,
CC1h: cc1h, CC1h: cc1h,
Img: img, Img: img,
ImgO: imgO, ImgO: imgO,
@@ -1041,12 +1041,12 @@ export function useModelPricingEditorState({
for (const model of models) { for (const model of models) {
if (model.billingMode === 'tiered_expr') { if (model.billingMode === 'tiered_expr') {
tieredOutput['billing_setting.billing_mode'][model.name] = 'tiered_expr';
const finalBillingExpr = combineBillingExpr( const finalBillingExpr = combineBillingExpr(
model.billingExpr, model.billingExpr,
model.requestRuleExpr, model.requestRuleExpr,
); );
if (finalBillingExpr) { if (finalBillingExpr) {
tieredOutput['billing_setting.billing_mode'][model.name] = 'tiered_expr';
tieredOutput['billing_setting.billing_expr'][model.name] = finalBillingExpr; tieredOutput['billing_setting.billing_expr'][model.name] = finalBillingExpr;
} }
} }