Delete leftover channel pricing controllers and routes after removing channel pricing, and align relay price helpers with global pricing plus user channel ratio only. Co-Authored-By: Codex <noreply@anthropic.com>master
| @@ -1,359 +0,0 @@ | |||||
| package controller | |||||
| import ( | |||||
| "fmt" | |||||
| "strconv" | |||||
| "strings" | |||||
| "github.com/QuantumNous/new-api/common" | |||||
| "github.com/QuantumNous/new-api/model" | |||||
| "github.com/QuantumNous/new-api/setting/ratio_setting" | |||||
| "github.com/gin-gonic/gin" | |||||
| ) | |||||
| // GetAllChannelPricing 获取所有渠道定价(分页) | |||||
| func GetAllChannelPricing(c *gin.Context) { | |||||
| page, _ := strconv.Atoi(c.DefaultQuery("p", "1")) | |||||
| pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) | |||||
| offset := (page - 1) * pageSize | |||||
| list, total, err := model.GetAllChannelPricing(offset, pageSize) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.ApiSuccess(c, gin.H{ | |||||
| "page": page, | |||||
| "page_size": pageSize, | |||||
| "total": total, | |||||
| "items": list, | |||||
| }) | |||||
| } | |||||
| // GetChannelPricingByModel 获取指定模型的所有渠道定价 | |||||
| func GetChannelPricingByModel(c *gin.Context) { | |||||
| modelName := c.Param("name") | |||||
| if modelName == "" { | |||||
| common.ApiErrorMsg(c, "model name is required") | |||||
| return | |||||
| } | |||||
| list, err := model.GetChannelPricingByModel(modelName) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.ApiSuccess(c, list) | |||||
| } | |||||
| // CreateChannelPricingRequest 创建渠道定价请求 | |||||
| type CreateChannelPricingRequest struct { | |||||
| Id int `json:"id"` | |||||
| ModelName string `json:"model_name" binding:"required"` | |||||
| ChannelId int `json:"channel_id" binding:"required"` | |||||
| QuotaType int `json:"quota_type"` | |||||
| ModelRatio float64 `json:"model_ratio"` | |||||
| CompletionRatio float64 `json:"completion_ratio"` | |||||
| ModelPrice float64 `json:"model_price"` | |||||
| TagIds string `json:"tag_ids"` | |||||
| CacheRatio float64 `json:"cache_ratio"` | |||||
| CacheCreationRatio float64 `json:"cache_creation_ratio"` | |||||
| ImageRatio float64 `json:"image_ratio"` | |||||
| AudioRatio float64 `json:"audio_ratio"` | |||||
| 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 | |||||
| if err := c.ShouldBindJSON(&req); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| if req.CacheRatio < 0 || req.CacheCreationRatio < 0 || req.ImageRatio < 0 || req.AudioRatio < 0 || req.AudioCompletionRatio < 0 { | |||||
| common.ApiErrorMsg(c, "ratio values must be >= 0") | |||||
| return | |||||
| } | |||||
| // 检查是否已存在 | |||||
| existing, _ := model.GetChannelPricing(req.ModelName, req.ChannelId) | |||||
| if existing != nil { | |||||
| applyRequestFields(existing, &req) | |||||
| if err := existing.Update(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.SysLog(fmt.Sprintf("[ChannelPricing] updated: id=%d model=%s channel=%d", existing.Id, existing.ModelName, existing.ChannelId)) | |||||
| common.ApiSuccess(c, existing) | |||||
| return | |||||
| } | |||||
| // 创建 | |||||
| cp := &model.ChannelPricing{ | |||||
| ModelName: req.ModelName, | |||||
| ChannelId: req.ChannelId, | |||||
| } | |||||
| applyRequestFields(cp, &req) | |||||
| if err := cp.Insert(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.SysLog(fmt.Sprintf("[ChannelPricing] created: model=%s channel=%d quotaType=%d modelRatio=%.4f completionRatio=%.4f modelPrice=%.4f cacheRatio=%.4f cacheCreationRatio=%.4f imageRatio=%.4f audioRatio=%.4f audioCompletionRatio=%.4f", | |||||
| req.ModelName, req.ChannelId, req.QuotaType, req.ModelRatio, req.CompletionRatio, req.ModelPrice, | |||||
| req.CacheRatio, req.CacheCreationRatio, req.ImageRatio, req.AudioRatio, req.AudioCompletionRatio)) | |||||
| common.ApiSuccess(c, cp) | |||||
| } | |||||
| // BatchCreateChannelPricingRequest 批量创建请求 | |||||
| type BatchCreateChannelPricingRequest struct { | |||||
| Items []*CreateChannelPricingRequest `json:"items" binding:"required"` | |||||
| } | |||||
| // BatchCreateChannelPricing 批量创建或更新渠道定价 | |||||
| func BatchCreateChannelPricing(c *gin.Context) { | |||||
| var req BatchCreateChannelPricingRequest | |||||
| if err := c.ShouldBindJSON(&req); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| pricings := make([]*model.ChannelPricing, 0, len(req.Items)) | |||||
| for _, item := range req.Items { | |||||
| 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.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 | |||||
| } | |||||
| } else { | |||||
| if err := cp.Insert(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| } | |||||
| } | |||||
| common.ApiSuccess(c, gin.H{"affected": len(pricings)}) | |||||
| } | |||||
| // DeleteChannelPricing 删除渠道定价 | |||||
| func DeleteChannelPricing(c *gin.Context) { | |||||
| idStr := c.Param("id") | |||||
| id, err := strconv.Atoi(idStr) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| cp := &model.ChannelPricing{Id: id} | |||||
| if err := cp.Delete(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.SysLog(fmt.Sprintf("[ChannelPricing] deleted: id=%d", id)) | |||||
| common.ApiSuccess(c, nil) | |||||
| } | |||||
| // CopyGlobalPricingRequest 复制全局定价请求 | |||||
| type CopyGlobalPricingRequest struct { | |||||
| Overwrite bool `json:"overwrite"` // 是否覆盖已存在的渠道定价 | |||||
| } | |||||
| // CopyGlobalPricing 复制全局定价到指定渠道 | |||||
| // 从 ratio_setting 读取全局定价信息,复制到 channel_pricing 表 | |||||
| func CopyGlobalPricing(c *gin.Context) { | |||||
| channelIdStr := c.Param("channel_id") | |||||
| channelId, err := strconv.Atoi(channelIdStr) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| var req CopyGlobalPricingRequest | |||||
| c.ShouldBindJSON(&req) | |||||
| // 获取该渠道支持的所有模型(使用 GetAbilitiesByChannelId) | |||||
| abilities, err := model.GetAbilitiesByChannelId(channelId) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| imported := 0 | |||||
| for _, ability := range abilities { | |||||
| // 检查是否已存在 | |||||
| existing, _ := model.GetChannelPricing(ability.Model, channelId) | |||||
| if existing != nil && !req.Overwrite { | |||||
| continue | |||||
| } | |||||
| // 获取全局扩展比率 | |||||
| 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 | |||||
| } | |||||
| globalAudioRatio, hasAudioRatio := ratio_setting.GetAudioRatioV2(ability.Model) | |||||
| if !hasAudioRatio { | |||||
| globalAudioRatio = 0 | |||||
| } | |||||
| globalAudioCompletionRatio, hasAudioCompRatio := ratio_setting.GetAudioCompletionRatioV2(ability.Model) | |||||
| if !hasAudioCompRatio { | |||||
| globalAudioCompletionRatio = 0 | |||||
| } | |||||
| // 确定定价类型 | |||||
| var quotaType int | |||||
| var ratio, completionRatio, price float64 | |||||
| // 优先检查是否有按次计费的价格 | |||||
| modelPrice, hasPrice := ratio_setting.GetModelPrice(ability.Model, false) | |||||
| if hasPrice { | |||||
| quotaType = model.QuotaTypeByCall | |||||
| price = modelPrice | |||||
| } else { | |||||
| // 使用按量计费 | |||||
| quotaType = model.QuotaTypeByTokens | |||||
| modelRatio, hasRatio, _ := ratio_setting.GetModelRatio(ability.Model) | |||||
| if hasRatio { | |||||
| ratio = modelRatio | |||||
| } | |||||
| completionRatio = ratio_setting.GetCompletionRatio(ability.Model) | |||||
| } | |||||
| if existing != nil { | |||||
| 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, | |||||
| } | |||||
| cp.ApplyFields(quotaType, ratio, completionRatio, price, "", | |||||
| globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio) | |||||
| if err := cp.Insert(); err == nil { | |||||
| imported++ | |||||
| } | |||||
| } | |||||
| } | |||||
| common.SysLog(fmt.Sprintf("[ChannelPricing] copyGlobalPricing: channel=%d imported=%d/%d", channelId, imported, len(abilities))) | |||||
| common.ApiSuccess(c, gin.H{ | |||||
| "total": len(abilities), | |||||
| "imported": imported, | |||||
| }) | |||||
| } | |||||
| // ChannelPricingWithTags 带标签详情的渠道定价响应 | |||||
| type ChannelPricingWithTags struct { | |||||
| *model.ChannelPricing | |||||
| Tags []*model.PricingTag `json:"tags"` | |||||
| } | |||||
| // GetChannelPricingByModelWithChannelInfo 获取指定模型的渠道定价(带渠道信息,所有用户可访问) | |||||
| func GetChannelPricingByModelWithChannelInfo(c *gin.Context) { | |||||
| // 使用通配符路由时,参数包含前导斜杠,需要去除 | |||||
| modelName := c.Param("name") | |||||
| if modelName == "" { | |||||
| common.ApiErrorMsg(c, "model name is required") | |||||
| return | |||||
| } | |||||
| // 去除前导斜杠(路由是 /channel-pricing/model/*name,name 会是 "/deepseek-ai/xxx") | |||||
| modelName = strings.TrimPrefix(modelName, "/") | |||||
| list, err := model.GetChannelPricingByModelWithChannelInfo(modelName) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| loggedIn, userId, userGroup, groupRatio := resolveCurrentUser(c) | |||||
| if loggedIn { | |||||
| for _, item := range list { | |||||
| userChannelRatio := model.GetUserChannelRatio(userId, modelName, item.ChannelId) | |||||
| item.UserRatio = groupRatio * userChannelRatio | |||||
| } | |||||
| } | |||||
| c.JSON(200, gin.H{ | |||||
| "success": true, | |||||
| "message": "", | |||||
| "data": list, | |||||
| "logged_in": loggedIn, | |||||
| "group": userGroup, | |||||
| "group_ratio": groupRatio, | |||||
| }) | |||||
| } | |||||
| // GetChannelPricingWithTags 获取渠道定价(带标签详情) | |||||
| func GetChannelPricingWithTags(c *gin.Context) { | |||||
| page, _ := strconv.Atoi(c.DefaultQuery("p", "1")) | |||||
| pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) | |||||
| offset := (page - 1) * pageSize | |||||
| list, total, err := model.GetAllChannelPricing(offset, pageSize) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| // 获取所有标签 | |||||
| tags, _ := model.GetAllPricingTags() | |||||
| tagMap := make(map[int]*model.PricingTag) | |||||
| for _, tag := range tags { | |||||
| tagMap[tag.Id] = tag | |||||
| } | |||||
| // 为每个定价填充标签详情 | |||||
| result := make([]*ChannelPricingWithTags, 0, len(list)) | |||||
| for _, cp := range list { | |||||
| item := &ChannelPricingWithTags{ | |||||
| ChannelPricing: cp, | |||||
| Tags: model.ParseTagIds(cp.TagIds, tagMap), | |||||
| } | |||||
| result = append(result, item) | |||||
| } | |||||
| common.ApiSuccess(c, gin.H{ | |||||
| "page": page, | |||||
| "page_size": pageSize, | |||||
| "total": total, | |||||
| "items": result, | |||||
| }) | |||||
| } | |||||
| @@ -1,131 +0,0 @@ | |||||
| package controller | |||||
| import ( | |||||
| "strconv" | |||||
| "github.com/QuantumNous/new-api/common" | |||||
| "github.com/QuantumNous/new-api/model" | |||||
| "github.com/gin-gonic/gin" | |||||
| ) | |||||
| // GetAllPricingTags 获取所有定价标签 | |||||
| func GetAllPricingTags(c *gin.Context) { | |||||
| list, err := model.GetAllPricingTags() | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.ApiSuccess(c, list) | |||||
| } | |||||
| // CreatePricingTagRequest 创建定价标签请求 | |||||
| type CreatePricingTagRequest struct { | |||||
| Name string `json:"name" binding:"required"` | |||||
| Color string `json:"color"` | |||||
| Description string `json:"description"` | |||||
| SortOrder int `json:"sort_order"` | |||||
| } | |||||
| // CreatePricingTag 创建定价标签 | |||||
| func CreatePricingTag(c *gin.Context) { | |||||
| var req CreatePricingTagRequest | |||||
| if err := c.ShouldBindJSON(&req); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| // 检查名称是否重复 | |||||
| existing, _ := model.GetPricingTagByName(req.Name) | |||||
| if existing != nil { | |||||
| common.ApiErrorMsg(c, "tag name already exists") | |||||
| return | |||||
| } | |||||
| pt := &model.PricingTag{ | |||||
| Name: req.Name, | |||||
| Color: req.Color, | |||||
| Description: req.Description, | |||||
| SortOrder: req.SortOrder, | |||||
| } | |||||
| if pt.Color == "" { | |||||
| pt.Color = "#1890ff" | |||||
| } | |||||
| if err := pt.Insert(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.ApiSuccess(c, pt) | |||||
| } | |||||
| // UpdatePricingTagRequest 更新定价标签请求 | |||||
| type UpdatePricingTagRequest struct { | |||||
| Name string `json:"name"` | |||||
| Color string `json:"color"` | |||||
| Description string `json:"description"` | |||||
| SortOrder int `json:"sort_order"` | |||||
| } | |||||
| // UpdatePricingTag 更新定价标签 | |||||
| func UpdatePricingTag(c *gin.Context) { | |||||
| idStr := c.Param("id") | |||||
| id, err := strconv.Atoi(idStr) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| var req UpdatePricingTagRequest | |||||
| if err := c.ShouldBindJSON(&req); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| pt, err := model.GetPricingTagById(id) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| // 检查名称是否与其他标签重复 | |||||
| if req.Name != "" && req.Name != pt.Name { | |||||
| existing, _ := model.GetPricingTagByName(req.Name) | |||||
| if existing != nil && existing.Id != id { | |||||
| common.ApiErrorMsg(c, "tag name already exists") | |||||
| return | |||||
| } | |||||
| pt.Name = req.Name | |||||
| } | |||||
| if req.Color != "" { | |||||
| pt.Color = req.Color | |||||
| } | |||||
| pt.Description = req.Description | |||||
| pt.SortOrder = req.SortOrder | |||||
| if err := pt.Update(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.ApiSuccess(c, pt) | |||||
| } | |||||
| // DeletePricingTag 删除定价标签 | |||||
| func DeletePricingTag(c *gin.Context) { | |||||
| idStr := c.Param("id") | |||||
| id, err := strconv.Atoi(idStr) | |||||
| if err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| pt := &model.PricingTag{Id: id} | |||||
| if err := pt.Delete(); err != nil { | |||||
| common.ApiError(c, err) | |||||
| return | |||||
| } | |||||
| common.ApiSuccess(c, nil) | |||||
| } | |||||
| @@ -18,26 +18,22 @@ import ( | |||||
| // HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present | // 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 { | func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo { | ||||
| groupRatioInfo := types.GroupRatioInfo{ | groupRatioInfo := types.GroupRatioInfo{ | ||||
| GroupRatio: 1.0, // default ratio | |||||
| GroupRatio: 1.0, | |||||
| GroupSpecialRatio: -1, | GroupSpecialRatio: -1, | ||||
| } | } | ||||
| // check auto group | |||||
| autoGroup, exists := ctx.Get("auto_group") | autoGroup, exists := ctx.Get("auto_group") | ||||
| if exists { | if exists { | ||||
| logger.LogDebug(ctx, fmt.Sprintf("final group: %s", autoGroup)) | logger.LogDebug(ctx, fmt.Sprintf("final group: %s", autoGroup)) | ||||
| relayInfo.UsingGroup = autoGroup.(string) | relayInfo.UsingGroup = autoGroup.(string) | ||||
| } | } | ||||
| // check user group special ratio | |||||
| userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.UsingGroup) | userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.UsingGroup) | ||||
| if ok { | if ok { | ||||
| // user group special ratio | |||||
| groupRatioInfo.GroupSpecialRatio = userGroupRatio | groupRatioInfo.GroupSpecialRatio = userGroupRatio | ||||
| groupRatioInfo.GroupRatio = userGroupRatio | groupRatioInfo.GroupRatio = userGroupRatio | ||||
| groupRatioInfo.HasSpecialRatio = true | groupRatioInfo.HasSpecialRatio = true | ||||
| } else { | } else { | ||||
| // normal group ratio | |||||
| groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.UsingGroup) | groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.UsingGroup) | ||||
| } | } | ||||
| @@ -49,39 +45,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens | |||||
| var usePrice bool | var usePrice bool | ||||
| var modelRatio float64 | var modelRatio float64 | ||||
| var completionRatio float64 | var completionRatio float64 | ||||
| var channelPricingFound bool | |||||
| var cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio float64 | |||||
| // 尝试获取渠道定价(优先于全局定价) | |||||
| // ChannelMeta 在 InitChannelMeta 之前为 nil,但 Distribute 已将 channelId 写入 context | |||||
| channelId := 0 | |||||
| if info != nil && info.ChannelMeta != nil && info.ChannelId > 0 { | |||||
| channelId = info.ChannelId | |||||
| } else { | |||||
| channelId = common.GetContextKeyInt(c, constant.ContextKeyChannelId) | |||||
| } | |||||
| if channelId > 0 { | |||||
| cp, found := model.GetEffectivePricing(info.OriginModelName, channelId) | |||||
| if found && cp != nil { | |||||
| modelRatio = cp.ModelRatio | |||||
| completionRatio = cp.CompletionRatio | |||||
| modelPrice = cp.ModelPrice | |||||
| usePrice = cp.QuotaType == model.QuotaTypeByCall | |||||
| channelPricingFound = true | |||||
| // 渠道定价的扩展比率(非零值直接使用) | |||||
| cacheRatio = cp.CacheRatio | |||||
| cacheCreationRatio = cp.CacheCreationRatio | |||||
| imageRatio = cp.ImageRatio | |||||
| audioRatio = cp.AudioRatio | |||||
| audioCompletionRatio = cp.AudioCompletionRatio | |||||
| } | |||||
| } | |||||
| // 如果没有渠道定价,使用全局定价 | |||||
| if !channelPricingFound { | |||||
| modelPrice, usePrice = ratio_setting.GetModelPrice(info.OriginModelName, false) | |||||
| } | |||||
| modelPrice, usePrice = ratio_setting.GetModelPrice(info.OriginModelName, false) | |||||
| groupRatioInfo := HandleGroupRatio(c, info) | groupRatioInfo := HandleGroupRatio(c, info) | ||||
| var preConsumedQuota int | var preConsumedQuota int | ||||
| @@ -91,22 +56,21 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens | |||||
| if meta.MaxTokens != 0 { | if meta.MaxTokens != 0 { | ||||
| preConsumedTokens += meta.MaxTokens | preConsumedTokens += meta.MaxTokens | ||||
| } | } | ||||
| // 如果没有找到渠道定价,获取全局 modelRatio 和 completionRatio | |||||
| if !channelPricingFound { | |||||
| var success bool | |||||
| var matchName string | |||||
| modelRatio, success, matchName = ratio_setting.GetModelRatio(info.OriginModelName) | |||||
| if !success { | |||||
| acceptUnsetRatio := false | |||||
| if info.UserSetting.AcceptUnsetRatioModel { | |||||
| acceptUnsetRatio = true | |||||
| } | |||||
| if !acceptUnsetRatio { | |||||
| return types.PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开启自用模式;Model %s ratio or price not set, please set or start self-use mode", matchName, matchName) | |||||
| } | |||||
| var success bool | |||||
| var matchName string | |||||
| modelRatio, success, matchName = ratio_setting.GetModelRatio(info.OriginModelName) | |||||
| if !success { | |||||
| acceptUnsetRatio := false | |||||
| if info.UserSetting.AcceptUnsetRatioModel { | |||||
| acceptUnsetRatio = true | |||||
| } | |||||
| if !acceptUnsetRatio { | |||||
| return types.PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开启自用模式;Model %s ratio or price not set, please set or start self-use mode", matchName, matchName) | |||||
| } | } | ||||
| completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName) | |||||
| } | } | ||||
| completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName) | |||||
| ratio := modelRatio * groupRatioInfo.GroupRatio | ratio := modelRatio * groupRatioInfo.GroupRatio | ||||
| preConsumedQuota = int(float64(preConsumedTokens) * ratio) | preConsumedQuota = int(float64(preConsumedTokens) * ratio) | ||||
| } else { | } else { | ||||
| @@ -116,27 +80,13 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens | |||||
| preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) | preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) | ||||
| } | } | ||||
| // 全局比率作为回退(仅当渠道未设置、值为 0 时生效) | |||||
| // 必须放在 usePrice 判断之外,因为 UpdatePriceDataForChannelPricing 可能改变 UsePrice | |||||
| if cacheRatio == 0 { | |||||
| cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName) | |||||
| } | |||||
| if cacheCreationRatio == 0 { | |||||
| cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName) | |||||
| } | |||||
| 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) | |||||
| } | |||||
| cacheRatio, _ := ratio_setting.GetCacheRatio(info.OriginModelName) | |||||
| cacheCreationRatio, _ := ratio_setting.GetCreateCacheRatio(info.OriginModelName) | |||||
| imageRatio, _ := ratio_setting.GetImageRatio(info.OriginModelName) | |||||
| audioRatio := ratio_setting.GetAudioRatio(info.OriginModelName) | |||||
| audioCompletionRatio := ratio_setting.GetAudioCompletionRatio(info.OriginModelName) | |||||
| // check if free model pre-consume is disabled | |||||
| if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume { | if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume { | ||||
| // if model price or ratio is 0, do not pre-consume quota | |||||
| if groupRatioInfo.GroupRatio == 0 { | if groupRatioInfo.GroupRatio == 0 { | ||||
| preConsumedQuota = 0 | preConsumedQuota = 0 | ||||
| freeModel = true | freeModel = true | ||||
| @@ -145,11 +95,9 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens | |||||
| preConsumedQuota = 0 | preConsumedQuota = 0 | ||||
| freeModel = true | freeModel = true | ||||
| } | } | ||||
| } else { | |||||
| if modelRatio == 0 { | |||||
| preConsumedQuota = 0 | |||||
| freeModel = true | |||||
| } | |||||
| } else if modelRatio == 0 { | |||||
| preConsumedQuota = 0 | |||||
| freeModel = true | |||||
| } | } | ||||
| } | } | ||||
| @@ -183,7 +131,6 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types. | |||||
| groupRatioInfo := HandleGroupRatio(c, info) | groupRatioInfo := HandleGroupRatio(c, info) | ||||
| modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true) | modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true) | ||||
| // 如果没有配置价格,则使用默认价格 | |||||
| if !success { | if !success { | ||||
| defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName] | defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName] | ||||
| if !ok { | if !ok { | ||||
| @@ -194,7 +141,6 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types. | |||||
| } | } | ||||
| quota := int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) | quota := int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) | ||||
| // 免费模型检测(与 ModelPriceHelper 对齐) | |||||
| freeModel := false | freeModel := false | ||||
| if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume { | if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume { | ||||
| if groupRatioInfo.GroupRatio == 0 || modelPrice == 0 { | if groupRatioInfo.GroupRatio == 0 || modelPrice == 0 { | ||||
| @@ -204,11 +150,11 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types. | |||||
| } | } | ||||
| priceData := types.PriceData{ | priceData := types.PriceData{ | ||||
| FreeModel: freeModel, | |||||
| ModelPrice: modelPrice, | |||||
| Quota: quota, | |||||
| GroupRatioInfo: groupRatioInfo, | |||||
| UserChannelRatio: 1.0, | |||||
| FreeModel: freeModel, | |||||
| ModelPrice: modelPrice, | |||||
| Quota: quota, | |||||
| GroupRatioInfo: groupRatioInfo, | |||||
| UserChannelRatio: 1.0, | |||||
| } | } | ||||
| return priceData | return priceData | ||||
| } | } | ||||
| @@ -225,45 +171,11 @@ func ContainPriceOrRatio(modelName string) bool { | |||||
| return false | return false | ||||
| } | } | ||||
| // UpdatePriceDataForChannelPricing 在渠道选择后更新 PriceData 以使用渠道定价 | |||||
| // ModelPriceHelper 在渠道选择之前被调用,此时 ChannelMeta 为 nil,导致渠道定价无法使用。 | |||||
| // 需要在渠道选择后调用此函数更新价格数据,用于后续的日志记录和计费计算。 | |||||
| // UpdatePriceDataForChannelPricing 保留原调用点,仅在选定渠道后应用用户渠道倍率。 | |||||
| func UpdatePriceDataForChannelPricing(c *gin.Context, info *relaycommon.RelayInfo, channelId int) { | func UpdatePriceDataForChannelPricing(c *gin.Context, info *relaycommon.RelayInfo, channelId int) { | ||||
| if info == nil || channelId <= 0 { | if info == nil || channelId <= 0 { | ||||
| return | return | ||||
| } | } | ||||
| cp, found := model.GetEffectivePricing(info.OriginModelName, channelId) | |||||
| if found { | |||||
| info.PriceData.ModelRatio = cp.ModelRatio | |||||
| info.PriceData.CompletionRatio = cp.CompletionRatio | |||||
| info.PriceData.UsePrice = cp.QuotaType == model.QuotaTypeByCall | |||||
| if info.PriceData.UsePrice { | |||||
| info.PriceData.ModelPrice = cp.ModelPrice | |||||
| } else { | |||||
| info.PriceData.ModelPrice = -1 | |||||
| } | |||||
| info.PriceData.ApplyChannelPricingRatios(cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio) | |||||
| if info.PriceData.UsePrice { | |||||
| info.PriceData.QuotaToPreConsume = int( | |||||
| cp.ModelPrice * common.QuotaPerUnit * info.PriceData.GroupRatioInfo.GroupRatio) | |||||
| } else { | |||||
| estimateTokens := info.GetEstimatePromptTokens() | |||||
| if estimateTokens > 0 { | |||||
| ratio := cp.ModelRatio * info.PriceData.GroupRatioInfo.GroupRatio | |||||
| info.PriceData.QuotaToPreConsume = int(float64(estimateTokens) * ratio) | |||||
| } | |||||
| } | |||||
| if common.DebugEnabled { | |||||
| println(fmt.Sprintf("[ChannelPricing] updatePriceData: model=%s channel=%d modelRatio=%.4f completionRatio=%.4f cacheRatio=%.4f imageRatio=%.4f audioRatio=%.4f", | |||||
| info.OriginModelName, channelId, cp.ModelRatio, cp.CompletionRatio, cp.CacheRatio, cp.ImageRatio, cp.AudioRatio)) | |||||
| } | |||||
| } | |||||
| // 应用用户-模型-渠道倍率(始终执行,不依赖渠道定价是否存在) | |||||
| ApplyUserChannelRatio(c, info, channelId) | ApplyUserChannelRatio(c, info, channelId) | ||||
| } | } | ||||
| @@ -6,13 +6,14 @@ import ( | |||||
| "testing" | "testing" | ||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/constant" | |||||
| "github.com/QuantumNous/new-api/dto" | "github.com/QuantumNous/new-api/dto" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| relaycommon "github.com/QuantumNous/new-api/relay/common" | relaycommon "github.com/QuantumNous/new-api/relay/common" | ||||
| "github.com/QuantumNous/new-api/setting/ratio_setting" | "github.com/QuantumNous/new-api/setting/ratio_setting" | ||||
| "github.com/QuantumNous/new-api/types" | "github.com/QuantumNous/new-api/types" | ||||
| "github.com/glebarez/sqlite" | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| "github.com/glebarez/sqlite" | |||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| "github.com/stretchr/testify/require" | "github.com/stretchr/testify/require" | ||||
| "gorm.io/gorm" | "gorm.io/gorm" | ||||
| @@ -23,54 +24,44 @@ const testModelUsePriceSwitch = "test-useprice-switch-model" | |||||
| func setupUsePriceSwitchTest(t *testing.T) { | func setupUsePriceSwitchTest(t *testing.T) { | ||||
| t.Helper() | t.Helper() | ||||
| // 初始化 SQLite 内存数据库 | |||||
| db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) | db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) | ||||
| require.NoError(t, err) | require.NoError(t, err) | ||||
| sqlDB, _ := db.DB() | |||||
| sqlDB, err := db.DB() | |||||
| require.NoError(t, err) | |||||
| sqlDB.SetMaxOpenConns(1) | sqlDB.SetMaxOpenConns(1) | ||||
| origDB := model.DB | origDB := model.DB | ||||
| origUsingSQLite := common.UsingSQLite | |||||
| origRedisEnabled := common.RedisEnabled | |||||
| model.DB = db | model.DB = db | ||||
| common.UsingSQLite = true | common.UsingSQLite = true | ||||
| common.RedisEnabled = false | common.RedisEnabled = false | ||||
| require.NoError(t, db.AutoMigrate(&model.ChannelPricing{})) | |||||
| require.NoError(t, db.AutoMigrate(&model.UserChannelRatio{})) | |||||
| // 全局定价:按次计费(modelPrice 存在 → usePrice=true) | |||||
| require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{"`+testModelUsePriceSwitch+`":0.5}`)) | require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{"`+testModelUsePriceSwitch+`":0.5}`)) | ||||
| // 全局 modelRatio 也设上(渠道按量计费时会用到) | |||||
| require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"`+testModelUsePriceSwitch+`":15}`)) | require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"`+testModelUsePriceSwitch+`":15}`)) | ||||
| require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(`{"`+testModelUsePriceSwitch+`":3}`)) | require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(`{"`+testModelUsePriceSwitch+`":3}`)) | ||||
| // 全局扩展比率 | |||||
| require.NoError(t, ratio_setting.UpdateCacheRatioByJSONString(`{"`+testModelUsePriceSwitch+`":0.1}`)) | require.NoError(t, ratio_setting.UpdateCacheRatioByJSONString(`{"`+testModelUsePriceSwitch+`":0.1}`)) | ||||
| require.NoError(t, ratio_setting.UpdateCreateCacheRatioByJSONString(`{"`+testModelUsePriceSwitch+`":1.25}`)) | require.NoError(t, ratio_setting.UpdateCreateCacheRatioByJSONString(`{"`+testModelUsePriceSwitch+`":1.25}`)) | ||||
| require.NoError(t, ratio_setting.UpdateImageRatioByJSONString(`{"`+testModelUsePriceSwitch+`":2.0}`)) | require.NoError(t, ratio_setting.UpdateImageRatioByJSONString(`{"`+testModelUsePriceSwitch+`":2.0}`)) | ||||
| require.NoError(t, ratio_setting.UpdateAudioRatioByJSONString(`{"`+testModelUsePriceSwitch+`":5.0}`)) | require.NoError(t, ratio_setting.UpdateAudioRatioByJSONString(`{"`+testModelUsePriceSwitch+`":5.0}`)) | ||||
| require.NoError(t, ratio_setting.UpdateAudioCompletionRatioByJSONString(`{"`+testModelUsePriceSwitch+`":3.0}`)) | require.NoError(t, ratio_setting.UpdateAudioCompletionRatioByJSONString(`{"`+testModelUsePriceSwitch+`":3.0}`)) | ||||
| // 渠道定价:按量计费(QuotaTypeByTokens),扩展比率全部为 0(用全局) | |||||
| cp := &model.ChannelPricing{ | |||||
| ModelName: testModelUsePriceSwitch, | |||||
| ChannelId: 9901, | |||||
| QuotaType: model.QuotaTypeByTokens, | |||||
| ModelRatio: 15, | |||||
| CompletionRatio: 3, | |||||
| // 扩展比率全部为 0 → 意味着回退全局 | |||||
| } | |||||
| require.NoError(t, cp.Insert()) | |||||
| t.Cleanup(func() { | t.Cleanup(func() { | ||||
| model.DB = origDB | model.DB = origDB | ||||
| sqlDB.Close() | |||||
| // 清理全局定价 | |||||
| ratio_setting.UpdateModelPriceByJSONString(`{}`) | |||||
| ratio_setting.UpdateModelRatioByJSONString(`{}`) | |||||
| ratio_setting.UpdateCompletionRatioByJSONString(`{}`) | |||||
| ratio_setting.UpdateCacheRatioByJSONString(`{}`) | |||||
| ratio_setting.UpdateCreateCacheRatioByJSONString(`{}`) | |||||
| ratio_setting.UpdateImageRatioByJSONString(`{}`) | |||||
| ratio_setting.UpdateAudioRatioByJSONString(`{}`) | |||||
| ratio_setting.UpdateAudioCompletionRatioByJSONString(`{}`) | |||||
| common.UsingSQLite = origUsingSQLite | |||||
| common.RedisEnabled = origRedisEnabled | |||||
| require.NoError(t, sqlDB.Close()) | |||||
| require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateCacheRatioByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateCreateCacheRatioByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateImageRatioByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateAudioRatioByJSONString(`{}`)) | |||||
| require.NoError(t, ratio_setting.UpdateAudioCompletionRatioByJSONString(`{}`)) | |||||
| }) | }) | ||||
| } | } | ||||
| @@ -79,88 +70,81 @@ func buildTestContext(t *testing.T) *gin.Context { | |||||
| gin.SetMode(gin.TestMode) | gin.SetMode(gin.TestMode) | ||||
| w := httptest.NewRecorder() | w := httptest.NewRecorder() | ||||
| c, _ := gin.CreateTestContext(w) | c, _ := gin.CreateTestContext(w) | ||||
| c.Request, _ = http.NewRequest("POST", "/v1/chat/completions", nil) | |||||
| req, err := http.NewRequest(http.MethodPost, "/v1/chat/completions", nil) | |||||
| require.NoError(t, err) | |||||
| c.Request = req | |||||
| return c | return c | ||||
| } | } | ||||
| // TestGlobalRatiosFallbackWhenGlobalUsePriceTrue 验证核心场景: | |||||
| // 全局按次计费 → 渠道按量计费 → PriceData 中扩展比率仍应被全局默认值填充 | |||||
| // 而不是保持 0(否则后续 UpdatePriceDataForChannelPricing 切换为按量时会丢失比率) | |||||
| func TestGlobalRatiosFallbackWhenGlobalUsePriceTrue(t *testing.T) { | |||||
| func TestGlobalRatiosFilledWhenUsePriceTrue(t *testing.T) { | |||||
| setupUsePriceSwitchTest(t) | setupUsePriceSwitchTest(t) | ||||
| c := buildTestContext(t) | c := buildTestContext(t) | ||||
| // 模拟渠道选择后的场景:ChannelMeta 不为空,能查到渠道定价 | |||||
| info := &relaycommon.RelayInfo{ | info := &relaycommon.RelayInfo{ | ||||
| OriginModelName: testModelUsePriceSwitch, | OriginModelName: testModelUsePriceSwitch, | ||||
| ChannelMeta: &relaycommon.ChannelMeta{ | |||||
| ChannelId: 9901, | |||||
| }, | |||||
| UserSetting: dto.UserSetting{}, | |||||
| UserSetting: dto.UserSetting{}, | |||||
| } | } | ||||
| meta := &types.TokenCountMeta{} | |||||
| priceData, err := ModelPriceHelper(c, info, 100, meta) | |||||
| priceData, err := ModelPriceHelper(c, info, 100, &types.TokenCountMeta{}) | |||||
| require.NoError(t, err) | require.NoError(t, err) | ||||
| // 断言:渠道按量计费覆盖了全局按次,扩展比率必须被全局默认值填充,不能是 0 | |||||
| assert.Equal(t, 0.1, priceData.CacheRatio, "CacheRatio 应从全局回退填充") | |||||
| assert.Equal(t, 1.25, priceData.CacheCreationRatio, "CacheCreationRatio 应从全局回退填充") | |||||
| assert.Equal(t, 2.0, priceData.ImageRatio, "ImageRatio 应从全局回退填充") | |||||
| assert.Equal(t, 5.0, priceData.AudioRatio, "AudioRatio 应从全局回退填充") | |||||
| assert.Equal(t, 3.0, priceData.AudioCompletionRatio, "AudioCompletionRatio 应从全局回退填充") | |||||
| assert.True(t, priceData.UsePrice) | |||||
| assert.Equal(t, 0.5, priceData.ModelPrice) | |||||
| assert.Equal(t, 0.1, priceData.CacheRatio) | |||||
| assert.Equal(t, 1.25, priceData.CacheCreationRatio) | |||||
| assert.Equal(t, 2.0, priceData.ImageRatio) | |||||
| assert.Equal(t, 5.0, priceData.AudioRatio) | |||||
| assert.Equal(t, 3.0, priceData.AudioCompletionRatio) | |||||
| } | } | ||||
| // TestGlobalRatiosFallbackWithoutChannelPricing 验证无渠道定价时全局比率也正常回退 | |||||
| func TestGlobalRatiosFallbackWithoutChannelPricing(t *testing.T) { | |||||
| func TestGlobalRatiosFilledWhenUseRatioMode(t *testing.T) { | |||||
| setupUsePriceSwitchTest(t) | setupUsePriceSwitchTest(t) | ||||
| c := buildTestContext(t) | c := buildTestContext(t) | ||||
| // 无 ChannelMeta → 渠道定价不可用,走全局 | |||||
| require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{}`)) | |||||
| info := &relaycommon.RelayInfo{ | info := &relaycommon.RelayInfo{ | ||||
| OriginModelName: testModelUsePriceSwitch, | OriginModelName: testModelUsePriceSwitch, | ||||
| UserSetting: dto.UserSetting{}, | UserSetting: dto.UserSetting{}, | ||||
| } | } | ||||
| meta := &types.TokenCountMeta{} | |||||
| priceData, err := ModelPriceHelper(c, info, 100, meta) | |||||
| priceData, err := ModelPriceHelper(c, info, 100, &types.TokenCountMeta{}) | |||||
| require.NoError(t, err) | require.NoError(t, err) | ||||
| // 全局是按次计费,UsePrice=true,但比率仍应被填充 | |||||
| assert.True(t, priceData.UsePrice) | |||||
| assert.Equal(t, 0.1, priceData.CacheRatio, "CacheRatio 应从全局回退填充") | |||||
| assert.Equal(t, 2.0, priceData.ImageRatio, "ImageRatio 应从全局回退填充") | |||||
| assert.False(t, priceData.UsePrice) | |||||
| assert.Equal(t, 15.0, priceData.ModelRatio) | |||||
| assert.Equal(t, 3.0, priceData.CompletionRatio) | |||||
| assert.Equal(t, 0.1, priceData.CacheRatio) | |||||
| assert.Equal(t, 2.0, priceData.ImageRatio) | |||||
| } | } | ||||
| // TestChannelRatiosOverrideGlobal 验证渠道非零值优先于全局 | |||||
| func TestChannelRatiosOverrideGlobal(t *testing.T) { | |||||
| func TestUpdatePriceDataForChannelPricingOnlyAppliesUserChannelRatio(t *testing.T) { | |||||
| setupUsePriceSwitchTest(t) | setupUsePriceSwitchTest(t) | ||||
| // 渠道定价设了自定义 cacheRatio=0.5 | |||||
| cp := &model.ChannelPricing{ | |||||
| ModelName: testModelUsePriceSwitch, | |||||
| ChannelId: 9902, | |||||
| QuotaType: model.QuotaTypeByTokens, | |||||
| ModelRatio: 15, | |||||
| CompletionRatio: 3, | |||||
| CacheRatio: 0.5, // 渠道自定义,覆盖全局 0.1 | |||||
| // 其他扩展比率保持 0,用全局 | |||||
| ratio := &model.UserChannelRatio{ | |||||
| UserId: 9, | |||||
| ModelName: testModelUsePriceSwitch, | |||||
| ChannelId: 9901, | |||||
| Ratio: 0.8, | |||||
| } | } | ||||
| require.NoError(t, cp.Insert()) | |||||
| require.NoError(t, ratio.Insert()) | |||||
| c := buildTestContext(t) | c := buildTestContext(t) | ||||
| c.Set(string(constant.ContextKeyUserId), 9) | |||||
| info := &relaycommon.RelayInfo{ | info := &relaycommon.RelayInfo{ | ||||
| OriginModelName: testModelUsePriceSwitch, | OriginModelName: testModelUsePriceSwitch, | ||||
| ChannelMeta: &relaycommon.ChannelMeta{ | |||||
| ChannelId: 9902, | |||||
| PriceData: types.PriceData{ | |||||
| ModelPrice: 0.5, | |||||
| UsePrice: true, | |||||
| QuotaToPreConsume: 1000, | |||||
| UserChannelRatio: 1.0, | |||||
| }, | }, | ||||
| UserSetting: dto.UserSetting{}, | |||||
| } | } | ||||
| priceData, err := ModelPriceHelper(c, info, 100, &types.TokenCountMeta{}) | |||||
| require.NoError(t, err) | |||||
| UpdatePriceDataForChannelPricing(c, info, 9901) | |||||
| assert.Equal(t, 0.5, priceData.CacheRatio, "渠道自定义 CacheRatio 应覆盖全局") | |||||
| assert.Equal(t, 2.0, priceData.ImageRatio, "ImageRatio 未设渠道值,应从全局回退") | |||||
| assert.Equal(t, 5.0, priceData.AudioRatio, "AudioRatio 未设渠道值,应从全局回退") | |||||
| assert.Equal(t, 0.8, info.PriceData.UserChannelRatio) | |||||
| assert.Equal(t, 800, info.PriceData.QuotaToPreConsume) | |||||
| assert.Equal(t, 0.5, info.PriceData.ModelPrice) | |||||
| } | } | ||||
| @@ -33,7 +33,6 @@ func SetApiRouter(router *gin.Engine) { | |||||
| apiRouter.GET("/home_page_content", controller.GetHomePageContent) | apiRouter.GET("/home_page_content", controller.GetHomePageContent) | ||||
| apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing) | apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing) | ||||
| apiRouter.GET("/pricing/user/*model", middleware.TryUserAuth(), controller.GetUserPricing) | apiRouter.GET("/pricing/user/*model", middleware.TryUserAuth(), controller.GetUserPricing) | ||||
| apiRouter.GET("/channel-pricing/model/*name", middleware.TryUserAuth(), controller.GetChannelPricingByModelWithChannelInfo) | |||||
| apiRouter.GET("/captcha", controller.GetCaptcha) | apiRouter.GET("/captcha", controller.GetCaptcha) | ||||
| apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) | apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) | ||||
| apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) | apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) | ||||
| @@ -192,19 +191,6 @@ func SetApiRouter(router *gin.Engine) { | |||||
| optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除 | optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除 | ||||
| } | } | ||||
| // 渠道定价路由(管理员权限) | |||||
| channelPricingRoute := apiRouter.Group("/channel_pricing") | |||||
| channelPricingRoute.Use(middleware.AdminAuth()) | |||||
| { | |||||
| channelPricingRoute.GET("/", controller.GetAllChannelPricing) | |||||
| channelPricingRoute.GET("/with_tags", controller.GetChannelPricingWithTags) | |||||
| channelPricingRoute.GET("/model/:name", controller.GetChannelPricingByModel) | |||||
| channelPricingRoute.POST("/", controller.CreateChannelPricing) | |||||
| channelPricingRoute.POST("/batch", controller.BatchCreateChannelPricing) | |||||
| channelPricingRoute.POST("/copy_global/:channel_id", controller.CopyGlobalPricing) | |||||
| channelPricingRoute.DELETE("/:id", controller.DeleteChannelPricing) | |||||
| } | |||||
| // 用户倍率路由(管理员权限) | // 用户倍率路由(管理员权限) | ||||
| userChannelRatioRoute := apiRouter.Group("/user_channel_ratio") | userChannelRatioRoute := apiRouter.Group("/user_channel_ratio") | ||||
| userChannelRatioRoute.Use(middleware.AdminAuth()) | userChannelRatioRoute.Use(middleware.AdminAuth()) | ||||
| @@ -215,16 +201,6 @@ func SetApiRouter(router *gin.Engine) { | |||||
| userChannelRatioRoute.DELETE("/:id", controller.DeleteUserChannelRatio) | userChannelRatioRoute.DELETE("/:id", controller.DeleteUserChannelRatio) | ||||
| } | } | ||||
| // 定价标签路由(管理员权限) | |||||
| pricingTagRoute := apiRouter.Group("/pricing_tag") | |||||
| pricingTagRoute.Use(middleware.AdminAuth()) | |||||
| { | |||||
| pricingTagRoute.GET("/", controller.GetAllPricingTags) | |||||
| pricingTagRoute.POST("/", controller.CreatePricingTag) | |||||
| pricingTagRoute.PUT("/:id", controller.UpdatePricingTag) | |||||
| pricingTagRoute.DELETE("/:id", controller.DeletePricingTag) | |||||
| } | |||||
| // 邮箱后缀额度规则路由(管理员权限) | // 邮箱后缀额度规则路由(管理员权限) | ||||
| emailQuotaRuleRoute := apiRouter.Group("/email_quota_rule") | emailQuotaRuleRoute := apiRouter.Group("/email_quota_rule") | ||||
| emailQuotaRuleRoute.Use(middleware.AdminAuth()) | emailQuotaRuleRoute.Use(middleware.AdminAuth()) | ||||