Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ var AutomaticDisableChannelEnabled = false
var AutomaticEnableChannelEnabled = false
var QuotaRemindThreshold = 1000
var PreConsumedQuota = 500
var RetryAvoidUsedChannelEnabled = false
var RetryPriorityMode = "sequential" // "sequential" 或 "round-robin"

var RetryTimes = 0

Expand Down
43 changes: 42 additions & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"strconv"
"strings"

"github.com/QuantumNous/new-api/common"
Expand Down Expand Up @@ -186,6 +187,15 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
break
}

// 如果 channel 为 nil(该优先级的所有渠道都被排除),切换到下一个优先级
// If channel is nil (all channels at this priority have been excluded), switch to next priority
if channel == nil {
retryParam.IncreasePriorityIndex()
logger.LogInfo(c, fmt.Sprintf("优先级用尽,切换到下一个优先级。当前 priorityIndex: %d", retryParam.GetPriorityIndex()))
continue
}

logger.LogInfo(c, fmt.Sprintf("选择渠道 #%d,retry=%d, priorityIndex=%d, mode=%s", channel.Id, retryParam.GetRetry(), retryParam.GetPriorityIndex(), common.RetryPriorityMode))
addUsedChannel(c, channel.Id)
requestBody, bodyErr := common.GetRequestBody(c)
if bodyErr != nil {
Expand Down Expand Up @@ -219,6 +229,20 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) {
break
}

// 注意:在 round-robin 模式下,不需要在这里增加 priorityIndex
// GetRandomSatisfiedChannel 中的模运算会自动处理优先级循环
// Note: In round-robin mode, no need to increase priorityIndex here
// The modulo operation in GetRandomSatisfiedChannel handles priority cycling automatically
}

// 如果所有重试都完成了,但没有找到可用渠道,返回错误
// If all retries are exhausted but no available channel found, return error
if newAPIError == nil && len(c.GetStringSlice("use_channel")) == 0 {
newAPIError = types.NewError(
fmt.Errorf("分组 %s 下模型 %s 的所有可用渠道都已被排除或不存在", relayInfo.TokenGroup, relayInfo.OriginModelName),
types.ErrorCodeGetChannelFailed,
)
}

useChannel := c.GetStringSlice("use_channel")
Expand Down Expand Up @@ -282,6 +306,21 @@ func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service
AutoBan: &autoBanInt,
}, nil
}

// 如果开关开启且处于重试阶段,从 use_channel 构建排除集合
if common.RetryAvoidUsedChannelEnabled && retryParam.GetRetry() > 0 {
useChannelStrs := c.GetStringSlice("use_channel")
for _, idStr := range useChannelStrs {
if id, err := strconv.Atoi(idStr); err == nil {
retryParam.AddUsedChannel(id)
}
}
// 记录排除信息到日志
if len(retryParam.UsedChannelIds) > 0 {
logger.LogInfo(c, fmt.Sprintf("重试排除了 %d 个已用渠道", len(retryParam.UsedChannelIds)))
}
}

channel, selectGroup, err := service.CacheGetRandomSatisfiedChannel(retryParam)

info.PriceData.GroupRatioInfo = helper.HandleGroupRatio(c, info)
Expand All @@ -290,7 +329,9 @@ func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service
return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, info.OriginModelName, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
}
if channel == nil {
return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(retry)", selectGroup, info.OriginModelName), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
// 该优先级的所有渠道都被排除,返回 nil 以便继续尝试下一个优先级
// All channels at this priority have been excluded, return nil to continue trying next priority
return nil, nil
}

