refactor: deprecate int32 (#7025)

* refactor: deprecate int32

* fix(db): reject legacy user quota schemas at startup

* fix(quota): enforce wallet bounds and saturating billing conversions

* fix(rate-limit): keep count*duration from wrapping int64

* fix: error message
This commit is contained in:
Seefs
2026-08-26 20:57:54 +08:00
committed by GitHub
parent 2d8e50bf36
commit a073f74b38
35 changed files with 445 additions and 116 deletions
+7 -3
View File
@@ -3,7 +3,6 @@ package service
import (
"errors"
"fmt"
"math"
"strings"
"time"
@@ -272,11 +271,16 @@ func CalcOpenRouterCacheCreateTokens(usage dto.Usage, priceData types.PriceData)
completionTokens := float64(usage.CompletionTokens)
promptCacheReadTokens := float64(usage.PromptTokensDetails.CachedTokens)
return int(math.Round((cost -
value := (cost -
totalPromptTokens*quotaPrice +
promptCacheReadTokens*(quotaPrice-promptCacheReadPrice) -
completionTokens*completionPrice) /
(promptCacheCreatePrice - quotaPrice)))
(promptCacheCreatePrice - quotaPrice)
quota, clamp := common.QuotaRoundChecked(value)
if clamp != nil {
return -1
}
return quota
}
func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, extraContent string) {
+25
View File
@@ -1,12 +1,15 @@
package service
import (
"math"
"net/http"
"testing"
"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
hosttypes "github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
@@ -42,6 +45,28 @@ func TestAttachQuotaSaturationNestsUnderAdminInfo(t *testing.T) {
require.Equal(t, common.MaxQuota, sat["clamped"])
}
func TestCalcViolationFeeQuotaSaturates(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
common.QuotaPerUnit = 500_000
t.Cleanup(func() { common.QuotaPerUnit = oldQuotaPerUnit })
require.Equal(t, common.MaxQuota, calcViolationFeeQuota(1e20, 1))
}
func TestCalcOpenRouterCacheCreateTokensDoesNotWrap(t *testing.T) {
oldQuotaPerUnit := common.QuotaPerUnit
common.QuotaPerUnit = 500_000
t.Cleanup(func() { common.QuotaPerUnit = oldQuotaPerUnit })
got := CalcOpenRouterCacheCreateTokens(dto.Usage{Cost: math.Inf(1)}, hosttypes.PriceData{
ModelRatio: 1,
CacheCreationRatio: 2,
CacheRatio: 1,
CompletionRatio: 1,
})
require.Equal(t, -1, got)
}
// TestAttachQuotaSaturationPreservesExistingAdminInfo verifies the marker is
// merged into a pre-existing admin_info map without clobbering it.
func TestAttachQuotaSaturationPreservesExistingAdminInfo(t *testing.T) {
+2 -2
View File
@@ -216,8 +216,8 @@ func composeTieredTextQuota(relayInfo *relaycommon.RelayInfo, summary textQuotaS
}
// Saturate the final sum, not just the surcharge: tieredQuota can be near
// MaxQuota and adding the surcharge could push the total past the int32
// quota policy bound (persisted quota columns are 32-bit).
// MaxQuota and adding the surcharge could push the total past the
// single-request quota policy bound.
total, clamp := common.QuotaFromDecimalChecked(
decimal.NewFromInt(int64(tieredQuota)).Add(summary.ToolCallSurchargeQuota),
)
+4 -4
View File
@@ -774,9 +774,9 @@ func TestComposeTieredTextQuotaErrorFallbackUsesPreConsumedQuota(t *testing.T) {
// settlement both saturates the quota and records the clamp on RelayInfo, so
// every consume path (text, audio, WSS) can surface it under admin_info.
func TestTryTieredSettleRecordsClampOnOverflow(t *testing.T) {
// exprOutput = p * 1e9; quotaBeforeGroup = p*1e9 / 1e6 * 5e5 far exceeds
// MaxInt32 and must saturate.
exprStr := `tier("base", p * 1000000000)`
// exprOutput = p * 1e12; quotaBeforeGroup = p*1e12 / 1e6 * 5e5 far exceeds
// the supported single-request range and must saturate.
exprStr := `tier("base", p * 1000000000000)`
relayInfo := &relaycommon.RelayInfo{
OriginModelName: "overflow-model",
TieredBillingSnapshot: &billingexpr.BillingSnapshot{
@@ -792,7 +792,7 @@ func TestTryTieredSettleRecordsClampOnOverflow(t *testing.T) {
require.True(t, ok)
require.NotNil(t, result)
require.Equal(t, math.MaxInt32, quota, "oversized settlement must clamp, never wrap negative")
require.Equal(t, common.MaxQuota, quota, "oversized settlement must clamp, never wrap negative")
require.NotNil(t, relayInfo.QuotaClamp, "clamp must be recorded on RelayInfo for admin auditing")
require.Equal(t, common.QuotaClampOverflow, relayInfo.QuotaClamp.Kind)
}
+1 -1
View File
@@ -180,7 +180,7 @@ func TryTieredSettle(relayInfo *relaycommon.RelayInfo, params billingexpr.TokenP
return true, quota, nil
}
// Surface any int32 saturation from settlement onto RelayInfo so the
// Surface any single-request saturation from settlement onto RelayInfo so the
// consume log records it under admin_info, regardless of which caller
// (text, audio, WSS) consumes the returned quota. First non-nil wins.
noteQuotaClamp(relayInfo, tr.Clamp)
+2 -2
View File
@@ -140,11 +140,11 @@ func getImageToken(c *gin.Context, fileMeta *types.FileMeta, model string, strea
if imageTokens > 1536 {
imageTokens = 1536
}
return int(math.Round(float64(imageTokens) * multiplier)), nil
return common.QuotaRound(float64(imageTokens) * multiplier), nil
}
// below cap
imageTokens := rawPatches
return int(math.Round(float64(imageTokens) * multiplier)), nil
return common.QuotaRound(float64(imageTokens) * multiplier), nil
}
// Tile-based calculation for 4o/4.1/4.5/o1/o3/etc.
+3 -4
View File
@@ -88,15 +88,14 @@ func calcViolationFeeQuota(amount, groupRatio float64) int {
if groupRatio <= 0 {
return 0
}
quota := decimal.NewFromFloat(amount).
quota := common.QuotaFromDecimal(decimal.NewFromFloat(amount).
Mul(decimal.NewFromFloat(common.QuotaPerUnit)).
Mul(decimal.NewFromFloat(groupRatio)).
Round(0).
IntPart()
Round(0))
if quota <= 0 {
return 0
}
return int(quota)
return quota
}
// ChargeViolationFeeIfNeeded charges an additional fee after the normal flow finishes (including refund).