From baed9754d09060d83f3bcdd020870e6a2162c186 Mon Sep 17 00:00:00 2001 From: fengsilin Date: Sat, 16 May 2026 01:11:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AE=8C=E6=95=B4=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=B8=A0=E9=81=93=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除了用户通过 sk-{key}:{channelId} 指定渠道、Playground 下拉框选择渠道、 以及管理员设置默认渠道 (is_default) 的所有后端代码。 涉及修改: - middleware/auth.go: 简化 parseTokenKey,移除 specific_channel_id 设置 - middleware/distributor.go: 移除指定渠道分发逻辑、默认通道检查、ModelRequest.ChannelId - constant/context_key.go: 移除 ContextKeyTokenSpecificChannelId - controller/playground_channels.go: 整个文件删除 - controller/relay.go: 移除 shouldRetry/shouldRetryTaskRelay 中的指定渠道跳过 - controller/channel.go: 移除 GetUserChannelsForBinding - controller/channel_pricing.go: 移除 SetDefaultChannel/ClearDefaultChannel - model/ability.go: 移除 GetModelChannelsForGroup - model/channel.go: 移除 GetAllChannelsForBinding - model/channel_pricing.go: 移除默认通道缓存及所有相关函数 - model/pricing.go: 移除 DefaultChannelName 字段 - router/api-router.go: 移除 4 条路由 - i18n: 移除 channel.id_format_error 和 distributor.invalid_channel_id --- constant/context_key.go | 1 - controller/channel.go | 27 ------- controller/channel_pricing.go | 47 ------------ controller/playground_channels.go | 62 --------------- controller/relay.go | 6 -- i18n/keys.go | 2 - i18n/locales/en.yaml | 2 - i18n/locales/zh-CN.yaml | 2 - i18n/locales/zh-TW.yaml | 2 - middleware/auth.go | 25 +----- middleware/distributor.go | 72 +---------------- model/ability.go | 52 ------------- model/channel.go | 10 --- model/channel_pricing.go | 123 +----------------------------- model/channel_pricing_test.go | 121 ----------------------------- model/pricing.go | 7 -- router/api-router.go | 8 +- 17 files changed, 10 insertions(+), 559 deletions(-) delete mode 100644 controller/playground_channels.go diff --git a/constant/context_key.go b/constant/context_key.go index f31e554..ea72ab8 100644 --- a/constant/context_key.go +++ b/constant/context_key.go @@ -15,7 +15,6 @@ const ( ContextKeyTokenKey ContextKey = "token_key" ContextKeyTokenId ContextKey = "token_id" ContextKeyTokenGroup ContextKey = "token_group" - ContextKeyTokenSpecificChannelId ContextKey = "specific_channel_id" ContextKeyTokenModelLimitEnabled ContextKey = "token_model_limit_enabled" ContextKeyTokenModelLimit ContextKey = "token_model_limit" ContextKeyTokenCrossGroupRetry ContextKey = "token_cross_group_retry" diff --git a/controller/channel.go b/controller/channel.go index 5d4b939..0531927 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -2106,30 +2106,3 @@ func OllamaVersion(c *gin.Context) { }) } -// GetUserChannelsForBinding 获取用户可用于绑定的渠道列表 -// 只返回 id、name、type 和 remark,不包含敏感信息(如key) -func GetUserChannelsForBinding(c *gin.Context) { - channels, err := model.GetAllChannelsForBinding() - if err != nil { - common.ApiError(c, err) - return - } - - // 构建返回结果 - result := make([]gin.H, 0, len(channels)) - for _, ch := range channels { - result = append(result, gin.H{ - "id": ch.Id, - "name": ch.Name, - "public_name": ch.PublicName, - "type": ch.Type, - "remark": ch.Remark, - }) - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": result, - }) -} diff --git a/controller/channel_pricing.go b/controller/channel_pricing.go index 4b54317..0adfd9d 100644 --- a/controller/channel_pricing.go +++ b/controller/channel_pricing.go @@ -357,50 +357,3 @@ func GetChannelPricingWithTags(c *gin.Context) { }) } -// SetDefaultChannelRequest 设置默认通道请求 -type SetDefaultChannelRequest struct { - ModelName string `json:"model_name" binding:"required"` - ChannelId int `json:"channel_id" binding:"required"` -} - -// SetDefaultChannel 设置指定模型的默认通道 -func SetDefaultChannel(c *gin.Context) { - var req SetDefaultChannelRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.ApiError(c, err) - return - } - - // 验证渠道定价记录存在 - existing, err := model.GetChannelPricing(req.ModelName, req.ChannelId) - if err != nil || existing == nil { - common.ApiErrorMsg(c, "channel pricing not found for this model and channel") - return - } - - if err := model.SetDefaultChannel(req.ModelName, req.ChannelId); err != nil { - common.ApiError(c, err) - return - } - - common.SysLog(fmt.Sprintf("[ChannelPricing] set default: model=%s channel=%d", req.ModelName, req.ChannelId)) - common.ApiSuccess(c, nil) -} - -// ClearDefaultChannel 清除指定模型的默认通道 -func ClearDefaultChannel(c *gin.Context) { - modelName := c.Param("name") - modelName = strings.TrimPrefix(modelName, "/") - if modelName == "" { - common.ApiErrorMsg(c, "model name is required") - return - } - - if err := model.ClearDefaultChannel(modelName); err != nil { - common.ApiError(c, err) - return - } - - common.SysLog(fmt.Sprintf("[ChannelPricing] cleared default: model=%s", modelName)) - common.ApiSuccess(c, nil) -} diff --git a/controller/playground_channels.go b/controller/playground_channels.go deleted file mode 100644 index 89a86c6..0000000 --- a/controller/playground_channels.go +++ /dev/null @@ -1,62 +0,0 @@ -package controller - -import ( - "net/http" - - "github.com/QuantumNous/new-api/model" - "github.com/gin-gonic/gin" -) - -func GetModelChannels(c *gin.Context) { - modelName := c.Query("model") - if modelName == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "model parameter is required", - }) - return - } - - userId := c.GetInt("id") - if userId == 0 { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "unauthorized", - }) - return - } - - userCache, err := model.GetUserCache(userId) - if err != nil || userCache == nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "failed to get user info", - }) - return - } - userGroup := userCache.Group - if userGroup == "" { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "failed to get user group", - }) - return - } - - channels, defaultChannelId, err := model.GetModelChannelsForGroup(modelName, userGroup) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "failed to query channels", - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": gin.H{ - "channels": channels, - "default_channel_id": defaultChannelId, - }, - }) -} diff --git a/controller/relay.go b/controller/relay.go index 687ddeb..07b1a8a 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -328,9 +328,6 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b if retryTimes <= 0 { return false } - if _, ok := c.Get("specific_channel_id"); ok { - return false - } code := openaiErr.StatusCode if code >= 200 && code < 300 { return false @@ -614,9 +611,6 @@ func shouldRetryTaskRelay(c *gin.Context, channelId int, taskErr *dto.TaskError, if retryTimes <= 0 { return false } - if _, ok := c.Get("specific_channel_id"); ok { - return false - } if taskErr.StatusCode == http.StatusTooManyRequests { return true } diff --git a/i18n/keys.go b/i18n/keys.go index 4d98540..13bcd57 100644 --- a/i18n/keys.go +++ b/i18n/keys.go @@ -152,7 +152,6 @@ const ( // Channel related messages const ( MsgChannelNotExists = "channel.not_exists" - MsgChannelIdFormatError = "channel.id_format_error" MsgChannelNoAvailableKey = "channel.no_available_key" MsgChannelGetListFailed = "channel.get_list_failed" MsgChannelGetTagsFailed = "channel.get_tags_failed" @@ -291,7 +290,6 @@ const ( // Distributor related messages const ( MsgDistributorInvalidRequest = "distributor.invalid_request" - MsgDistributorInvalidChannelId = "distributor.invalid_channel_id" MsgDistributorChannelDisabled = "distributor.channel_disabled" MsgDistributorTokenNoModelAccess = "distributor.token_no_model_access" MsgDistributorTokenModelForbidden = "distributor.token_model_forbidden" diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 54dbf91..fcb80ec 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -132,7 +132,6 @@ topup.invalid_quota: "Invalid top-up quota" # Channel messages channel.not_exists: "Channel does not exist" -channel.id_format_error: "Channel ID format error" channel.no_available_key: "No available channel keys" channel.get_list_failed: "Failed to get channel list, please try again later" channel.get_tags_failed: "Failed to get tags, please try again later" @@ -243,7 +242,6 @@ common.invalid_input: "Invalid input" # Distributor messages distributor.invalid_request: "Invalid request: {{.Error}}" -distributor.invalid_channel_id: "Invalid channel ID" distributor.channel_disabled: "This channel has been disabled" distributor.token_no_model_access: "This token has no access to any models" distributor.token_model_forbidden: "This token has no access to model {{.Model}}" diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index 4e0b5cd..ec52e55 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -133,7 +133,6 @@ topup.invalid_quota: "无效的充值额度" # Channel messages channel.not_exists: "渠道不存在" -channel.id_format_error: "渠道ID格式错误" channel.no_available_key: "没有可用的渠道密钥" channel.get_list_failed: "获取渠道列表失败,请稍后重试" channel.get_tags_failed: "获取标签失败,请稍后重试" @@ -244,7 +243,6 @@ common.invalid_input: "输入不合法" # Distributor messages distributor.invalid_request: "无效的请求,{{.Error}}" -distributor.invalid_channel_id: "无效的渠道 Id" distributor.channel_disabled: "该渠道已被禁用" distributor.token_no_model_access: "该令牌无权访问任何模型" distributor.token_model_forbidden: "该令牌无权访问模型 {{.Model}}" diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml index dcdd331..75c5479 100644 --- a/i18n/locales/zh-TW.yaml +++ b/i18n/locales/zh-TW.yaml @@ -133,7 +133,6 @@ topup.invalid_quota: "無效的充值額度" # Channel messages channel.not_exists: "管道不存在" -channel.id_format_error: "管道ID格式錯誤" channel.no_available_key: "沒有可用的管道密鑰" channel.get_list_failed: "獲取管道列表失敗,請稍後重試" channel.get_tags_failed: "獲取標籤失敗,請稍後重試" @@ -244,7 +243,6 @@ common.invalid_input: "輸入不合法" # Distributor messages distributor.invalid_request: "無效的請求,{{.Error}}" -distributor.invalid_channel_id: "無效的管道 Id" distributor.channel_disabled: "該管道已被禁用" distributor.token_no_model_access: "該令牌無權存取任何模型" distributor.token_model_forbidden: "該令牌無權存取模型 {{.Model}}" diff --git a/middleware/auth.go b/middleware/auth.go index 2e41910..10ec6f3 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -30,29 +30,12 @@ func validUserInfo(username string, role int) bool { return true } -// parseTokenKey 解析令牌字符串,提取 key 和可选的 channelId -// 支持格式:sk-{key} 或 sk-{key}:{channelId} 或 sk-{key}-{channelId} -// 返回:key, parts(parts[0]=key, parts[1]=channelId 如果指定) +// parseTokenKey 解析令牌字符串,提取 key +// 支持格式:sk-{key} func parseTokenKey(key string) (string, []string) { key = strings.TrimPrefix(key, "sk-") - - // 先按冒号分割获取 channelId(新格式:sk-{key}:{channelId}) - var colonChannelId string - if colonIdx := strings.Index(key, ":"); colonIdx != -1 { - colonChannelId = key[colonIdx+1:] - key = key[:colonIdx] - } - parts := strings.Split(key, "-") key = parts[0] - - // 如果冒号方式指定了渠道,验证并追加到 parts - if colonChannelId != "" { - // 验证 channelId 为有效数字,防止 sk-abc:def:ghi 等格式 - if _, err := strconv.Atoi(colonChannelId); err == nil { - parts = append(parts, colonChannelId) - } - } return key, parts } @@ -409,9 +392,5 @@ func SetupContextForToken(c *gin.Context, token *model.Token, parts ...string) e } common.SetContextKey(c, constant.ContextKeyTokenGroup, token.Group) common.SetContextKey(c, constant.ContextKeyTokenCrossGroupRetry, token.CrossGroupRetry) - // 所有用户都可以通过 sk-{key}:{channelId} 或 sk-{key}-{channelId} 指定渠道 - if len(parts) > 1 { - c.Set("specific_channel_id", parts[1]) - } return nil } diff --git a/middleware/distributor.go b/middleware/distributor.go index 4a94aca..3938379 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "slices" - "strconv" "strings" "time" @@ -23,9 +22,8 @@ import ( ) type ModelRequest struct { - Model string `json:"model"` - Group string `json:"group,omitempty"` - ChannelId int `json:"channel_id,omitempty"` + Model string `json:"model"` + Group string `json:"group,omitempty"` } func Distribute() func(c *gin.Context) { @@ -36,31 +34,7 @@ func Distribute() func(c *gin.Context) { abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()})) return } - channelId, ok := common.GetContextKey(c, constant.ContextKeyTokenSpecificChannelId) - if ok { - id, err := strconv.Atoi(channelId.(string)) - if err != nil { - abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId)) - return - } - channel, err = model.GetChannelById(id, true) - if err != nil { - abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId)) - return - } - if channel.Status != common.ChannelStatusEnabled { - abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled)) - return - } - // 验证指定渠道是否支持请求的模型 - if shouldSelectChannel && modelRequest.Model != "" { - usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup) - if !model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, channel.Id) { - abortWithOpenAiMessage(c, http.StatusServiceUnavailable, i18n.T(c, i18n.MsgDistributorNoAvailableChannel, map[string]any{"Group": usingGroup, "Model": modelRequest.Model}), types.ErrorCodeModelNotFound) - return - } - } - } else { + // Normal channel selection logic // check token model mapping modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled) @@ -108,39 +82,6 @@ func Distribute() func(c *gin.Context) { } } - // 默认通道检查(管理员配置的模型默认通道) - if channel == nil { - if defaultChannelId, ok := model.GetDefaultChannelId(modelRequest.Model); ok { - defaultCh, err := model.CacheGetChannel(defaultChannelId) - if err != nil || defaultCh == nil { - common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d not found in cache, fallback", modelRequest.Model, defaultChannelId)) - } else if defaultCh.Status != common.ChannelStatusEnabled { - common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d disabled(status=%d), fallback", modelRequest.Model, defaultChannelId, defaultCh.Status)) - } else if usingGroup == "auto" { - userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) - autoGroups := service.GetUserAutoGroup(userGroup) - for _, g := range autoGroups { - if model.IsChannelEnabledForGroupModel(g, modelRequest.Model, defaultCh.Id) { - channel = defaultCh - selectGroup = g - common.SetContextKey(c, constant.ContextKeyAutoGroup, g) - common.SysLog(fmt.Sprintf("[Distribute] model=%s using default_channel=%d (auto group=%s)", modelRequest.Model, defaultChannelId, g)) - break - } - } - if channel == nil { - common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d not enabled for any auto group, fallback", modelRequest.Model, defaultChannelId)) - } - } else if model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, defaultCh.Id) { - channel = defaultCh - selectGroup = usingGroup - common.SysLog(fmt.Sprintf("[Distribute] model=%s using default_channel=%d (group=%s)", modelRequest.Model, defaultChannelId, usingGroup)) - } else { - common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d not enabled for group=%s, fallback", modelRequest.Model, defaultChannelId, usingGroup)) - } - } - } - // 通道亲和性检查(仅在未选中默认通道时生效) if channel == nil { if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found { @@ -189,7 +130,6 @@ func Distribute() func(c *gin.Context) { } } } - } common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now()) SetupContextForSelectedChannel(c, channel, modelRequest.Model) c.Next() @@ -374,12 +314,6 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { modelRequest.Group = g } common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group) - // channel_id: body 优先,fallback 到 header - if req.ChannelId > 0 { - common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, strconv.Itoa(req.ChannelId)) - } else if ch := c.GetHeader("X-Channel-Id"); ch != "" { - common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, ch) - } } if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" { diff --git a/model/ability.go b/model/ability.go index 30ae6cb..baad11b 100644 --- a/model/ability.go +++ b/model/ability.go @@ -66,58 +66,6 @@ func GetAbilitiesByChannelId(channelId int) ([]*Ability, error) { return abilities, err } -// GetModelChannelsForGroup 返回指定模型在指定分组下的可用渠道列表及默认渠道ID -func GetModelChannelsForGroup(modelName string, group string) ([]map[string]any, int, error) { - var channelIds []int - err := DB.Model(&Ability{}). - Where("model = ?", modelName). - Where("enabled = ?", true). - Where(commonGroupCol+" = ?", group). - Distinct("channel_id"). - Pluck("channel_id", &channelIds).Error - if err != nil { - return nil, 0, err - } - - if len(channelIds) == 0 { - return []map[string]any{}, 0, nil - } - - type channelInfo struct { - Id int `json:"id"` - Name string `json:"name"` - PublicName string `json:"public_name"` - } - var channels []channelInfo - err = DB.Table("channels"). - Where("id IN ? AND status = ?", channelIds, common.ChannelStatusEnabled). - Select("id, name, public_name"). - Find(&channels).Error - if err != nil { - return nil, 0, err - } - - defaultChannelId := 0 - if defaultChId, ok := GetDefaultChannelId(modelName); ok { - for _, id := range channelIds { - if id == defaultChId { - defaultChannelId = defaultChId - break - } - } - } - - result := make([]map[string]any, 0, len(channels)) - for _, ch := range channels { - result = append(result, map[string]any{ - "id": ch.Id, - "name": ch.Name, - "public_name": ch.PublicName, - }) - } - - return result, defaultChannelId, nil -} func getPriority(group string, model string, retry int) (int, error) { diff --git a/model/channel.go b/model/channel.go index a4f680f..e16a544 100644 --- a/model/channel.go +++ b/model/channel.go @@ -283,16 +283,6 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan return channels, err } -// GetAllChannelsForBinding 获取所有启用的渠道(用于用户绑定渠道) -// 只返回 id, name, type, remark,不包含敏感信息 -func GetAllChannelsForBinding() ([]*Channel, error) { - var channels []*Channel - err := DB.Select("id, name, public_name, type, remark"). - Where("status = ?", common.ChannelStatusEnabled). - Order("priority desc"). - Find(&channels).Error - return channels, err -} func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, error) { var channels []*Channel diff --git a/model/channel_pricing.go b/model/channel_pricing.go index 545d734..60d774c 100644 --- a/model/channel_pricing.go +++ b/model/channel_pricing.go @@ -16,10 +16,6 @@ import ( var ( channelPricingCache = make(map[string]*ChannelPricing) // key: "modelName:channelId" channelPricingCacheLock sync.RWMutex - - // 默认通道缓存:modelName → channelId - defaultChannelCache = make(map[string]int) - defaultChannelCacheLock sync.RWMutex ) // QuotaType 计费类型 @@ -85,9 +81,7 @@ func (cp *ChannelPricing) Insert() error { err := DB.Create(cp).Error if err == nil { setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) - if cp.IsDefault { - setDefaultChannelCache(cp.ModelName, cp.ChannelId) - } + } return err } @@ -101,11 +95,7 @@ func (cp *ChannelPricing) Update() error { Updates(cp).Error if err == nil { setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) - if cp.IsDefault { - setDefaultChannelCache(cp.ModelName, cp.ChannelId) - } else { - clearDefaultChannelCacheIfMatch(cp.ModelName, cp.Id) - } + } return err } @@ -118,9 +108,7 @@ func (cp *ChannelPricing) Delete() error { err := DB.Delete(cp).Error if err == nil { removeCache(getChannelPricingCacheKey(existing.ModelName, existing.ChannelId)) - if existing.IsDefault { - clearDefaultChannelCache(existing.ModelName) - } + } return err } @@ -243,7 +231,6 @@ func LoadChannelPricingCache() { } channelPricingCacheLock.Unlock() - rebuildDefaultChannelCache(pricings) common.SysLog(fmt.Sprintf("[ChannelPricing] cache loaded %d records", len(pricings))) } @@ -344,107 +331,3 @@ func GetChannelPricingByModelWithChannelInfo(modelName string) ([]*ChannelPricin return results, nil } -// === 默认通道缓存 === - -// syncIsDefaultToCache 将默认标记的变更同步到 channelPricingCache -func syncIsDefaultToCache(modelName string, channelId int, isDefault bool) { - channelPricingCacheLock.Lock() - for key, cp := range channelPricingCache { - if cp.ModelName == modelName { - if isDefault { - cp.IsDefault = cp.ChannelId == channelId - } else { - cp.IsDefault = false - } - } - channelPricingCache[key] = cp - } - channelPricingCacheLock.Unlock() -} - -// setDefaultChannelCache 设置默认通道缓存 -func setDefaultChannelCache(modelName string, channelId int) { - defaultChannelCacheLock.Lock() - defaultChannelCache[modelName] = channelId - defaultChannelCacheLock.Unlock() -} - -// clearDefaultChannelCache 清除指定模型的默认通道缓存 -func clearDefaultChannelCache(modelName string) { - defaultChannelCacheLock.Lock() - delete(defaultChannelCache, modelName) - defaultChannelCacheLock.Unlock() -} - -// clearDefaultChannelCacheIfMatch 如果默认通道的定价记录 ID 匹配则清除 -func clearDefaultChannelCacheIfMatch(modelName string, pricingId int) { - defaultChannelCacheLock.RLock() - cachedId, ok := defaultChannelCache[modelName] - defaultChannelCacheLock.RUnlock() - if !ok { - return - } - // 需要通过缓存找到对应的 pricing 来比对 - key := getChannelPricingCacheKey(modelName, cachedId) - channelPricingCacheLock.RLock() - cp, exists := channelPricingCache[key] - channelPricingCacheLock.RUnlock() - if exists && cp.Id == pricingId { - clearDefaultChannelCache(modelName) - } -} - -// rebuildDefaultChannelCache 从全量数据构建默认通道缓存(启动时调用) -func rebuildDefaultChannelCache(pricings []*ChannelPricing) { - defaultChannelCacheLock.Lock() - defaultChannelCache = make(map[string]int) - for _, cp := range pricings { - if cp.IsDefault { - defaultChannelCache[cp.ModelName] = cp.ChannelId - } - } - defaultChannelCacheLock.Unlock() - common.SysLog(fmt.Sprintf("[ChannelPricing] default channel cache loaded %d records", len(defaultChannelCache))) -} - -// GetDefaultChannelId 获取指定模型的默认通道 ID(纯内存读) -func GetDefaultChannelId(modelName string) (int, bool) { - defaultChannelCacheLock.RLock() - id, ok := defaultChannelCache[modelName] - defaultChannelCacheLock.RUnlock() - return id, ok -} - -// SetDefaultChannel 设置指定模型的默认通道(事务保证互斥) -func SetDefaultChannel(modelName string, channelId int) error { - return DB.Transaction(func(tx *gorm.DB) error { - // 清除该模型所有现有的默认标记 - if err := tx.Model(&ChannelPricing{}). - Where("model_name = ? AND is_default = ?", modelName, true). - Update("is_default", false).Error; err != nil { - return err - } - // 设置新的默认 - if err := tx.Model(&ChannelPricing{}). - Where("model_name = ? AND channel_id = ?", modelName, channelId). - Update("is_default", true).Error; err != nil { - return err - } - // 更新缓存 - setDefaultChannelCache(modelName, channelId) - syncIsDefaultToCache(modelName, channelId, true) - return nil - }) -} - -// ClearDefaultChannel 清除指定模型的默认通道标记 -func ClearDefaultChannel(modelName string) error { - err := DB.Model(&ChannelPricing{}). - Where("model_name = ? AND is_default = ?", modelName, true). - Update("is_default", false).Error - if err == nil { - clearDefaultChannelCache(modelName) - syncIsDefaultToCache(modelName, 0, false) - } - return err -} diff --git a/model/channel_pricing_test.go b/model/channel_pricing_test.go index bb58d3e..9ed8a9f 100644 --- a/model/channel_pricing_test.go +++ b/model/channel_pricing_test.go @@ -119,124 +119,3 @@ func TestGetEffectivePricing_NotFound(t *testing.T) { assert.False(t, ok) assert.Nil(t, found) } - -// === 默认通道测试 === - -func TestDefaultChannel_SetAndGet(t *testing.T) { - setupChannelPricingDB(t) - - // 创建两条定价记录 - cp1 := &ChannelPricing{ModelName: "default-test-model", ChannelId: 100, QuotaType: QuotaTypeByTokens, ModelRatio: 1.0} - cp2 := &ChannelPricing{ModelName: "default-test-model", ChannelId: 200, QuotaType: QuotaTypeByTokens, ModelRatio: 2.0} - require.NoError(t, cp1.Insert()) - require.NoError(t, cp2.Insert()) - t.Cleanup(func() { cp1.Delete(); cp2.Delete() }) - - // 初始没有默认 - _, ok := GetDefaultChannelId("default-test-model") - assert.False(t, ok) - - // 设置通道 100 为默认 - require.NoError(t, SetDefaultChannel("default-test-model", 100)) - id, ok := GetDefaultChannelId("default-test-model") - assert.True(t, ok) - assert.Equal(t, 100, id) - - // 切换默认到通道 200 - require.NoError(t, SetDefaultChannel("default-test-model", 200)) - id, ok = GetDefaultChannelId("default-test-model") - assert.True(t, ok) - assert.Equal(t, 200, id) - - // 验证旧的默认标记被清除 - found, _ := GetEffectivePricing("default-test-model", 100) - assert.False(t, found.IsDefault) - found, _ = GetEffectivePricing("default-test-model", 200) - assert.True(t, found.IsDefault) -} - -func TestDefaultChannel_Clear(t *testing.T) { - setupChannelPricingDB(t) - - cp := &ChannelPricing{ModelName: "clear-test-model", ChannelId: 300, QuotaType: QuotaTypeByTokens, ModelRatio: 1.0} - require.NoError(t, cp.Insert()) - t.Cleanup(func() { cp.Delete() }) - - require.NoError(t, SetDefaultChannel("clear-test-model", 300)) - _, ok := GetDefaultChannelId("clear-test-model") - assert.True(t, ok) - - require.NoError(t, ClearDefaultChannel("clear-test-model")) - _, ok = GetDefaultChannelId("clear-test-model") - assert.False(t, ok) -} - -func TestDefaultChannel_DeleteClearsCache(t *testing.T) { - setupChannelPricingDB(t) - - cp := &ChannelPricing{ModelName: "delete-test-model", ChannelId: 400, QuotaType: QuotaTypeByTokens, ModelRatio: 1.0, IsDefault: true} - require.NoError(t, cp.Insert()) - - id, ok := GetDefaultChannelId("delete-test-model") - assert.True(t, ok) - assert.Equal(t, 400, id) - - // 删除记录后缓存应清除 - require.NoError(t, cp.Delete()) - _, ok = GetDefaultChannelId("delete-test-model") - assert.False(t, ok) -} - -func TestDefaultChannel_LoadCache(t *testing.T) { - db := setupChannelPricingDB(t) - - // 直接插入数据(绕过缓存) - db.Create(&ChannelPricing{ModelName: "load-model", ChannelId: 500, ModelRatio: 1.0, IsDefault: true}) - db.Create(&ChannelPricing{ModelName: "load-model", ChannelId: 501, ModelRatio: 2.0, IsDefault: false}) - db.Create(&ChannelPricing{ModelName: "other-model", ChannelId: 502, ModelRatio: 1.0, IsDefault: true}) - - // 全量加载缓存 - LoadChannelPricingCache() - - id, ok := GetDefaultChannelId("load-model") - assert.True(t, ok) - assert.Equal(t, 500, id) - - id, ok = GetDefaultChannelId("other-model") - assert.True(t, ok) - assert.Equal(t, 502, id) - - // 无默认的模型 - _, ok = GetDefaultChannelId("nonexistent") - assert.False(t, ok) -} - -func TestDefaultChannel_InsertWithDefault(t *testing.T) { - setupChannelPricingDB(t) - - cp := &ChannelPricing{ModelName: "insert-default-model", ChannelId: 600, ModelRatio: 1.0, IsDefault: true} - require.NoError(t, cp.Insert()) - t.Cleanup(func() { cp.Delete() }) - - id, ok := GetDefaultChannelId("insert-default-model") - assert.True(t, ok) - assert.Equal(t, 600, id) -} - -func TestDefaultChannel_UpdateWithDefault(t *testing.T) { - setupChannelPricingDB(t) - - cp1 := &ChannelPricing{ModelName: "update-default-model", ChannelId: 700, ModelRatio: 1.0, IsDefault: true} - cp2 := &ChannelPricing{ModelName: "update-default-model", ChannelId: 701, ModelRatio: 2.0} - require.NoError(t, cp1.Insert()) - require.NoError(t, cp2.Insert()) - t.Cleanup(func() { cp1.Delete(); cp2.Delete() }) - - // cp1 是默认,通过 Update 把 cp2 设为默认 - cp2.IsDefault = true - require.NoError(t, cp2.Update()) - - id, ok := GetDefaultChannelId("update-default-model") - assert.True(t, ok) - assert.Equal(t, 701, id) -} diff --git a/model/pricing.go b/model/pricing.go index 991d89f..c287b7c 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -32,7 +32,6 @@ type Pricing struct { SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` PricingVersion string `json:"pricing_version,omitempty"` Type int `json:"type"` - DefaultChannelName string `json:"default_channel_name,omitempty"` } type PricingVendor struct { @@ -350,12 +349,6 @@ func updatePricing() { // 使用渠道定价表中的实际数据,选取最便宜的渠道 applyBestChannelPricing(&pricing, cpMap[model], model) - // 填充默认通道名称 - if chId, ok := GetDefaultChannelId(model); ok { - if name, found := channelNameMap[chId]; found { - pricing.DefaultChannelName = name - } - } pricingMap = append(pricingMap, pricing) } diff --git a/router/api-router.go b/router/api-router.go index 158c079..16c9b4d 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -77,9 +77,7 @@ func SetApiRouter(router *gin.Engine) { selfRoute.GET("/self/groups", controller.GetUserGroups) selfRoute.GET("/self", controller.GetSelf) selfRoute.GET("/models", controller.GetUserModels) - selfRoute.GET("/model_channels", controller.GetModelChannels) - selfRoute.GET("/channels", controller.GetUserChannelsForBinding) - selfRoute.PUT("/self", controller.UpdateSelf) + selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.DELETE("/self", controller.DeleteSelf) selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/passkey", controller.PasskeyStatus) @@ -204,9 +202,7 @@ func SetApiRouter(router *gin.Engine) { channelPricingRoute.POST("/batch", controller.BatchCreateChannelPricing) channelPricingRoute.POST("/copy_global/:channel_id", controller.CopyGlobalPricing) channelPricingRoute.DELETE("/:id", controller.DeleteChannelPricing) - channelPricingRoute.POST("/set_default", controller.SetDefaultChannel) - channelPricingRoute.DELETE("/default/*name", controller.ClearDefaultChannel) - } + } // 用户倍率路由(管理员权限) userChannelRatioRoute := apiRouter.Group("/user_channel_ratio")