fix: resolve 5 audit findings in channel/credits/scheduling
P0-1: Credits degraded response retry + fail-open - Add isAntigravityDegradedResponse() to detect transient API failures - Retry up to 3 times with exponential backoff (500ms/1s/2s) - Invalidate singleflight cache between retries - Fail-open after exhausting retries instead of 5h circuit break P1-1: Fix channel restriction pre-check timing conflict - Swap checkClaudeCodeRestriction before checkChannelPricingRestriction - Ensures channel restriction is checked against final fallback groupID P1-2: Add interval pricing validation (frontend + backend) - Backend: ValidateIntervals() with boundary, price, overlap checks - Frontend: validateIntervals() with Chinese error messages - Rules: MinTokens>=0, MaxTokens>MinTokens, prices>=0, no overlap P2: Fix cross-platform same-model pricing/mapping override - Store cache keys using original platform instead of group platform - Lookup across matching platforms (antigravity→anthropic→gemini) - Prevents anthropic/gemini same-name models from overwriting each other
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -177,6 +179,94 @@ func (c *Channel) Clone() *Channel {
|
||||
return &cp
|
||||
}
|
||||
|
||||
// ValidateIntervals 校验区间列表的合法性。
|
||||
// 规则:MinTokens >= 0;MaxTokens 若非 nil 则 > 0 且 > MinTokens;
|
||||
// 所有价格字段 >= 0;区间按 MinTokens 排序后无重叠((min, max] 语义);
|
||||
// 无界区间(MaxTokens=nil)必须是最后一个。间隙允许(回退默认价格)。
|
||||
func ValidateIntervals(intervals []PricingInterval) error {
|
||||
if len(intervals) == 0 {
|
||||
return nil
|
||||
}
|
||||
sorted := make([]PricingInterval, len(intervals))
|
||||
copy(sorted, intervals)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].MinTokens < sorted[j].MinTokens
|
||||
})
|
||||
|
||||
for i := range sorted {
|
||||
if err := validateSingleInterval(&sorted[i], i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return validateIntervalOverlap(sorted)
|
||||
}
|
||||
|
||||
// validateSingleInterval 校验单个区间的字段合法性
|
||||
func validateSingleInterval(iv *PricingInterval, idx int) error {
|
||||
if iv.MinTokens < 0 {
|
||||
return fmt.Errorf("interval #%d: min_tokens (%d) must be >= 0", idx+1, iv.MinTokens)
|
||||
}
|
||||
if iv.MaxTokens != nil {
|
||||
if *iv.MaxTokens <= 0 {
|
||||
return fmt.Errorf("interval #%d: max_tokens (%d) must be > 0", idx+1, *iv.MaxTokens)
|
||||
}
|
||||
if *iv.MaxTokens <= iv.MinTokens {
|
||||
return fmt.Errorf("interval #%d: max_tokens (%d) must be > min_tokens (%d)",
|
||||
idx+1, *iv.MaxTokens, iv.MinTokens)
|
||||
}
|
||||
}
|
||||
return validateIntervalPrices(iv, idx)
|
||||
}
|
||||
|
||||
// validateIntervalPrices 校验区间内所有价格字段 >= 0
|
||||
func validateIntervalPrices(iv *PricingInterval, idx int) error {
|
||||
prices := []struct {
|
||||
name string
|
||||
val *float64
|
||||
}{
|
||||
{"input_price", iv.InputPrice},
|
||||
{"output_price", iv.OutputPrice},
|
||||
{"cache_write_price", iv.CacheWritePrice},
|
||||
{"cache_read_price", iv.CacheReadPrice},
|
||||
{"per_request_price", iv.PerRequestPrice},
|
||||
}
|
||||
for _, p := range prices {
|
||||
if p.val != nil && *p.val < 0 {
|
||||
return fmt.Errorf("interval #%d: %s must be >= 0", idx+1, p.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateIntervalOverlap 校验排序后的区间列表无重叠,且无界区间在最后
|
||||
func validateIntervalOverlap(sorted []PricingInterval) error {
|
||||
for i, iv := range sorted {
|
||||
// 无界区间必须是最后一个
|
||||
if iv.MaxTokens == nil && i < len(sorted)-1 {
|
||||
return fmt.Errorf("interval #%d: unbounded interval (max_tokens=null) must be the last one",
|
||||
i+1)
|
||||
}
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
prev := sorted[i-1]
|
||||
// 检查重叠:前一个区间的上界 > 当前区间的下界则重叠
|
||||
// (min, max] 语义:prev 覆盖 (prev.Min, prev.Max],cur 覆盖 (cur.Min, cur.Max]
|
||||
if prev.MaxTokens == nil || *prev.MaxTokens > iv.MinTokens {
|
||||
return fmt.Errorf("interval #%d and #%d overlap: prev max=%s > cur min=%d",
|
||||
i, i+1, formatMaxTokensLabel(prev.MaxTokens), iv.MinTokens)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatMaxTokensLabel(max *int) string {
|
||||
if max == nil {
|
||||
return "∞"
|
||||
}
|
||||
return fmt.Sprintf("%d", *max)
|
||||
}
|
||||
|
||||
// ChannelUsageFields 渠道相关的使用记录字段(嵌入到各平台的 RecordUsageInput 中)
|
||||
type ChannelUsageFields struct {
|
||||
ChannelID int64 // 渠道 ID(0 = 无渠道)
|
||||
|
||||
Reference in New Issue
Block a user