newAPIError := middleware.SetupContextForSelectedChannel(c, channel, info.OriginModelName)
Expand Down
21 changes: 18 additions & 3 deletions model/ability.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,13 @@ func getPriority(group string, model string, retry int) (int, error) {

// 确定要使用的优先级
var priorityToUse int
if retry >= len(priorities) {
// 如果重试次数大于优先级数,则使用最小的优先级
if common.RetryPriorityMode == "round-robin" && len(priorities) > 0 {
// 轮询模式:始终使用模运算循环
// Round-robin mode: always use modulo operation to cycle through priorities
priorityToUse = priorities[retry%len(priorities)]
} else if retry >= len(priorities) {
// 顺序模式:如果重试次数大于优先级数,则使用最小的优先级
// Sequential mode: if retry exceeds priority count, use lowest priority
priorityToUse = priorities[len(priorities)-1]
} else {
priorityToUse = priorities[retry]
Expand All @@ -103,14 +108,24 @@ func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) {
return channelQuery, nil
}

func GetChannel(group string, model string, retry int) (*Channel, error) {
func GetChannel(group string, model string, retry int, excludeIds map[int]struct{}) (*Channel, error) {
var abilities []Ability

var err error = nil
channelQuery, err := getChannelQuery(group, model, retry)
if err != nil {
return nil, err
}

// 如果有排除列表,添加 NOT IN 条件
if len(excludeIds) > 0 {
var excludeIdList []int
for id := range excludeIds {
excludeIdList = append(excludeIdList, id)
}
channelQuery = channelQuery.Where("channel_id NOT IN ?", excludeIdList)
}

if common.UsingSQLite || common.UsingPostgreSQL {
err = channelQuery.Order("weight DESC").Find(&abilities).Error
} else {
Expand Down
30 changes: 26 additions & 4 deletions model/channel_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ func SyncChannelCache(frequency int) {
}
}

func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
func GetRandomSatisfiedChannel(group string, model string, retry int, excludeIds map[int]struct{}) (*Channel, error) {
// if memory cache is disabled, get channel directly from database
if !common.MemoryCacheEnabled {
return GetChannel(group, model, retry)
return GetChannel(group, model, retry, excludeIds)
}

channelSyncLock.RLock()
Expand All @@ -117,6 +117,12 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,

if len(channels) == 1 {
if channel, ok := channelsIDM[channels[0]]; ok {
// 检查是否需要排除
if len(excludeIds) > 0 {
if _, excluded := excludeIds[channels[0]]; excluded {
return nil, nil // 唯一渠道已被排除
}
}
return channel, nil
}
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channels[0])
Expand All @@ -136,7 +142,15 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
}
sort.Sort(sort.Reverse(sort.IntSlice(sortedUniquePriorities)))

if retry >= len(uniquePriorities) {
// 轮询模式:始终使用模运算来循环优先级
// Round-robin mode: always use modulo operation to cycle through priorities
// 顺序模式:超出范围时使用最低优先级
// Sequential mode: use lowest priority when out of range
if common.RetryPriorityMode == "round-robin" && len(uniquePriorities) > 0 {
// 轮询模式:始终使用模运算循环
retry = retry % len(uniquePriorities)
} else if retry >= len(uniquePriorities) {
// 顺序模式:超出范围时使用最低优先级
retry = len(uniquePriorities) - 1
}
targetPriority := int64(sortedUniquePriorities[retry])
Expand All @@ -147,6 +161,12 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
for _, channelId := range channels {
if channel, ok := channelsIDM[channelId]; ok {
if channel.GetPriority() == targetPriority {
// 如果需要排除且渠道在排除列表中,则跳过
if len(excludeIds) > 0 {
if _, excluded := excludeIds[channelId]; excluded {
continue
}
}
sumWeight += channel.GetWeight()
targetChannels = append(targetChannels, channel)
}
Expand All @@ -156,7 +176,9 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
}

if len(targetChannels) == 0 {
return nil, errors.New(fmt.Sprintf("no channel found, group: %s, model: %s, priority: %d", group, model, targetPriority))
// 该优先级的所有渠道都被排除,返回 nil 以便尝试下一个优先级
// All channels at this priority have been excluded, return nil to try next priority
return nil, nil
}

// smoothing factor and adjustment
Expand Down
8 changes: 8 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func InitOptionMap() {
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
common.OptionMap["RetryAvoidUsedChannelEnabled"] = strconv.FormatBool(common.RetryAvoidUsedChannelEnabled)
common.OptionMap["RetryPriorityMode"] = common.RetryPriorityMode
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
Expand Down Expand Up @@ -243,6 +245,8 @@ func updateOptionMap(key string, value string) (err error) {
common.AutomaticDisableChannelEnabled = boolValue
case "AutomaticEnableChannelEnabled":
common.AutomaticEnableChannelEnabled = boolValue
case "RetryAvoidUsedChannelEnabled":
common.RetryAvoidUsedChannelEnabled = boolValue
case "LogConsumeEnabled":
common.LogConsumeEnabled = boolValue
case "DisplayInCurrencyEnabled":
Expand Down Expand Up @@ -407,6 +411,10 @@ func updateOptionMap(key string, value string) (err error) {
err = setting.UpdateModelRequestRateLimitGroupByJSONString(value)
case "RetryTimes":
common.RetryTimes, _ = strconv.Atoi(value)
case "RetryPriorityMode":
if value == "sequential" || value == "round-robin" {
common.RetryPriorityMode = value
}
Comment on lines +414 to +417

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject invalid RetryPriorityMode values instead of silently accepting them

Right now an unsupported value still gets stored in common.OptionMap and the update returns nil, so the UI sees “saved” even though runtime keeps the old mode. Since UpdateOption saves to DB before validation, this can persist a bad value and create a confusing mismatch.

Consider validating and returning an error (and restoring the OptionMap entry). Ideally validate before DB save in UpdateOption as well.

🛠️ Suggested fix
-import (
-	"strconv"
-	"strings"
-	"time"
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
@@
 	case "RetryPriorityMode":
-		if value == "sequential" || value == "round-robin" {
-			common.RetryPriorityMode = value
-		}
+		if value != "sequential" && value != "round-robin" {
+			common.OptionMap[key] = common.RetryPriorityMode
+			return fmt.Errorf("invalid RetryPriorityMode: %s", value)
+		}
+		common.RetryPriorityMode = value
🤖 Prompt for AI Agents
In `@model/option.go` around lines 414 - 417, The branch handling the
"RetryPriorityMode" option currently accepts any value into common.OptionMap and
only sets common.RetryPriorityMode for "sequential" or "round-robin", causing
invalid values to persist; modify the setter in the switch for
"RetryPriorityMode" to validate the incoming value and return an error for any
unsupported value instead of silently accepting it, and update UpdateOption to
validate RetryPriorityMode (and any similar enums) before persisting to the DB —
if a validation fails, restore the previous entry in common.OptionMap (or avoid
mutating it) and return the error so the UI does not show “saved.” Ensure
references to RetryPriorityMode, common.RetryPriorityMode, common.OptionMap and
the UpdateOption function are used to locate and change the logic.

case "DataExportInterval":
common.DataExportInterval, _ = strconv.Atoi(value)
case "DataExportDefaultTime":
Expand Down
55 changes: 48 additions & 7 deletions service/channel_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import (
)

type RetryParam struct {
Ctx *gin.Context
TokenGroup string
ModelName string
Retry *int
resetNextTry bool
Ctx *gin.Context
TokenGroup string
ModelName string
Retry *int
resetNextTry bool
UsedChannelIds map[int]struct{} // 已使用的渠道ID集合
CurrentPriorityIndex int // 当前使用的优先级索引
}

func (p *RetryParam) GetRetry() int {
Expand Down Expand Up @@ -45,6 +47,33 @@ func (p *RetryParam) ResetRetryNextTry() {
p.resetNextTry = true
}

// AddUsedChannel 添加已使用的渠道ID
func (p *RetryParam) AddUsedChannel(channelId int) {
if p.UsedChannelIds == nil {
p.UsedChannelIds = make(map[int]struct{})
}
p.UsedChannelIds[channelId] = struct{}{}
}

// IsChannelUsed 检查渠道是否已被使用
func (p *RetryParam) IsChannelUsed(channelId int) bool {
if p.UsedChannelIds == nil {
return false
}
_, used := p.UsedChannelIds[channelId]
return used
}

// IncreasePriorityIndex 增加优先级索引(切换到下一个优先级)
func (p *RetryParam) IncreasePriorityIndex() {
p.CurrentPriorityIndex++
}

// GetPriorityIndex 获取当前优先级索引
func (p *RetryParam) GetPriorityIndex() int {
return p.CurrentPriorityIndex
}

// CacheGetRandomSatisfiedChannel tries to get a random channel that satisfies the requirements.
// 尝试获取一个满足要求的随机渠道。
//
Expand Down Expand Up @@ -115,7 +144,7 @@ func CacheGetRandomSatisfiedChannel(param *RetryParam) (*model.Channel, string,
}
logger.LogDebug(param.Ctx, "Auto selecting group: %s, priorityRetry: %d", autoGroup, priorityRetry)

channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry)
channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry, param.UsedChannelIds)
if channel == nil {
// Current group has no available channel for this model, try next group
// 当前分组没有该模型的可用渠道,尝试下一个分组
Expand Down Expand Up @@ -153,10 +182,22 @@ func CacheGetRandomSatisfiedChannel(param *RetryParam) (*model.Channel, string,
break
}
} else {
channel, err = model.GetRandomSatisfiedChannel(param.TokenGroup, param.ModelName, param.GetRetry())
// 在 round-robin 模式下,使用 retry 参数(会在 GetRandomSatisfiedChannel 中进行模运算)
// 在 sequential 模式下,使用 priorityIndex(只有当前优先级用尽时才会增加)
// In round-robin mode, use retry parameter (modulo operation in GetRandomSatisfiedChannel)
// In sequential mode, use priorityIndex (only increases when current priority is exhausted)
retryOrPriority := param.GetPriorityIndex()
if common.RetryPriorityMode == "round-robin" {
retryOrPriority = param.GetRetry()
}
channel, err = model.GetRandomSatisfiedChannel(param.TokenGroup, param.ModelName, retryOrPriority, param.UsedChannelIds)
if err != nil {
return nil, param.TokenGroup, err
}
// 如果 channel 为 nil 但没有错误,说明该优先级的所有渠道都被排除了
// 返回 nil 以便外层循环继续尝试下一个优先级
// If channel is nil but no error, it means all channels at this priority have been excluded
// Return nil to allow outer loop to try next priority
}
return channel, selectGroup, nil
}
2 changes: 2 additions & 0 deletions web/src/components/settings/OperationSetting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const OperationSetting = () => {
QuotaRemindThreshold: 0,
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
RetryAvoidUsedChannelEnabled: false,
RetryPriorityMode: 'sequential',
AutomaticDisableKeywords: '',
AutomaticDisableStatusCodes: '401',
'monitor_setting.auto_test_channel_enabled': false,
Expand Down
7 changes: 7 additions & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,13 @@
"成功": "Success",
"成功兑换额度:": "Successful redemption amount:",
"成功时自动启用通道": "Enable channel when successful",
"重试时避开已尝试渠道": "Avoid used channels on retry",
"启用后,重试时不会再次选择已失败的渠道。注意:渠道数量较少时可能更早失败。": "When enabled, retries will not select channels that have already failed. Note: May fail earlier when there are fewer channels.",
"重试优先级模式": "Retry Priority Mode",
"选择重试模式": "Select retry mode",
"顺序:同一优先级内尝试所有渠道后才降级;轮询:每个优先级轮流尝试": "Sequential: Try all channels in same priority before downgrade; Round-robin: Rotate through priorities",
"分组顺序重试": "Sequential Retry",
"分组轮询重试": "Round-robin Retry",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "I have understood that disabling two-factor authentication will permanently delete all related settings and backup codes, this operation cannot be undone",
"我已阅读并同意": "I have read and agree to",
"或": "or",
Expand Down
7 changes: 7 additions & 0 deletions web/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,13 @@
"成功": "成功",
"成功兑换额度:": "成功兑换额度:",
"成功时自动启用通道": "成功时自动启用通道",
"重试时避开已尝试渠道": "重试时避开已尝试渠道",
"启用后,重试时不会再次选择已失败的渠道。注意:渠道数量较少时可能更早失败。": "启用后,重试时不会再次选择已失败的渠道。注意:渠道数量较少时可能更早失败。",
"重试优先级模式": "重试优先级模式",
"选择重试模式": "选择重试模式",
"顺序:同一优先级内尝试所有渠道后才降级;轮询:每个优先级轮流尝试": "顺序:同一优先级内尝试所有渠道后才降级;轮询:每个优先级轮流尝试",
"分组顺序重试": "分组顺序重试",
"分组轮询重试": "分组轮询重试",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销",
"我已阅读并同意": "我已阅读并同意",
"或": "或",
Expand Down
Loading