fix(billing): surface quota saturation events for admin auditing
Thread int32 saturation clamps from tiered settlement and video task recompute into the consume/task logs under admin_info, so oversized or malformed billing inputs stay auditable. Clamp negative audio duration before token conversion and gate the saturation UI markers on admin.
This commit is contained in:
@@ -291,12 +291,9 @@ func TestQuotaRound(t *testing.T) {
|
||||
{999.4999, 999},
|
||||
{999.5, 1000},
|
||||
{1e9 + 0.5, 1e9 + 1},
|
||||
// Oversized expression results must saturate at int32 bounds
|
||||
// instead of wrapping into a negative charge.
|
||||
// Oversized expression results saturate at int32 (delegated to
|
||||
// common.QuotaRound); full saturation coverage lives in common.
|
||||
{3.6893488147419103e19, math.MaxInt32},
|
||||
{-3.6893488147419103e19, math.MinInt32},
|
||||
{math.Inf(1), math.MaxInt32},
|
||||
{math.NaN(), 0},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := billingexpr.QuotaRound(tt.in)
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
package billingexpr
|
||||
|
||||
import "math"
|
||||
import "github.com/QuantumNous/new-api/common"
|
||||
|
||||
// QuotaRound converts a float64 quota value to int using half-away-from-zero
|
||||
// rounding. Every tiered billing path (pre-consume, settlement, breakdown
|
||||
// validation, log fields) MUST use this function to avoid +-1 discrepancies.
|
||||
// rounding with int32 saturation. Every tiered billing path (pre-consume,
|
||||
// settlement, breakdown validation, log fields) MUST use this function to
|
||||
// avoid +-1 discrepancies.
|
||||
//
|
||||
// The result saturates at int32 bounds: quota columns are 32-bit integers in
|
||||
// the database, and an oversized expression result must never wrap around
|
||||
// and turn a charge into a credit.
|
||||
// It delegates to common.QuotaRound so all quota rounding/conversion shares
|
||||
// one saturation + logging policy (see common/quota_math.go).
|
||||
func QuotaRound(f float64) int {
|
||||
r := math.Round(f)
|
||||
if math.IsNaN(r) {
|
||||
return 0
|
||||
}
|
||||
if r >= math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
if r <= math.MinInt32 {
|
||||
return math.MinInt32
|
||||
}
|
||||
return int(r)
|
||||
return common.QuotaRound(f)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package billingexpr
|
||||
|
||||
import "github.com/QuantumNous/new-api/common"
|
||||
|
||||
// quotaConversion converts raw expression output to quota based on the
|
||||
// expression version. This is the central dispatch point for future versions
|
||||
// that may use a different conversion formula.
|
||||
@@ -23,7 +25,7 @@ func ComputeTieredQuotaWithRequest(snap *BillingSnapshot, params TokenParams, re
|
||||
}
|
||||
|
||||
quotaBeforeGroup := quotaConversion(cost, snap)
|
||||
afterGroup := QuotaRound(quotaBeforeGroup * snap.GroupRatio)
|
||||
afterGroup, clamp := common.QuotaRoundChecked(quotaBeforeGroup * snap.GroupRatio)
|
||||
crossed := trace.MatchedTier != snap.EstimatedTier
|
||||
|
||||
return TieredResult{
|
||||
@@ -31,5 +33,6 @@ func ComputeTieredQuotaWithRequest(snap *BillingSnapshot, params TokenParams, re
|
||||
ActualQuotaAfterGroup: afterGroup,
|
||||
MatchedTier: trace.MatchedTier,
|
||||
CrossedTier: crossed,
|
||||
Clamp: clamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package billingexpr_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/pkg/billingexpr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestComputeTieredQuota_ClampOnOverflow guards the billing-safety invariant
|
||||
// that an oversized tiered settlement clamps to the int32 max instead of
|
||||
// wrapping into a credit, and that the saturation event is surfaced on the
|
||||
// result so callers can record it for admin auditing.
|
||||
func TestComputeTieredQuota_ClampOnOverflow(t *testing.T) {
|
||||
// exprOutput = p * 1e9 = 1e18; quotaBeforeGroup = 1e18 / 1e6 * 5e5 = 5e17,
|
||||
// which far exceeds MaxInt32 and must saturate.
|
||||
exprStr := `tier("base", p * 1000000000)`
|
||||
snap := &billingexpr.BillingSnapshot{
|
||||
BillingMode: "tiered_expr",
|
||||
ExprString: exprStr,
|
||||
ExprHash: billingexpr.ExprHashString(exprStr),
|
||||
GroupRatio: 1.0,
|
||||
QuotaPerUnit: 500_000,
|
||||
}
|
||||
|
||||
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 1_000_000_000})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, math.MaxInt32, result.ActualQuotaAfterGroup, "oversized quota must clamp to int32 max, never wrap negative")
|
||||
require.NotNil(t, result.Clamp, "clamp event must be surfaced so it can be audited")
|
||||
assert.Equal(t, common.QuotaClampOverflow, result.Clamp.Kind)
|
||||
assert.Equal(t, math.MaxInt32, result.Clamp.Clamped)
|
||||
}
|
||||
|
||||
// TestComputeTieredQuota_NoClampInRange confirms an in-range settlement leaves
|
||||
// Clamp nil, so the audit path is a no-op in the common case.
|
||||
func TestComputeTieredQuota_NoClampInRange(t *testing.T) {
|
||||
exprStr := `tier("base", p * 2 + c * 10)`
|
||||
snap := &billingexpr.BillingSnapshot{
|
||||
BillingMode: "tiered_expr",
|
||||
ExprString: exprStr,
|
||||
ExprHash: billingexpr.ExprHashString(exprStr),
|
||||
GroupRatio: 1.0,
|
||||
QuotaPerUnit: 500_000,
|
||||
}
|
||||
|
||||
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 1000, C: 500})
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, result.Clamp, "in-range settlement must not report a clamp")
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package billingexpr
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
type RequestInput struct {
|
||||
@@ -57,6 +59,11 @@ type TieredResult struct {
|
||||
ActualQuotaAfterGroup int `json:"actual_quota_after_group"`
|
||||
MatchedTier string `json:"matched_tier"`
|
||||
CrossedTier bool `json:"crossed_tier"`
|
||||
// Clamp records an int32 saturation event during quota conversion so the
|
||||
// caller can surface it on the consume log for admin auditing. Nil when no
|
||||
// clamping occurred. Not serialized: the marker is attached separately via
|
||||
// the shared quota-saturation audit path.
|
||||
Clamp *common.QuotaClamp `json:"-"`
|
||||
}
|
||||
|
||||
// ExprHashString returns the SHA-256 hex digest of an expression string.
|
||||
|
||||
Reference in New Issue
Block a user