fix(billing): validate quantity parameters and harden quota calculations
Bound user-supplied count/duration parameters at request validation, route ratio multipliers through guarded setters, and use saturating int conversions in all quota math paths.
This commit is contained in:
@@ -297,8 +297,8 @@ func RecalculateTaskQuotaByTokens(ctx context.Context, task *model.Task, totalTo
|
||||
}
|
||||
}
|
||||
|
||||
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio * otherMultiplier
|
||||
actualQuota := int(float64(totalTokens) * modelRatio * finalGroupRatio * otherMultiplier)
|
||||
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio * otherMultiplier(饱和转换,防止溢出成负数)
|
||||
actualQuota := common.QuotaFromFloat(float64(totalTokens) * modelRatio * finalGroupRatio * otherMultiplier)
|
||||
|
||||
reason := fmt.Sprintf("token重算:tokens=%d, modelRatio=%.2f, groupRatio=%.2f, otherMultiplier=%.4f", totalTokens, modelRatio, finalGroupRatio, otherMultiplier)
|
||||
RecalculateTaskQuota(ctx, task, actualQuota, reason)
|
||||
|
||||
+10
-2
@@ -287,7 +287,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
|
||||
if !ratio.IsZero() && quotaCalculateDecimal.LessThanOrEqual(decimal.Zero) {
|
||||
quotaCalculateDecimal = decimal.NewFromInt(1)
|
||||
}
|
||||
summary.Quota = int(quotaCalculateDecimal.Round(0).IntPart())
|
||||
summary.Quota = decimalToQuota(quotaCalculateDecimal)
|
||||
} else {
|
||||
quotaCalculateDecimal := dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
|
||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota)
|
||||
@@ -297,7 +297,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
|
||||
quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
|
||||
}
|
||||
}
|
||||
summary.Quota = int(quotaCalculateDecimal.Round(0).IntPart())
|
||||
summary.Quota = decimalToQuota(quotaCalculateDecimal)
|
||||
}
|
||||
|
||||
if summary.TotalTokens == 0 {
|
||||
@@ -309,6 +309,14 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
|
||||
return summary
|
||||
}
|
||||
|
||||
// decimalToQuota converts a computed quota decimal to int with saturation
|
||||
// (see common.QuotaFromFloat). Oversized multipliers (e.g. an absurd image
|
||||
// generation count) must never wrap around and turn a charge into a credit.
|
||||
func decimalToQuota(d decimal.Decimal) int {
|
||||
f, _ := d.Round(0).Float64()
|
||||
return common.QuotaFromFloat(f)
|
||||
}
|
||||
|
||||
func usageSemanticFromUsage(relayInfo *relaycommon.RelayInfo, usage *dto.Usage) string {
|
||||
if usage != nil && usage.UsageSemantic != "" {
|
||||
return usage.UsageSemantic
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -12,9 +13,22 @@ import (
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDecimalToQuotaSaturation guards the billing invariant that an oversized
|
||||
// quota product (e.g. per-call price multiplied by a huge image n ratio) must
|
||||
// saturate instead of wrapping into a negative charge (credit).
|
||||
func TestDecimalToQuotaSaturation(t *testing.T) {
|
||||
// 2000 quota per call * n=18446744073686646784 overflows int64.
|
||||
overflowing := decimal.NewFromInt(2000).Mul(decimal.NewFromFloat(1.8446744073686647e19))
|
||||
require.Equal(t, math.MaxInt32, decimalToQuota(overflowing))
|
||||
|
||||
require.Equal(t, math.MinInt32, decimalToQuota(overflowing.Neg()))
|
||||
require.Equal(t, 42, decimalToQuota(decimal.NewFromFloat(41.7)))
|
||||
}
|
||||
|
||||
func TestCalculateTextQuotaSummaryUnifiedForClaudeSemantic(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user