diff --git a/backend/internal/handler/admin/channel_handler.go b/backend/internal/handler/admin/channel_handler.go index 9c058aa6..0e860fe0 100644 --- a/backend/internal/handler/admin/channel_handler.go +++ b/backend/internal/handler/admin/channel_handler.go @@ -2,6 +2,7 @@ package admin import ( "errors" + "fmt" "strconv" "strings" @@ -233,10 +234,26 @@ func validatePricingBillingMode(pricing []service.ChannelModelPricing) error { return errors.New("per-request price or intervals required for per_request/image billing mode") } } + // 校验 interval:至少有一个价格字段非空 + for _, iv := range p.Intervals { + if iv.InputPrice == nil && iv.OutputPrice == nil && + iv.CacheWritePrice == nil && iv.CacheReadPrice == nil && + iv.PerRequestPrice == nil { + return fmt.Errorf("interval [%d, %s] has no price fields set for model %v", + iv.MinTokens, formatMaxTokens(iv.MaxTokens), p.Models) + } + } } return nil } +func formatMaxTokens(max *int) string { + if max == nil { + return "∞" + } + return fmt.Sprintf("%d", *max) +} + // --- Handlers --- // List handles listing channels with pagination diff --git a/backend/internal/service/model_pricing_resolver.go b/backend/internal/service/model_pricing_resolver.go index b6f20da2..ffa03111 100644 --- a/backend/internal/service/model_pricing_resolver.go +++ b/backend/internal/service/model_pricing_resolver.go @@ -106,9 +106,12 @@ func (r *ModelPricingResolver) applyChannelOverrides(ctx context.Context, groupI // applyTokenOverrides 应用 token 模式的渠道覆盖 func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricing, resolved *ResolvedPricing) { - // 如果有区间定价,使用区间 - if len(chPricing.Intervals) > 0 { - resolved.Intervals = chPricing.Intervals + // 过滤掉所有价格字段都为空的无效 interval + validIntervals := filterValidIntervals(chPricing.Intervals) + + // 如果有有效的区间定价,使用区间 + if len(validIntervals) > 0 { + resolved.Intervals = validIntervals return } @@ -147,6 +150,20 @@ func (r *ModelPricingResolver) applyRequestTierOverrides(chPricing *ChannelModel } } +// filterValidIntervals 过滤掉所有价格字段都为空的无效 interval。 +// 前端可能创建了只有 min/max 但无价格的空 interval。 +func filterValidIntervals(intervals []PricingInterval) []PricingInterval { + var valid []PricingInterval + for _, iv := range intervals { + if iv.InputPrice != nil || iv.OutputPrice != nil || + iv.CacheWritePrice != nil || iv.CacheReadPrice != nil || + iv.PerRequestPrice != nil { + valid = append(valid, iv) + } + } + return valid +} + // GetIntervalPricing 根据 context token 数获取区间定价。 // 如果有区间列表,找到匹配区间并构造 ModelPricing;否则直接返回 BasePricing。 func (r *ModelPricingResolver) GetIntervalPricing(resolved *ResolvedPricing, totalContextTokens int) *ModelPricing { diff --git a/frontend/src/components/admin/channel/IntervalRow.vue b/frontend/src/components/admin/channel/IntervalRow.vue index 6f6e5826..21dcc90d 100644 --- a/frontend/src/components/admin/channel/IntervalRow.vue +++ b/frontend/src/components/admin/channel/IntervalRow.vue @@ -1,5 +1,6 @@