Skip to content
Merged
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
5 changes: 3 additions & 2 deletions controller/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"

Expand All @@ -27,17 +28,17 @@ func GetUserGroups(c *gin.Context) {
userGroup := ""
userId := c.GetInt("id")
userGroup, _ = model.GetUserGroup(userId, false)
userUsableGroups := service.GetUserUsableGroups(userGroup)
for groupName, ratio := range ratio_setting.GetGroupRatioCopy() {
// UserUsableGroups contains the groups that the user can use
userUsableGroups := setting.GetUserUsableGroups(userGroup)
if desc, ok := userUsableGroups[groupName]; ok {
usableGroups[groupName] = map[string]interface{}{
"ratio": ratio,
"desc": desc,
}
}
}
if setting.GroupInUserUsableGroups("auto") {
if _, ok := userUsableGroups["auto"]; ok {
usableGroups["auto"] = map[string]interface{}{
"ratio": "自动",
"desc": setting.GetUsableGroupDescription("auto"),
Expand Down
4 changes: 2 additions & 2 deletions controller/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/minimax"
"github.com/QuantumNous/new-api/relay/channel/moonshot"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
Expand Down Expand Up @@ -149,7 +149,7 @@ func ListModels(c *gin.Context, modelType int) {
}
var models []string
if tokenGroup == "auto" {
for _, autoGroup := range setting.AutoGroups {
for _, autoGroup := range service.GetUserAutoGroup(userGroup) {
groupModels := model.GetGroupEnabledModels(autoGroup)
for _, g := range groupModels {
if !common.StringsContains(models, g) {
Expand Down
6 changes: 3 additions & 3 deletions controller/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package controller

import (
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/ratio_setting"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -30,7 +30,7 @@ func GetPricing(c *gin.Context) {
}
}

usableGroup = setting.GetUserUsableGroups(group)
usableGroup = service.GetUserUsableGroups(group)
// check groupRatio contains usableGroup
for group := range ratio_setting.GetGroupRatioCopy() {
if _, ok := usableGroup[group]; !ok {
Expand All @@ -45,7 +45,7 @@ func GetPricing(c *gin.Context) {
"group_ratio": groupRatio,
"usable_group": usableGroup,
"supported_endpoint": model.GetSupportedEndpointMap(),
"auto_groups": setting.AutoGroups,
"auto_groups": service.GetUserAutoGroup(group),
})
}

Expand Down
2 changes: 1 addition & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
AutoBan: &autoBanInt,
}, nil
}
channel, selectGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
channel, selectGroup, err := service.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
if err != nil {
return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
}
Expand Down
3 changes: 2 additions & 1 deletion controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"

"github.com/QuantumNous/new-api/constant"
Expand Down Expand Up @@ -579,7 +580,7 @@ func GetUserModels(c *gin.Context) {
common.ApiError(c, err)
return
}
groups := setting.GetUserUsableGroups(user.Group)
groups := service.GetUserUsableGroups(user.Group)
var models []string
for group := range groups {
for _, g := range model.GetGroupEnabledModels(group) {
Expand Down
3 changes: 2 additions & 1 deletion logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ func LogError(ctx context.Context, msg string) {
logHelper(ctx, loggerError, msg)
}

func LogDebug(ctx context.Context, msg string) {
func LogDebug(ctx context.Context, msg string, args ...any) {
msg = fmt.Sprintf(msg, args...)
if common.DebugEnabled {
Comment on lines +69 to 71

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 | 🔴 Critical

🧩 Analysis chain

Avoid formatting when debug is disabled; also verify callsites use printf-style.

fmt.Sprintf runs even if debug logs are off. Guard it to avoid work on hot paths. Also ensure callers provide format verbs now that args are variadic.

Apply:

-func LogDebug(ctx context.Context, msg string, args ...any) {
-	msg = fmt.Sprintf(msg, args...)
-	if common.DebugEnabled {
-		logHelper(ctx, loggerDebug, msg)
-	}
-}
+func LogDebug(ctx context.Context, msg string, args ...any) {
+	if !common.DebugEnabled {
+		return
+	}
+	if len(args) > 0 {
+		msg = fmt.Sprintf(msg, args...)
+	}
+	logHelper(ctx, loggerDebug, msg)
+}

Find callsites that pass extra args but no format verbs:


🏁 Script executed:

#!/bin/bash
# Calls with extra args and a string literal without '%' in the format.
rg -nP 'logger\.LogDebug\([^,]+,\s*"[^"%]*"\s*,'
# Calls passing a likely gin Context as first arg.
rg -nP 'logger\.LogDebug\(\s*c\s*,'

Length of output: 1486


Fix format strings at callsites before applying LogDebug refactor.

Verification found 2 critical mismatches in service/channel_select.go:

  • Line 24: logger.LogDebug(c, "Auto selecting group:", autoGroup) — format string has no %v but passes autoGroup
  • Line 31: logger.LogDebug(c, "Auto selected group:", autoGroup) — same issue

These must be converted to printf-style:

logger.LogDebug(c, "Auto selecting group: %v", autoGroup)
logger.LogDebug(c, "Auto selected group: %v", autoGroup)

After fixing these 2 callsites, the proposed refactor can be safely applied.

🤖 Prompt for AI Agents
In logger/logger.go around lines 69 to 71, the new LogDebug expects printf-style
format strings but two callsites in service/channel_select.go (line 24 and line
31) pass values without format verbs; update those calls to use printf
formatting (e.g., change "Auto selecting group:" with autoGroup to "Auto
selecting group: %v" and "Auto selected group:" with autoGroup to "Auto selected
group: %v") so the arguments match the format string, then proceed with the
LogDebug refactor.

logHelper(ctx, loggerDebug, msg)
}
Expand Down
6 changes: 3 additions & 3 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/ratio_setting"

"github.com/gin-contrib/sessions"
Expand Down Expand Up @@ -266,8 +266,8 @@ func TokenAuth() func(c *gin.Context) {
tokenGroup := token.Group
if tokenGroup != "" {
// check common.UserUsableGroups[userGroup]
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
if _, ok := service.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("无权访问 %s 分组", tokenGroup))
return
}
// check group in common.GroupRatio
Expand Down
15 changes: 7 additions & 8 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/QuantumNous/new-api/model"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"

Expand Down Expand Up @@ -80,7 +79,7 @@ func Distribute() func(c *gin.Context) {
return
}
var selectGroup string
userGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
// check path is /pg/chat/completions
if strings.HasPrefix(c.Request.URL.Path, "/pg/chat/completions") {
playgroundRequest := &dto.PlayGroundRequest{}
Expand All @@ -90,17 +89,17 @@ func Distribute() func(c *gin.Context) {
return
}
if playgroundRequest.Group != "" {
if !setting.GroupInUserUsableGroups(playgroundRequest.Group) && playgroundRequest.Group != userGroup {
if !service.GroupInUserUsableGroups(usingGroup, playgroundRequest.Group) && playgroundRequest.Group != usingGroup {
abortWithOpenAiMessage(c, http.StatusForbidden, "无权访问该分组")
return
}
userGroup = playgroundRequest.Group
usingGroup = playgroundRequest.Group
}
}
channel, selectGroup, err = model.CacheGetRandomSatisfiedChannel(c, userGroup, modelRequest.Model, 0)
channel, selectGroup, err = service.CacheGetRandomSatisfiedChannel(c, usingGroup, modelRequest.Model, 0)
if err != nil {
showGroup := userGroup
if userGroup == "auto" {
showGroup := usingGroup
if usingGroup == "auto" {
showGroup = fmt.Sprintf("auto(%s)", selectGroup)
}
message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(distributor): %s", showGroup, modelRequest.Model, err.Error())
Expand All @@ -113,7 +112,7 @@ func Distribute() func(c *gin.Context) {
return
}
if channel == nil {
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", userGroup, modelRequest.Model), string(types.ErrorCodeModelNotFound))
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", usingGroup, modelRequest.Model), string(types.ErrorCodeModelNotFound))
return
}
}
Expand Down
2 changes: 1 addition & 1 deletion model/ability.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) {
return channelQuery, nil
}

func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
func GetChannel(group string, model string, retry int) (*Channel, error) {
var abilities []Ability

var err error = nil
Expand Down
58 changes: 14 additions & 44 deletions model/channel_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import (

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"

"github.com/gin-gonic/gin"
)

var group2model2channels map[string]map[string][]int // enabled channel
Expand Down Expand Up @@ -96,43 +93,10 @@ func SyncChannelCache(frequency int) {
}
}

func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, model string, retry int) (*Channel, string, error) {
var channel *Channel
var err error
selectGroup := group
if group == "auto" {
if len(setting.AutoGroups) == 0 {
return nil, selectGroup, errors.New("auto groups is not enabled")
}
for _, autoGroup := range setting.AutoGroups {
if common.DebugEnabled {
println("autoGroup:", autoGroup)
}
channel, _ = getRandomSatisfiedChannel(autoGroup, model, retry)
if channel == nil {
continue
} else {
c.Set("auto_group", autoGroup)
selectGroup = autoGroup
if common.DebugEnabled {
println("selectGroup:", selectGroup)
}
break
}
}
} else {
channel, err = getRandomSatisfiedChannel(group, model, retry)
if err != nil {
return nil, group, err
}
}
return channel, selectGroup, nil
}

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

channelSyncLock.RLock()
Expand Down Expand Up @@ -178,30 +142,36 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
targetPriority := int64(sortedUniquePriorities[retry])

// get the priority for the given retry number
var shouldSmooth = false
var sumWeight = 0
var targetChannels []*Channel
for _, channelId := range channels {
if channel, ok := channelsIDM[channelId]; ok {
if channel.GetPriority() == targetPriority {
sumWeight += channel.GetWeight()
targetChannels = append(targetChannels, channel)
}
} else {
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
}
}
if sumWeight/len(targetChannels) < 10 {
shouldSmooth = true
}

// 平滑系数
smoothingFactor := 10
// Calculate the total weight of all channels up to endIdx
totalWeight := 0
for _, channel := range targetChannels {
totalWeight += channel.GetWeight() + smoothingFactor
smoothingFactor := 1
if shouldSmooth {
smoothingFactor = 100
}
// Calculate the total weight of all channels up to endIdx
totalWeight := sumWeight * smoothingFactor
// Generate a random value in the range [0, totalWeight)
randomWeight := rand.Intn(totalWeight)

// Find a channel based on its weight
for _, channel := range targetChannels {
randomWeight -= channel.GetWeight() + smoothingFactor
randomWeight -= channel.GetWeight() * smoothingFactor
if randomWeight < 0 {
return channel, nil
Comment on lines +145 to 176

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 | 🔴 Critical

🧩 Analysis chain

Fix possible panic (rand.Intn(0)) and align weighting with DB path

If all target channel weights are zero, totalWeight becomes 0, causing rand.Intn(0) to panic. DB path uses a +10 baseline; mirror it to keep parity.

Apply this diff:

-	var shouldSmooth = false
-	var sumWeight = 0
+	var shouldSmooth = false
+	var sumWeight = 0
 	...
-	if sumWeight/len(targetChannels) < 10 {
+	if sumWeight/len(targetChannels) < 10 {
 		shouldSmooth = true
 	}
 	// 平滑系数
-	smoothingFactor := 1
+	smoothingFactor := 1
 	if shouldSmooth {
 		smoothingFactor = 100
 	}
-	// Calculate the total weight of all channels up to endIdx
-	totalWeight := sumWeight * smoothingFactor
-	// Generate a random value in the range [0, totalWeight)
-	randomWeight := rand.Intn(totalWeight)
+	// Add baseline to match DB selection logic (+10 per channel)
+	const base = 10
+	totalWeight := (sumWeight + base*len(targetChannels)) * smoothingFactor
+	// Guard: if somehow still zero (shouldn't), fallback to uniform
+	if totalWeight <= 0 {
+		return targetChannels[rand.Intn(len(targetChannels))], nil
+	}
+	randomWeight := rand.Intn(totalWeight)
 	// Find a channel based on its weight
 	for _, channel := range targetChannels {
-		randomWeight -= channel.GetWeight() * smoothingFactor
+		randomWeight -= (channel.GetWeight() + base) * smoothingFactor
 		if randomWeight < 0 {
 			return channel, nil
 		}
 	}

🏁 Script executed:

#!/bin/bash
# Sanity: search for any stale call sites to old channel selection APIs.
rg -nP -C2 '\b(CacheGetRandomSatisfiedChannel|getRandomSatisfiedChannel\()' || true
# Optional: check all usages of GetRandomSatisfiedChannel now reference the exported symbol.
rg -nP -C2 '\bGetRandomSatisfiedChannel\('

Length of output: 2501



🏁 Script executed:

#!/bin/bash
# Examine the actual code at lines 145-176 in model/channel_cache.go
sed -n '90,200p' model/channel_cache.go

Length of output: 3102


🏁 Script executed:

#!/bin/bash
# Search for DB-related channel selection logic that uses baseline weighting
rg -nP -C5 'GetChannel|DatabaseGetRandomSatisfiedChannel' model/channel_cache.go || true

Length of output: 1020


🏁 Script executed:

#!/bin/bash
# Search for references to +10 baseline or weight baseline in the file
rg -nP -C3 '\+\s*10|baseline' model/channel_cache.go || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's a pattern where weight + 10 is used elsewhere
rg -nP 'weight.*\+.*10|10.*weight' model/channel_cache.go || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search the entire channel_cache file for GetChannel method to understand the DB fallback
rg -nA 10 'func.*GetChannel\(' model/channel_cache.go || true

Length of output: 304


🏁 Script executed:

#!/bin/bash
# Check the full context of the GetRandomSatisfiedChannel function definition
sed -n '96,180p' model/channel_cache.go

Length of output: 2539


🏁 Script executed:

#!/bin/bash
# Look for any constants or baseline definitions near the function
rg -nP 'const|var.*=.*10' model/channel_cache.go | head -20

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Check if there's database selection logic in the same file
rg -nP 'database|db\.' model/channel_cache.go | head -15 || true

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Search for the fallback GetChannel call mentioned in the scratchpad
rg -nA 15 'GetChannel.*\(' model/channel_cache.go

Length of output: 1326


🏁 Script executed:

#!/bin/bash
# Check the structure to see if there's an old implementation or fallback path
rg -nP 'rand\.Intn|randomWeight' model/channel_cache.go

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Look broadly at the entire function to understand its structure
cat model/channel_cache.go | grep -n "func GetRandomSatisfiedChannel" -A 100 | head -120

Length of output: 3221


🏁 Script executed:

#!/bin/bash
# Search for channel weight baseline logic in related files
rg -rn '\+\s*10.*weight|weight.*\+.*10' service/ model/ || true

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check if there's a comment or code mentioning database selection with baseline
rg -n 'database\|GetChannel\|db selection' model/channel_cache.go -C 3 || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Directly extract and display the specific lines mentioned
sed -n '145,176p' model/channel_cache.go

Length of output: 923


🏁 Script executed:

#!/bin/bash
# Get full context: 20 lines before and after the target lines
sed -n '125,196p' model/channel_cache.go

Length of output: 1962


🏁 Script executed:

#!/bin/bash
# Find all function definitions to understand file structure
rg -n '^func' model/channel_cache.go

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Search for any place where a weight baseline is added
rg -nP 'weight\s*\+\s*10|base\s*=\s*10' . --type go -g '!**/test' || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the database fallback implementation pattern
rg -nA 20 'MemoryCacheEnabled.*false|database.*select' model/channel_cache.go || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check what comes before and after the problematic code section
awk 'NR>=130 && NR<=185' model/channel_cache.go

Length of output: 1606


🏁 Script executed:

#!/bin/bash
# Search for channel weight calculations in the db layer
find . -name "*.go" -type f | xargs grep -l "GetChannel\|database" | xargs grep -n "weight" | grep -v test | head -20

Length of output: 1491


🏁 Script executed:

#!/bin/bash
# Let's search for any DB layer channel selection
rg -n 'GetChannel|FromDatabase' model/ -C 2 | head -50 || true

Length of output: 2027


🏁 Script executed:

#!/bin/bash
# Check the entire flow to see what happens when MemoryCacheEnabled is false
sed -n '96,120p' model/channel_cache.go

Length of output: 780


🏁 Script executed:

#!/bin/bash
# Get the database selection code path
rg -nA 30 'MemoryCacheEnabled' model/channel_cache.go | head -60

Length of output: 1992


🏁 Script executed:

#!/bin/bash
# Simple: just cat the whole file to understand structure
head -250 model/channel_cache.go | tail -150

Length of output: 3925


🏁 Script executed:

#!/bin/bash
# Direct extraction
awk 'NR>=140 && NR<=180' model/channel_cache.go

Length of output: 1162


🏁 Script executed:

#!/bin/bash
# Look at wider context around the indicated lines
head -200 model/channel_cache.go | tail -80

Length of output: 2191


🏁 Script executed:

#!/bin/bash
# Most direct approach - get lines 145-176 and surrounding lines
sed -n '140,185p' model/channel_cache.go

Length of output: 1281


🏁 Script executed:

#!/bin/bash
# Count total lines in file to ensure line numbers are valid
wc -l model/channel_cache.go

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Now get exact range with context
sed -n '136,190p' model/channel_cache.go

Length of output: 1474


🏁 Script executed:

#!/bin/bash
# Show from function definition to beyond the target lines
sed -n '96,180p' model/channel_cache.go

Length of output: 2539


🏁 Script executed:

#!/bin/bash
# View complete GetRandomSatisfiedChannel function
sed -n '/^func GetRandomSatisfiedChannel/,/^func [a-zA-Z]/p' model/channel_cache.go | head -150

Length of output: 2591


🏁 Script executed:

#!/bin/bash
# Simple dump of critical section
sed -n '95,200p' model/channel_cache.go

Length of output: 2973


🏁 Script executed:

#!/bin/bash
# Most basic - just show lines 145-176
sed -n '145,176p' model/channel_cache.go

Length of output: 923


Fix possible panic (rand.Intn(0)) and align weighting with DB path

If all target channel weights are zero, totalWeight becomes 0, causing rand.Intn(0) to panic. The database fallback path uses a +10 baseline per channel; mirror it in cache selection to maintain consistent behavior.

	// get the priority for the given retry number
	var shouldSmooth = false
	var sumWeight = 0
	var targetChannels []*Channel
	for _, channelId := range channels {
		if channel, ok := channelsIDM[channelId]; ok {
			if channel.GetPriority() == targetPriority {
				sumWeight += channel.GetWeight()
				targetChannels = append(targetChannels, channel)
			}
		} else {
			return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
		}
	}
	if sumWeight/len(targetChannels) < 10 {
		shouldSmooth = true
	}

	// 平滑系数
	smoothingFactor := 1
	if shouldSmooth {
		smoothingFactor = 100
	}
-	// Calculate the total weight of all channels up to endIdx
-	totalWeight := sumWeight * smoothingFactor
-	// Generate a random value in the range [0, totalWeight)
-	randomWeight := rand.Intn(totalWeight)
+	// Add baseline to match DB selection logic (+10 per channel)
+	const base = 10
+	totalWeight := (sumWeight + base*len(targetChannels)) * smoothingFactor
+	// Guard: if somehow still zero (shouldn't), fallback to uniform
+	if totalWeight <= 0 {
+		return targetChannels[rand.Intn(len(targetChannels))], nil
+	}
+	randomWeight := rand.Intn(totalWeight)
 	// Find a channel based on its weight
 	for _, channel := range targetChannels {
-		randomWeight -= channel.GetWeight() * smoothingFactor
+		randomWeight -= (channel.GetWeight() + base) * smoothingFactor
 		if randomWeight < 0 {
 			return channel, nil
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var shouldSmooth = false
var sumWeight = 0
var targetChannels []*Channel
for _, channelId := range channels {
if channel, ok := channelsIDM[channelId]; ok {
if channel.GetPriority() == targetPriority {
sumWeight += channel.GetWeight()
targetChannels = append(targetChannels, channel)
}
} else {
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
}
}
if sumWeight/len(targetChannels) < 10 {
shouldSmooth = true
}
// 平滑系数
smoothingFactor := 10
// Calculate the total weight of all channels up to endIdx
totalWeight := 0
for _, channel := range targetChannels {
totalWeight += channel.GetWeight() + smoothingFactor
smoothingFactor := 1
if shouldSmooth {
smoothingFactor = 100
}
// Calculate the total weight of all channels up to endIdx
totalWeight := sumWeight * smoothingFactor
// Generate a random value in the range [0, totalWeight)
randomWeight := rand.Intn(totalWeight)
// Find a channel based on its weight
for _, channel := range targetChannels {
randomWeight -= channel.GetWeight() + smoothingFactor
randomWeight -= channel.GetWeight() * smoothingFactor
if randomWeight < 0 {
return channel, nil
var shouldSmooth = false
var sumWeight = 0
var targetChannels []*Channel
for _, channelId := range channels {
if channel, ok := channelsIDM[channelId]; ok {
if channel.GetPriority() == targetPriority {
sumWeight += channel.GetWeight()
targetChannels = append(targetChannels, channel)
}
} else {
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
}
}
if sumWeight/len(targetChannels) < 10 {
shouldSmooth = true
}
// 平滑系数
smoothingFactor := 1
if shouldSmooth {
smoothingFactor = 100
}
// Add baseline to match DB selection logic (+10 per channel)
const base = 10
totalWeight := (sumWeight + base*len(targetChannels)) * smoothingFactor
// Guard: if somehow still zero (shouldn't), fallback to uniform
if totalWeight <= 0 {
return targetChannels[rand.Intn(len(targetChannels))], nil
}
randomWeight := rand.Intn(totalWeight)
// Find a channel based on its weight
for _, channel := range targetChannels {
randomWeight -= (channel.GetWeight() + base) * smoothingFactor
if randomWeight < 0 {
return channel, nil
🤖 Prompt for AI Agents
In model/channel_cache.go around lines 145-176, totalWeight can be zero (causing
rand.Intn(0) panic) and the cache selection uses raw weights while the DB
fallback adds a +10 baseline per channel; fix by computing sumWeight using
(channel.GetWeight() + 10) for each target channel (and apply the same
smoothingFactor to those adjusted weights), ensure totalWeight is > 0 before
calling rand.Intn (return a clear error or fall back to a deterministic choice
if it somehow remains 0), and use the same adjusted weights when decrementing
randomWeight during selection so cache and DB paths behave consistently.

}
Expand Down
5 changes: 2 additions & 3 deletions relay/helper/price.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/logger"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
Expand All @@ -22,9 +23,7 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.
// check auto group
autoGroup, exists := ctx.Get("auto_group")
if exists {
if common.DebugEnabled {
println(fmt.Sprintf("final group: %s", autoGroup))
}
logger.LogDebug(ctx, fmt.Sprintf("final group: %s", autoGroup))
relayInfo.UsingGroup = autoGroup.(string)
}

Expand Down
42 changes: 42 additions & 0 deletions service/channel_select.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package service

import (
"errors"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/gin-gonic/gin"
)

func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) {
var channel *model.Channel
var err error
selectGroup := group
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
if group == "auto" {
if len(setting.GetAutoGroups()) == 0 {
return nil, selectGroup, errors.New("auto groups is not enabled")
}
for _, autoGroup := range GetUserAutoGroup(userGroup) {
logger.LogDebug(c, "Auto selecting group:", autoGroup)
channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
if channel == nil {
continue
} else {
c.Set("auto_group", autoGroup)
selectGroup = autoGroup
logger.LogDebug(c, "Auto selected group:", autoGroup)
break
}
}
} else {
channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry)
if err != nil {
return nil, group, err
}
}
return channel, selectGroup, nil
}
Comment on lines +14 to +42

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 | 🔴 Critical

Fix logger ctx type, format usage, and return errors when no channel found.

  • logger.LogDebug expects context.Context, not *gin.Context. Use c.Request.Context().
  • Provide format verbs; args are currently ignored.
  • Return a non-nil error when no channel is found (both auto and direct paths).
  • Optionally log per-iteration errors.

Apply:

@@
-import (
-	"errors"
+import (
+	"errors"
+	"fmt"
@@
 func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) {
@@
 	if group == "auto" {
 		if len(setting.GetAutoGroups()) == 0 {
 			return nil, selectGroup, errors.New("auto groups is not enabled")
 		}
-		for _, autoGroup := range GetUserAutoGroup(userGroup) {
-			logger.LogDebug(c, "Auto selecting group:", autoGroup)
-			channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
+		for _, autoGroup := range GetUserAutoGroup(userGroup) {
+			logger.LogDebug(c.Request.Context(), "Auto selecting group: %s", autoGroup)
+			channel, err = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
+			if err != nil {
+				logger.LogDebug(c.Request.Context(), "Auto group %s selection error: %v", autoGroup, err)
+			}
 			if channel == nil {
 				continue
 			} else {
 				c.Set("auto_group", autoGroup)
 				selectGroup = autoGroup
-				logger.LogDebug(c, "Auto selected group:", autoGroup)
+				logger.LogDebug(c.Request.Context(), "Auto selected group: %s", autoGroup)
 				break
 			}
 		}
+		if channel == nil {
+			return nil, selectGroup, fmt.Errorf("no available channel found in auto groups for model %s", modelName)
+		}
 	} else {
 		channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry)
 		if err != nil {
 			return nil, group, err
 		}
+		if channel == nil {
+			return nil, group, fmt.Errorf("no available channel found in group %s for model %s", group, modelName)
+		}
 	}
 	return channel, selectGroup, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) {
var channel *model.Channel
var err error
selectGroup := group
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
if group == "auto" {
if len(setting.GetAutoGroups()) == 0 {
return nil, selectGroup, errors.New("auto groups is not enabled")
}
for _, autoGroup := range GetUserAutoGroup(userGroup) {
logger.LogDebug(c, "Auto selecting group:", autoGroup)
channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
if channel == nil {
continue
} else {
c.Set("auto_group", autoGroup)
selectGroup = autoGroup
logger.LogDebug(c, "Auto selected group:", autoGroup)
break
}
}
} else {
channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry)
if err != nil {
return nil, group, err
}
}
return channel, selectGroup, nil
}
func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) {
var channel *model.Channel
var err error
selectGroup := group
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
if group == "auto" {
if len(setting.GetAutoGroups()) == 0 {
return nil, selectGroup, errors.New("auto groups is not enabled")
}
for _, autoGroup := range GetUserAutoGroup(userGroup) {
logger.LogDebug(c.Request.Context(), "Auto selecting group: %s", autoGroup)
channel, err = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
if err != nil {
logger.LogDebug(c.Request.Context(), "Auto group %s selection error: %v", autoGroup, err)
}
if channel == nil {
continue
} else {
c.Set("auto_group", autoGroup)
selectGroup = autoGroup
logger.LogDebug(c.Request.Context(), "Auto selected group: %s", autoGroup)
break
}
}
if channel == nil {
return nil, selectGroup, fmt.Errorf("no available channel found in auto groups for model %s", modelName)
}
} else {
channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry)
if err != nil {
return nil, group, err
}
if channel == nil {
return nil, group, fmt.Errorf("no available channel found in group %s for model %s", group, modelName)
}
}
return channel, selectGroup, nil
}

Loading