diff --git a/controller/channel_pricing.go b/controller/channel_pricing.go index 9768e70..9292ff9 100644 --- a/controller/channel_pricing.go +++ b/controller/channel_pricing.go @@ -66,6 +66,12 @@ type CreateChannelPricingRequest struct { AudioCompletionRatio float64 `json:"audio_completion_ratio"` } +// applyRequest 将请求字段应用到 ChannelPricing +func applyRequestFields(cp *model.ChannelPricing, req *CreateChannelPricingRequest) { + cp.ApplyFields(req.QuotaType, req.ModelRatio, req.CompletionRatio, req.ModelPrice, req.TagIds, + req.CacheRatio, req.CacheCreationRatio, req.ImageRatio, req.AudioRatio, req.AudioCompletionRatio) +} + // CreateChannelPricing 创建或更新渠道定价 func CreateChannelPricing(c *gin.Context) { var req CreateChannelPricingRequest @@ -82,17 +88,7 @@ func CreateChannelPricing(c *gin.Context) { // 检查是否已存在 existing, _ := model.GetChannelPricing(req.ModelName, req.ChannelId) if existing != nil { - // 更新 - existing.QuotaType = req.QuotaType - existing.ModelRatio = req.ModelRatio - existing.CompletionRatio = req.CompletionRatio - existing.ModelPrice = req.ModelPrice - existing.TagIds = req.TagIds - existing.CacheRatio = req.CacheRatio - existing.CacheCreationRatio = req.CacheCreationRatio - existing.ImageRatio = req.ImageRatio - existing.AudioRatio = req.AudioRatio - existing.AudioCompletionRatio = req.AudioCompletionRatio + applyRequestFields(existing, &req) if err := existing.Update(); err != nil { common.ApiError(c, err) return @@ -104,19 +100,10 @@ func CreateChannelPricing(c *gin.Context) { // 创建 cp := &model.ChannelPricing{ - ModelName: req.ModelName, - ChannelId: req.ChannelId, - QuotaType: req.QuotaType, - ModelRatio: req.ModelRatio, - CompletionRatio: req.CompletionRatio, - ModelPrice: req.ModelPrice, - TagIds: req.TagIds, - CacheRatio: req.CacheRatio, - CacheCreationRatio: req.CacheCreationRatio, - ImageRatio: req.ImageRatio, - AudioRatio: req.AudioRatio, - AudioCompletionRatio: req.AudioCompletionRatio, + ModelName: req.ModelName, + ChannelId: req.ChannelId, } + applyRequestFields(cp, &req) if err := cp.Insert(); err != nil { common.ApiError(c, err) return @@ -143,36 +130,20 @@ func BatchCreateChannelPricing(c *gin.Context) { pricings := make([]*model.ChannelPricing, 0, len(req.Items)) for _, item := range req.Items { - pricings = append(pricings, &model.ChannelPricing{ - ModelName: item.ModelName, - ChannelId: item.ChannelId, - QuotaType: item.QuotaType, - ModelRatio: item.ModelRatio, - CompletionRatio: item.CompletionRatio, - ModelPrice: item.ModelPrice, - TagIds: item.TagIds, - CacheRatio: item.CacheRatio, - CacheCreationRatio: item.CacheCreationRatio, - ImageRatio: item.ImageRatio, - AudioRatio: item.AudioRatio, - AudioCompletionRatio: item.AudioCompletionRatio, - }) + cp := &model.ChannelPricing{ + ModelName: item.ModelName, + ChannelId: item.ChannelId, + } + applyRequestFields(cp, item) + pricings = append(pricings, cp) } // 使用事务逐个处理(GORM 的批量 upsert 在不同数据库表现不一致) for _, cp := range pricings { existing, _ := model.GetChannelPricing(cp.ModelName, cp.ChannelId) if existing != nil { - existing.QuotaType = cp.QuotaType - existing.ModelRatio = cp.ModelRatio - existing.CompletionRatio = cp.CompletionRatio - existing.ModelPrice = cp.ModelPrice - existing.TagIds = cp.TagIds - existing.CacheRatio = cp.CacheRatio - existing.CacheCreationRatio = cp.CacheCreationRatio - existing.ImageRatio = cp.ImageRatio - existing.AudioRatio = cp.AudioRatio - existing.AudioCompletionRatio = cp.AudioCompletionRatio + existing.ApplyFields(cp.QuotaType, cp.ModelRatio, cp.CompletionRatio, cp.ModelPrice, cp.TagIds, + cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio) if err := existing.Update(); err != nil { common.ApiError(c, err) return @@ -241,18 +212,9 @@ func CopyGlobalPricing(c *gin.Context) { } // 获取全局扩展比率 - globalCacheRatio, hasCacheRatio := ratio_setting.GetCacheRatio(ability.Model) - if !hasCacheRatio { - globalCacheRatio = 0 - } - globalCacheCreationRatio, hasCacheCreationRatio := ratio_setting.GetCreateCacheRatio(ability.Model) - if !hasCacheCreationRatio { - globalCacheCreationRatio = 0 - } - globalImageRatio, hasImageRatio := ratio_setting.GetImageRatio(ability.Model) - if !hasImageRatio { - globalImageRatio = 0 - } + globalCacheRatio, _ := ratio_setting.GetCacheRatio(ability.Model) + globalCacheCreationRatio, _ := ratio_setting.GetCreateCacheRatio(ability.Model) + globalImageRatio, _ := ratio_setting.GetImageRatio(ability.Model) globalAudioRatio := ratio_setting.GetAudioRatio(ability.Model) globalAudioCompletionRatio := ratio_setting.GetAudioCompletionRatio(ability.Model) @@ -276,32 +238,18 @@ func CopyGlobalPricing(c *gin.Context) { } if existing != nil { - existing.QuotaType = quotaType - existing.ModelRatio = ratio - existing.CompletionRatio = completionRatio - existing.ModelPrice = price - existing.CacheRatio = globalCacheRatio - existing.CacheCreationRatio = globalCacheCreationRatio - existing.ImageRatio = globalImageRatio - existing.AudioRatio = globalAudioRatio - existing.AudioCompletionRatio = globalAudioCompletionRatio + existing.ApplyFields(quotaType, ratio, completionRatio, price, "", + globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio) if err := existing.Update(); err == nil { imported++ } } else { cp := &model.ChannelPricing{ - ModelName: ability.Model, - ChannelId: channelId, - QuotaType: quotaType, - ModelRatio: ratio, - CompletionRatio: completionRatio, - ModelPrice: price, - CacheRatio: globalCacheRatio, - CacheCreationRatio: globalCacheCreationRatio, - ImageRatio: globalImageRatio, - AudioRatio: globalAudioRatio, - AudioCompletionRatio: globalAudioCompletionRatio, + ModelName: ability.Model, + ChannelId: channelId, } + cp.ApplyFields(quotaType, ratio, completionRatio, price, "", + globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio) if err := cp.Insert(); err == nil { imported++ } diff --git a/model/channel_pricing.go b/model/channel_pricing.go index 9a5c05d..e7932b1 100644 --- a/model/channel_pricing.go +++ b/model/channel_pricing.go @@ -46,16 +46,40 @@ type ChannelPricing struct { AudioCompletionRatio float64 `json:"audio_completion_ratio" gorm:"default:0"` } +// setCache 写穿透缓存 +func setCache(key string, cp *ChannelPricing) { + channelPricingCacheLock.Lock() + channelPricingCache[key] = cp + channelPricingCacheLock.Unlock() +} + +func removeCache(key string) { + channelPricingCacheLock.Lock() + delete(channelPricingCache, key) + channelPricingCacheLock.Unlock() +} + +// ApplyFields 批量设置定价字段(消除 controller 层的重复赋值) +func (cp *ChannelPricing) ApplyFields(quotaType int, modelRatio, completionRatio, modelPrice float64, tagIds string, cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio float64) { + cp.QuotaType = quotaType + cp.ModelRatio = modelRatio + cp.CompletionRatio = completionRatio + cp.ModelPrice = modelPrice + cp.TagIds = tagIds + cp.CacheRatio = cacheRatio + cp.CacheCreationRatio = cacheCreationRatio + cp.ImageRatio = imageRatio + cp.AudioRatio = audioRatio + cp.AudioCompletionRatio = audioCompletionRatio +} + func (cp *ChannelPricing) Insert() error { now := common.GetTimestamp() cp.CreatedTime = now cp.UpdatedTime = now err := DB.Create(cp).Error if err == nil { - key := getChannelPricingCacheKey(cp.ModelName, cp.ChannelId) - channelPricingCacheLock.Lock() - channelPricingCache[key] = cp - channelPricingCacheLock.Unlock() + setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) } return err } @@ -68,10 +92,7 @@ func (cp *ChannelPricing) Update() error { "audio_ratio", "audio_completion_ratio", "updated_time"). Updates(cp).Error if err == nil { - key := getChannelPricingCacheKey(cp.ModelName, cp.ChannelId) - channelPricingCacheLock.Lock() - channelPricingCache[key] = cp - channelPricingCacheLock.Unlock() + setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) } return err } @@ -83,10 +104,7 @@ func (cp *ChannelPricing) Delete() error { } err := DB.Delete(cp).Error if err == nil { - key := getChannelPricingCacheKey(existing.ModelName, existing.ChannelId) - channelPricingCacheLock.Lock() - delete(channelPricingCache, key) - channelPricingCacheLock.Unlock() + removeCache(getChannelPricingCacheKey(existing.ModelName, existing.ChannelId)) } return err } @@ -134,7 +152,7 @@ func BatchUpsertChannelPricing(pricings []*ChannelPricing) error { } // 使用 GORM 的 OnConflict 实现 upsert // 唯一索引为 idx_model_channel (model_name, channel_id) - return DB.Clauses(clause.OnConflict{ + err := DB.Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "model_name"}, {Name: "channel_id"}, @@ -153,6 +171,12 @@ func BatchUpsertChannelPricing(pricings []*ChannelPricing) error { "updated_time", }), }).Create(&pricings).Error + if err == nil { + for _, cp := range pricings { + setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) + } + } + return err } // getChannelPricingCacheKey 生成缓存键 diff --git a/relay/helper/price.go b/relay/helper/price.go index 532f739..3523086 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -14,9 +14,6 @@ import ( "github.com/gin-gonic/gin" ) -// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration -const claudeCacheCreation1hMultiplier = 6 / 3.75 - // HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo { groupRatioInfo := types.GroupRatioInfo{ @@ -52,13 +49,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens var modelRatio float64 var completionRatio float64 var channelPricingFound bool - var cacheRatio float64 - var imageRatio float64 - var cacheCreationRatio float64 - var cacheCreationRatio5m float64 - var cacheCreationRatio1h float64 - var audioRatio float64 - var audioCompletionRatio float64 // 尝试获取渠道定价(优先于全局定价) channelMetaAvailable := info != nil && info.ChannelMeta != nil && info.ChannelId > 0 @@ -70,23 +60,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens modelPrice = cp.ModelPrice usePrice = cp.QuotaType == model.QuotaTypeByCall channelPricingFound = true - if cp.CacheRatio != 0 { - cacheRatio = cp.CacheRatio - } - if cp.CacheCreationRatio != 0 { - cacheCreationRatio = cp.CacheCreationRatio - cacheCreationRatio5m = cp.CacheCreationRatio - cacheCreationRatio1h = cp.CacheCreationRatio * claudeCacheCreation1hMultiplier - } - if cp.ImageRatio != 0 { - imageRatio = cp.ImageRatio - } - if cp.AudioRatio != 0 { - audioRatio = cp.AudioRatio - } - if cp.AudioCompletionRatio != 0 { - audioCompletionRatio = cp.AudioCompletionRatio - } if common.DebugEnabled { println(fmt.Sprintf("[ChannelPricing] hit: model=%s channel=%d source=cache", info.OriginModelName, info.ChannelId)) } @@ -123,24 +96,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens } completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName) } - if cacheRatio == 0 { - cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName) - } - if cacheCreationRatio == 0 { - cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName) - cacheCreationRatio5m = cacheCreationRatio - // 固定1h和5min缓存写入价格的比例 - cacheCreationRatio1h = cacheCreationRatio * claudeCacheCreation1hMultiplier - } - if imageRatio == 0 { - imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName) - } - if audioRatio == 0 { - audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName) - } - if audioCompletionRatio == 0 { - audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName) - } ratio := modelRatio * groupRatioInfo.GroupRatio preConsumedQuota = int(float64(preConsumedTokens) * ratio) } else { @@ -170,20 +125,38 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens } priceData := types.PriceData{ - FreeModel: freeModel, - ModelPrice: modelPrice, - ModelRatio: modelRatio, - CompletionRatio: completionRatio, - GroupRatioInfo: groupRatioInfo, - UsePrice: usePrice, - CacheRatio: cacheRatio, - ImageRatio: imageRatio, - AudioRatio: audioRatio, - AudioCompletionRatio: audioCompletionRatio, - CacheCreationRatio: cacheCreationRatio, - CacheCreation5mRatio: cacheCreationRatio5m, - CacheCreation1hRatio: cacheCreationRatio1h, - QuotaToPreConsume: preConsumedQuota, + FreeModel: freeModel, + ModelPrice: modelPrice, + ModelRatio: modelRatio, + CompletionRatio: completionRatio, + GroupRatioInfo: groupRatioInfo, + UsePrice: usePrice, + QuotaToPreConsume: preConsumedQuota, + } + + // 应用渠道定价的扩展比率(非零值覆盖),然后回退全局比率 + if channelMetaAvailable { + if cp, found := model.GetEffectivePricing(info.OriginModelName, info.ChannelId); found { + priceData.ApplyChannelPricingRatios(cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio) + } + } + // 全局比率作为回退(仅当对应字段仍为 0 时生效) + if priceData.CacheRatio == 0 { + priceData.CacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName) + } + if priceData.CacheCreationRatio == 0 { + priceData.CacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName) + priceData.CacheCreation5mRatio = priceData.CacheCreationRatio + priceData.CacheCreation1hRatio = priceData.CacheCreationRatio * types.ClaudeCacheCreation1hMultiplier + } + if priceData.ImageRatio == 0 { + priceData.ImageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName) + } + if priceData.AudioRatio == 0 { + priceData.AudioRatio = ratio_setting.GetAudioRatio(info.OriginModelName) + } + if priceData.AudioCompletionRatio == 0 { + priceData.AudioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName) } if common.DebugEnabled { @@ -261,23 +234,7 @@ func UpdatePriceDataForChannelPricing(c *gin.Context, info *relaycommon.RelayInf info.PriceData.ModelPrice = -1 } - if cp.CacheRatio != 0 { - info.PriceData.CacheRatio = cp.CacheRatio - } - if cp.CacheCreationRatio != 0 { - info.PriceData.CacheCreationRatio = cp.CacheCreationRatio - info.PriceData.CacheCreation5mRatio = cp.CacheCreationRatio - info.PriceData.CacheCreation1hRatio = cp.CacheCreationRatio * claudeCacheCreation1hMultiplier - } - if cp.ImageRatio != 0 { - info.PriceData.ImageRatio = cp.ImageRatio - } - if cp.AudioRatio != 0 { - info.PriceData.AudioRatio = cp.AudioRatio - } - if cp.AudioCompletionRatio != 0 { - info.PriceData.AudioCompletionRatio = cp.AudioCompletionRatio - } + info.PriceData.ApplyChannelPricingRatios(cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio) if info.PriceData.UsePrice { info.PriceData.QuotaToPreConsume = int( diff --git a/types/price_data.go b/types/price_data.go index 93bc6ae..6076f3b 100644 --- a/types/price_data.go +++ b/types/price_data.go @@ -2,6 +2,10 @@ package types import "fmt" +// ClaudeCacheCreation1hMultiplier 1小时缓存写入价格相对于5分钟的比例 +// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration +const ClaudeCacheCreation1hMultiplier = 6 / 3.75 + type GroupRatioInfo struct { GroupRatio float64 GroupSpecialRatio float64 @@ -27,6 +31,28 @@ type PriceData struct { GroupRatioInfo GroupRatioInfo } +// ApplyChannelPricingRatios 将渠道定价的扩展比率应用到 PriceData(非零值覆盖) +// cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio +func (p *PriceData) ApplyChannelPricingRatios(cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio float64) { + if cacheRatio != 0 { + p.CacheRatio = cacheRatio + } + if cacheCreationRatio != 0 { + p.CacheCreationRatio = cacheCreationRatio + p.CacheCreation5mRatio = cacheCreationRatio + p.CacheCreation1hRatio = cacheCreationRatio * ClaudeCacheCreation1hMultiplier + } + if imageRatio != 0 { + p.ImageRatio = imageRatio + } + if audioRatio != 0 { + p.AudioRatio = audioRatio + } + if audioCompletionRatio != 0 { + p.AudioCompletionRatio = audioCompletionRatio + } +} + func (p *PriceData) AddOtherRatio(key string, ratio float64) { if p.OtherRatios == nil { p.OtherRatios = make(map[string]float64)