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:
CaIon
2026-07-07 12:20:07 +08:00
parent c9943d37ad
commit bae799ccb1
32 changed files with 710 additions and 104 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
} else if duration > 0 {
// 计算 token: ceil(duration) / 60.0 * 1000,即每分钟 1000 tokens。
// duration 解析自上游返回的音频元数据,饱和转换防止 int 回绕。
completionTokens := common.QuotaFromFloat(math.Round(math.Ceil(duration) / 60.0 * 1000))
completionTokens := common.QuotaRound(math.Ceil(duration) / 60.0 * 1000)
usage.CompletionTokens = completionTokens
usage.CompletionTokenDetails.AudioTokens = completionTokens
}
+5
View File
@@ -163,6 +163,11 @@ type RelayInfo struct {
PriceData types.PriceData
// QuotaClamp is set (non-nil) when a quota conversion saturated at the
// int32 bound (or NaN fallback) while computing this request's charge.
// It is surfaced onto the consume/task log's admin_info for auditing.
QuotaClamp *common.QuotaClamp
// TieredBillingSnapshot is a frozen snapshot of tiered billing rules
// captured at pre-consume time. Non-nil only when billing mode is "tiered_expr".
TieredBillingSnapshot *billingexpr.BillingSnapshot
+18 -2
View File
@@ -202,7 +202,9 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
quotaWithRatios *= ra
}
}
info.PriceData.Quota = common.QuotaFromFloat(quotaWithRatios)
quota, clamp := common.QuotaFromFloatChecked(quotaWithRatios)
info.PriceData.Quota = quota
noteTaskQuotaClamp(info, clamp)
}
// 7. 预扣费(仅首次 — 重试时 info.Billing 已存在,跳过)
@@ -278,7 +280,21 @@ func recalcQuotaFromRatios(info *relaycommon.RelayInfo, ratios map[string]float6
result *= ra
}
}
return common.QuotaFromFloat(result)
quota, clamp := common.QuotaFromFloatChecked(result)
noteTaskQuotaClamp(info, clamp)
return quota
}
// noteTaskQuotaClamp records the first quota saturation event onto the task's
// RelayInfo so LogTaskConsumption can surface it on the submit log's
// admin_info. First non-nil clamp wins.
func noteTaskQuotaClamp(info *relaycommon.RelayInfo, clamp *common.QuotaClamp) {
if clamp == nil || info == nil {
return
}
if info.QuotaClamp == nil {
info.QuotaClamp = clamp
}
}
var fetchRespBuilders = map[int]func(c *gin.Context) (respBody []byte, taskResp *dto.TaskError){