257 lines
8.4 KiB
Go
257 lines
8.4 KiB
Go
package service
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"sort"
|
|||
|
|
"strings"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// PlazaOfficialPricing 模型广场展示用的 LiteLLM 官方参考价(USD per token)。
|
|||
|
|
// 字段为 nil 表示官方数据中该项缺失(0 视为未配置)。
|
|||
|
|
type PlazaOfficialPricing struct {
|
|||
|
|
InputPrice *float64
|
|||
|
|
OutputPrice *float64
|
|||
|
|
CacheWritePrice *float64 // 5m 缓存写入(= LiteLLM cache_creation)
|
|||
|
|
CacheWrite1hPrice *float64 // 1h 缓存写入(LiteLLM cache_creation_above_1hr)
|
|||
|
|
CacheReadPrice *float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PlazaModel 模型广场中单个模型条目:渠道定价 + 官方参考价。
|
|||
|
|
type PlazaModel struct {
|
|||
|
|
Name string
|
|||
|
|
Platform string
|
|||
|
|
Pricing *ChannelModelPricing
|
|||
|
|
OfficialPricing *PlazaOfficialPricing
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PlazaGroup 模型广场中以分组为顶层的条目。
|
|||
|
|
//
|
|||
|
|
// 与 AvailableGroupRef 相比多了 Description 与 Models;Models 来自该分组关联渠道的
|
|||
|
|
// 支持模型(普通分组按分组平台隔离,Composite 分组展开关联渠道已配置的
|
|||
|
|
// 具体平台),与「可用渠道」页口径一致。
|
|||
|
|
type PlazaGroup struct {
|
|||
|
|
ID int64
|
|||
|
|
Name string
|
|||
|
|
Description string
|
|||
|
|
Platform string
|
|||
|
|
SubscriptionType string
|
|||
|
|
RateMultiplier float64
|
|||
|
|
PeakRateEnabled bool
|
|||
|
|
PeakStart string
|
|||
|
|
PeakEnd string
|
|||
|
|
PeakRateMultiplier float64
|
|||
|
|
IsExclusive bool
|
|||
|
|
// 图片按次实付倍率:ImageRateIndependent 为 true 时,图片计费模型的实付
|
|||
|
|
// = 档位价 × ImageRateMultiplier,不乘分组/用户专属倍率(与计费口径一致)。
|
|||
|
|
ImageRateIndependent bool
|
|||
|
|
ImageRateMultiplier float64
|
|||
|
|
Models []PlazaModel
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListPlazaGroups 返回模型广场数据:每个活跃分组附带其可用模型与定价。
|
|||
|
|
//
|
|||
|
|
// 聚合口径与 ListAvailable 一致(Active 渠道、SupportedModels ∪ 全局定价回落、
|
|||
|
|
// 平台隔离),仅把顶层从渠道换成分组:
|
|||
|
|
// - 渠道按 lower(name) 排序后遍历,保证同名模型去重结果确定;
|
|||
|
|
// - 同分组同名模型「先见者胜」,仅当已存条目无定价而新条目有定价时升级替换;
|
|||
|
|
// - 图片计费模型的档位价按实收口径合成(分组图片价 > 渠道档位价 > 渠道默认按次价,
|
|||
|
|
// 见 plazaImageDisplayPricing);
|
|||
|
|
// - 每个模型附带 LiteLLM 官方参考价(查不到为 nil);
|
|||
|
|
// - 只返回 Models 非空的分组;分组按 RateMultiplier 升序(同倍率按名称),
|
|||
|
|
// 组内模型按名称排序。
|
|||
|
|
//
|
|||
|
|
// 可见性过滤(专属分组)不在此层做,由 handler 按登录态裁剪。
|
|||
|
|
func (s *ChannelService) ListPlazaGroups(ctx context.Context) ([]PlazaGroup, error) {
|
|||
|
|
channels, err := s.repo.ListAll(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("list channels: %w", err)
|
|||
|
|
}
|
|||
|
|
groups, err := s.groupRepo.ListActive(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("list active groups: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sort.SliceStable(channels, func(i, j int) bool {
|
|||
|
|
return strings.ToLower(channels[i].Name) < strings.ToLower(channels[j].Name)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
byGroup := make(map[int64]*PlazaGroup, len(groups))
|
|||
|
|
groupEnt := make(map[int64]*Group, len(groups))
|
|||
|
|
order := make([]int64, 0, len(groups))
|
|||
|
|
for i := range groups {
|
|||
|
|
g := &groups[i]
|
|||
|
|
byGroup[g.ID] = &PlazaGroup{
|
|||
|
|
ID: g.ID,
|
|||
|
|
Name: g.Name,
|
|||
|
|
Description: g.Description,
|
|||
|
|
Platform: g.Platform,
|
|||
|
|
SubscriptionType: g.SubscriptionType,
|
|||
|
|
RateMultiplier: g.RateMultiplier,
|
|||
|
|
PeakRateEnabled: g.PeakRateEnabled,
|
|||
|
|
PeakStart: g.PeakStart,
|
|||
|
|
PeakEnd: g.PeakEnd,
|
|||
|
|
PeakRateMultiplier: g.PeakRateMultiplier,
|
|||
|
|
IsExclusive: g.IsExclusive,
|
|||
|
|
ImageRateIndependent: g.ImageRateIndependent,
|
|||
|
|
ImageRateMultiplier: g.ImageRateMultiplier,
|
|||
|
|
}
|
|||
|
|
groupEnt[g.ID] = g
|
|||
|
|
order = append(order, g.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type modelKey struct {
|
|||
|
|
platform string
|
|||
|
|
name string
|
|||
|
|
}
|
|||
|
|
// modelIdx[groupID][platform+modelName] = index into byGroup[groupID].Models
|
|||
|
|
modelIdx := make(map[int64]map[modelKey]int, len(groups))
|
|||
|
|
for i := range channels {
|
|||
|
|
ch := &channels[i]
|
|||
|
|
if ch.Status != StatusActive {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
ch.normalizeBillingModelSource()
|
|||
|
|
supported := ch.SupportedModels()
|
|||
|
|
s.fillGlobalPricingFallback(supported)
|
|||
|
|
|
|||
|
|
for _, gid := range ch.GroupIDs {
|
|||
|
|
pg, ok := byGroup[gid]
|
|||
|
|
if !ok {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
idx := modelIdx[gid]
|
|||
|
|
if idx == nil {
|
|||
|
|
idx = make(map[modelKey]int, len(supported))
|
|||
|
|
modelIdx[gid] = idx
|
|||
|
|
}
|
|||
|
|
for j := range supported {
|
|||
|
|
m := supported[j]
|
|||
|
|
if pg.Platform == PlatformComposite {
|
|||
|
|
if !isConcreteRequestPlatform(m.Platform) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
} else if m.Platform != pg.Platform {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
pricing := plazaImageDisplayPricing(m.Pricing, groupEnt[gid])
|
|||
|
|
key := modelKey{platform: m.Platform, name: m.Name}
|
|||
|
|
if at, seen := idx[key]; seen {
|
|||
|
|
// 先见者胜;仅当已存条目无定价而新条目有定价时升级。
|
|||
|
|
if pg.Models[at].Pricing == nil && pricing != nil {
|
|||
|
|
pg.Models[at].Pricing = pricing
|
|||
|
|
}
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
idx[key] = len(pg.Models)
|
|||
|
|
pg.Models = append(pg.Models, PlazaModel{
|
|||
|
|
Name: m.Name,
|
|||
|
|
Platform: m.Platform,
|
|||
|
|
Pricing: pricing,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
officialMemo := make(map[string]*PlazaOfficialPricing)
|
|||
|
|
out := make([]PlazaGroup, 0, len(order))
|
|||
|
|
for _, gid := range order {
|
|||
|
|
pg := byGroup[gid]
|
|||
|
|
if len(pg.Models) == 0 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
sort.SliceStable(pg.Models, func(i, j int) bool {
|
|||
|
|
if pg.Models[i].Name != pg.Models[j].Name {
|
|||
|
|
return pg.Models[i].Name < pg.Models[j].Name
|
|||
|
|
}
|
|||
|
|
return pg.Models[i].Platform < pg.Models[j].Platform
|
|||
|
|
})
|
|||
|
|
for j := range pg.Models {
|
|||
|
|
pg.Models[j].OfficialPricing = s.lookupOfficialPricing(pg.Models[j].Name, officialMemo)
|
|||
|
|
}
|
|||
|
|
out = append(out, *pg)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|||
|
|
if out[i].RateMultiplier != out[j].RateMultiplier {
|
|||
|
|
return out[i].RateMultiplier < out[j].RateMultiplier
|
|||
|
|
}
|
|||
|
|
return out[i].Name < out[j].Name
|
|||
|
|
})
|
|||
|
|
return out, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// plazaImageDisplayPricing 为图片计费模型合成展示定价,使档位价与实收口径一致:
|
|||
|
|
// 每档(1K/2K/4K)单价 = 分组图片价 > 渠道同档位价 > 渠道默认按次价,无价的档不展示。
|
|||
|
|
// 分组未配任何图片价、或定价非图片模式时原样返回。返回克隆,不修改入参
|
|||
|
|
// (渠道定价指针指向缓存共享数据)。
|
|||
|
|
func plazaImageDisplayPricing(p *ChannelModelPricing, g *Group) *ChannelModelPricing {
|
|||
|
|
if p == nil || g == nil || p.BillingMode != BillingModeImage {
|
|||
|
|
return p
|
|||
|
|
}
|
|||
|
|
if g.ImagePrice1K == nil && g.ImagePrice2K == nil && g.ImagePrice4K == nil {
|
|||
|
|
return p
|
|||
|
|
}
|
|||
|
|
channelTierPrice := func(label string) *float64 {
|
|||
|
|
for i := range p.Intervals {
|
|||
|
|
if p.Intervals[i].TierLabel == label && p.Intervals[i].PerRequestPrice != nil {
|
|||
|
|
return p.Intervals[i].PerRequestPrice
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return p.PerRequestPrice
|
|||
|
|
}
|
|||
|
|
tiers := []struct {
|
|||
|
|
label string
|
|||
|
|
groupPrice *float64
|
|||
|
|
}{
|
|||
|
|
{"1K", g.ImagePrice1K},
|
|||
|
|
{"2K", g.ImagePrice2K},
|
|||
|
|
{"4K", g.ImagePrice4K},
|
|||
|
|
}
|
|||
|
|
clone := *p
|
|||
|
|
clone.Intervals = make([]PricingInterval, 0, len(tiers))
|
|||
|
|
for i, t := range tiers {
|
|||
|
|
price := t.groupPrice
|
|||
|
|
if price == nil {
|
|||
|
|
price = channelTierPrice(t.label)
|
|||
|
|
}
|
|||
|
|
if price == nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
v := *price
|
|||
|
|
clone.Intervals = append(clone.Intervals, PricingInterval{
|
|||
|
|
TierLabel: t.label,
|
|||
|
|
PerRequestPrice: &v,
|
|||
|
|
SortOrder: i,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return &clone
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// lookupOfficialPricing 查询模型的 LiteLLM 官方参考价,带 memo 避免同名模型重复转换。
|
|||
|
|
// pricingService 为 nil(测试场景)或查不到时返回 nil。
|
|||
|
|
func (s *ChannelService) lookupOfficialPricing(modelName string, memo map[string]*PlazaOfficialPricing) *PlazaOfficialPricing {
|
|||
|
|
if s.pricingService == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if cached, ok := memo[modelName]; ok {
|
|||
|
|
return cached
|
|||
|
|
}
|
|||
|
|
var result *PlazaOfficialPricing
|
|||
|
|
if lp := s.pricingService.GetModelPricing(modelName); lp != nil && !lp.TokenPricingAbsent {
|
|||
|
|
result = &PlazaOfficialPricing{
|
|||
|
|
InputPrice: nonZeroPtr(lp.InputCostPerToken),
|
|||
|
|
OutputPrice: nonZeroPtr(lp.OutputCostPerToken),
|
|||
|
|
CacheWritePrice: nonZeroPtr(lp.CacheCreationInputTokenCost),
|
|||
|
|
CacheWrite1hPrice: nonZeroPtr(lp.CacheCreationInputTokenCostAbove1hr),
|
|||
|
|
CacheReadPrice: nonZeroPtr(lp.CacheReadInputTokenCost),
|
|||
|
|
}
|
|||
|
|
if result.InputPrice == nil && result.OutputPrice == nil &&
|
|||
|
|
result.CacheWritePrice == nil && result.CacheWrite1hPrice == nil && result.CacheReadPrice == nil {
|
|||
|
|
result = nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
memo[modelName] = result
|
|||
|
|
return result
|
|||
|
|
}
|