fix(billing): extend quantity validation and saturating conversions to remaining paths

Bound max-tokens fields across all relay format validators, saturate
tiered-expression rounding and audio/tool/task token conversions, and
route legacy remix ratios through the guarded setter.
This commit is contained in:
CaIon
2026-07-07 01:08:52 +08:00
parent d0bd8aac74
commit c9943d37ad
13 changed files with 155 additions and 30 deletions
+2 -2
View File
@@ -54,7 +54,7 @@ func calculateAudioQuota(info QuotaInfo) int {
groupRatio := decimal.NewFromFloat(info.GroupRatio)
quota := modelPrice.Mul(quotaPerUnit).Mul(groupRatio)
return int(quota.IntPart())
return decimalToQuota(quota)
}
completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(info.ModelName))
@@ -83,7 +83,7 @@ func calculateAudioQuota(info QuotaInfo) int {
quota = decimal.NewFromInt(1)
}
return int(quota.Round(0).IntPart())
return decimalToQuota(quota)
}
func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage) error {
+3 -5
View File
@@ -145,15 +145,13 @@ func composeTieredTextQuota(relayInfo *relaycommon.RelayInfo, summary textQuotaS
if tieredResult != nil {
if snap := relayInfo.TieredBillingSnapshot; snap != nil {
return int(decimal.NewFromFloat(tieredResult.ActualQuotaBeforeGroup).
return decimalToQuota(decimal.NewFromFloat(tieredResult.ActualQuotaBeforeGroup).
Mul(decimal.NewFromFloat(snap.GroupRatio)).
Add(summary.ToolCallSurchargeQuota).
Round(0).
IntPart())
Add(summary.ToolCallSurchargeQuota))
}
}
return tieredQuota + int(summary.ToolCallSurchargeQuota.Round(0).IntPart())
return tieredQuota + decimalToQuota(summary.ToolCallSurchargeQuota)
}
func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage) textQuotaSummary {
+12 -4
View File
@@ -208,8 +208,14 @@ func EstimateRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *rela
if err != nil {
return 0, fmt.Errorf("error getting audio duration: %v", err)
}
// 一分钟 1000 token,与 $price / minute 对齐
totalAudioToken += int(math.Round(math.Ceil(duration) / 60.0 * 1000))
// 一分钟 1000 token,与 $price / minute 对齐
// duration 来自用户上传文件的元数据,可被伪造成天文数字,
// 必须饱和转换防止 int 回绕成负数 token。
audioTokens := common.QuotaFromFloat(math.Round(math.Ceil(duration) / 60.0 * 1000))
if audioTokens < 0 {
audioTokens = 0
}
totalAudioToken += audioTokens
}
return totalAudioToken, nil
}
@@ -377,7 +383,8 @@ func CountAudioTokenInput(audioBase64 string, audioFormat string) (int, error) {
if err != nil {
return 0, err
}
return int(duration / 60 * 100 / 0.06), nil
// duration 来自用户提供的音频元数据,饱和转换防止 int 回绕
return common.QuotaFromFloat(duration / 60 * 100 / 0.06), nil
}
func CountAudioTokenOutput(audioBase64 string, audioFormat string) (int, error) {
@@ -388,7 +395,8 @@ func CountAudioTokenOutput(audioBase64 string, audioFormat string) (int, error)
if err != nil {
return 0, err
}
return int(duration / 60 * 200 / 0.24), nil
// duration 来自上游返回的音频元数据,饱和转换防止 int 回绕
return common.QuotaFromFloat(duration / 60 * 200 / 0.24), nil
}
// CountTextToken 统计文本的token数量,仅OpenAI模型使用tokenizer,其余模型使用估算
+2 -2
View File
@@ -49,7 +49,7 @@ func ComputeToolCallQuota(usage ToolCallUsage, groupRatio float64) ToolCallResul
return
}
totalPrice := pricePer1K * float64(count) / 1000
quota := int(math.Round(totalPrice * common.QuotaPerUnit * groupRatio))
quota := common.QuotaFromFloat(math.Round(totalPrice * common.QuotaPerUnit * groupRatio))
items = append(items, ToolCallItem{
Name: toolName,
CallCount: count,
@@ -70,7 +70,7 @@ func ComputeToolCallQuota(usage ToolCallUsage, groupRatio float64) ToolCallResul
if usage.ImageGenerationCall {
price := operation_setting.GetGPTImage1PriceOnceCall(usage.ImageGenerationQuality, usage.ImageGenerationSize)
quota := int(math.Round(price * common.QuotaPerUnit * groupRatio))
quota := common.QuotaFromFloat(math.Round(price * common.QuotaPerUnit * groupRatio))
items = append(items, ToolCallItem{
Name: "image_generation",
CallCount: 1,