fix(channels): supported models = mapping ∪ pricing with global LiteLLM fallback
Why: channels with model pricing entries but no model mapping (e.g. azcc with 3 priced claude models, no mapping) were rendering as 未配置模型 in the 'Available Channels' page. The algorithm only iterated ModelMapping and silently dropped any platform without a mapping entry. Changes: - channel.go: SupportedModels now unions mapping + pricing entries. For exact mapping src → target, pricing is looked up by target (the actually billed name), not by src. - channel_available.go: ListAvailable enriches each entry with nil pricing via PricingService.GetModelPricing (global LiteLLM fallback) so the popover always shows a price. - channel_service.go: NewChannelService takes *PricingService as 4th param. - channel_test.go: rewrote 4 tests that froze the old mapping-only semantics; added pricing-only / mapping-target / target-missing coverage.
This commit is contained in:
@@ -36,7 +36,10 @@ type AvailableChannel struct {
|
||||
|
||||
// ListAvailable 返回所有渠道的可用视图:每个渠道附带关联分组信息与支持模型列表。
|
||||
//
|
||||
// 支持模型通过 (*Channel).SupportedModels() 计算得到(见 channel.go)。
|
||||
// 支持模型通过 (*Channel).SupportedModels() 计算(mapping ∪ pricing 并联)。
|
||||
// 对于渠道未配置定价的模型,进一步用 PricingService 的全局 LiteLLM 数据合成
|
||||
// 一份展示用定价,让用户看到默认价格而非"未配置"。
|
||||
//
|
||||
// 关联分组信息通过 groupRepo.ListActive 查询后按 ID 映射;渠道 GroupIDs 中未在活跃列表中
|
||||
// 的分组(已停用或删除)会被忽略。
|
||||
//
|
||||
@@ -78,6 +81,9 @@ func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel,
|
||||
|
||||
ch.normalizeBillingModelSource()
|
||||
|
||||
supported := ch.SupportedModels()
|
||||
s.fillGlobalPricingFallback(supported)
|
||||
|
||||
out = append(out, AvailableChannel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
@@ -86,7 +92,7 @@ func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel,
|
||||
BillingModelSource: ch.BillingModelSource,
|
||||
RestrictModels: ch.RestrictModels,
|
||||
Groups: groups,
|
||||
SupportedModels: ch.SupportedModels(),
|
||||
SupportedModels: supported,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,3 +101,49 @@ func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel,
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// fillGlobalPricingFallback 对未命中渠道定价的支持模型,从全局 LiteLLM 数据合成一份
|
||||
// 展示用定价(按 token 计费)。仅用于「可用渠道」展示,不影响真实计费链路。
|
||||
//
|
||||
// 当 s.pricingService 为 nil(测试场景),跳过回落。
|
||||
func (s *ChannelService) fillGlobalPricingFallback(models []SupportedModel) {
|
||||
if s.pricingService == nil {
|
||||
return
|
||||
}
|
||||
for i := range models {
|
||||
if models[i].Pricing != nil {
|
||||
continue
|
||||
}
|
||||
lp := s.pricingService.GetModelPricing(models[i].Name)
|
||||
if lp == nil {
|
||||
continue
|
||||
}
|
||||
models[i].Pricing = synthesizePricingFromLiteLLM(lp)
|
||||
}
|
||||
}
|
||||
|
||||
// synthesizePricingFromLiteLLM 把 LiteLLM 的定价数据转成 ChannelModelPricing 形态,
|
||||
// 仅用于展示。BillingMode 固定为 token;图片场景的 OutputCostPerImageToken 也归到
|
||||
// ImageOutputPrice 字段(与渠道侧"图片输出按 token 计价"语义一致)。
|
||||
//
|
||||
// LiteLLM 中字段 0 视为未配置,不带入展示。
|
||||
func synthesizePricingFromLiteLLM(lp *LiteLLMModelPricing) *ChannelModelPricing {
|
||||
if lp == nil {
|
||||
return nil
|
||||
}
|
||||
return &ChannelModelPricing{
|
||||
BillingMode: BillingModeToken,
|
||||
InputPrice: nonZeroPtr(lp.InputCostPerToken),
|
||||
OutputPrice: nonZeroPtr(lp.OutputCostPerToken),
|
||||
CacheWritePrice: nonZeroPtr(lp.CacheCreationInputTokenCost),
|
||||
CacheReadPrice: nonZeroPtr(lp.CacheReadInputTokenCost),
|
||||
ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken),
|
||||
}
|
||||
}
|
||||
|
||||
func nonZeroPtr(v float64) *float64 {
|
||||
if v == 0 {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user