diff --git a/model/ability.go b/model/ability.go index d950a6adbfc4..2b61957166b3 100644 --- a/model/ability.go +++ b/model/ability.go @@ -60,7 +60,9 @@ func GetAllEnableAbilities() []Ability { return abilities } -func getPriority(group string, model string, retry int) (int, error) { +// getPriority reports the priority to use for this retry step, and whether the +// group still serves the model. +func getPriority(group string, model string, retry int) (int, bool, error) { var priorities []int err := DB.Model(&Ability{}). @@ -71,12 +73,13 @@ func getPriority(group string, model string, retry int) (int, error) { if err != nil { // 处理错误 - return 0, err + return 0, false, err } if len(priorities) == 0 { - // 如果没有查询到优先级,则返回错误 - return 0, errors.New("数据库一致性被破坏") + // 分组内已无该模型的启用渠道,属于正常的无可用渠道。 + // 重试期间可达:自动禁用会把刚失败的渠道置为禁用。 + return 0, false, nil } // 确定要使用的优先级 @@ -87,19 +90,21 @@ func getPriority(group string, model string, retry int) (int, error) { } else { priorityToUse = priorities[retry] } - return priorityToUse, nil + return priorityToUse, true, nil } func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) { maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(commonGroupCol+" = ? and model = ? and enabled = ?", group, model, true) channelQuery := DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = (?)", group, model, true, maxPrioritySubQuery) if retry != 0 { - priority, err := getPriority(group, model, retry) + priority, found, err := getPriority(group, model, retry) if err != nil { return nil, err - } else { - channelQuery = DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = ?", group, model, true, priority) } + if !found { + return nil, nil + } + channelQuery = DB.Where(commonGroupCol+" = ? and model = ? and enabled = ? and priority = ?", group, model, true, priority) } return channelQuery, nil @@ -113,6 +118,9 @@ func GetChannel(group string, model string, retry int, requestPath string) (*Cha if err != nil { return nil, err } + if channelQuery == nil { + return nil, nil + } if common.UsingMainDatabase(common.DatabaseTypeSQLite) || common.UsingMainDatabase(common.DatabaseTypePostgreSQL) { err = channelQuery.Order("weight DESC").Find(&abilities).Error } else { diff --git a/model/ability_no_channel_test.go b/model/ability_no_channel_test.go new file mode 100644 index 000000000000..7c7c96962b01 --- /dev/null +++ b/model/ability_no_channel_test.go @@ -0,0 +1,57 @@ +package model + +import ( + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGetChannelReportsNoChannelAfterLastAbilityDisabled reproduces what a client +// sees when auto-ban disables the last channel serving a model in its group while +// a request is still retrying: the retry re-queries the group, finds nothing, and +// must report "no available channel" rather than a database consistency failure. +func TestGetChannelReportsNoChannelAfterLastAbilityDisabled(t *testing.T) { + prevMemoryCache := common.MemoryCacheEnabled + common.MemoryCacheEnabled = false + t.Cleanup(func() { common.MemoryCacheEnabled = prevMemoryCache }) + + require.NoError(t, DB.AutoMigrate(&Channel{}, &Ability{})) + require.NoError(t, DB.Exec("DELETE FROM abilities").Error) + require.NoError(t, DB.Exec("DELETE FROM channels").Error) + t.Cleanup(func() { + require.NoError(t, DB.Exec("DELETE FROM abilities").Error) + require.NoError(t, DB.Exec("DELETE FROM channels").Error) + }) + + priority := int64(100) + channel := &Channel{ + Status: common.ChannelStatusEnabled, + Name: "only-channel", + Group: "chat", + Models: "gpt-test", + Priority: &priority, + } + require.NoError(t, DB.Create(channel).Error) + require.NoError(t, DB.Create(&Ability{ + Group: "chat", + Model: "gpt-test", + ChannelId: channel.Id, + Enabled: true, + Priority: &priority, + }).Error) + + selected, err := GetChannel("chat", "gpt-test", 0, "") + require.NoError(t, err) + require.NotNil(t, selected, "the first attempt must find the only channel") + + // The channel fails and auto-ban disables it, exactly as UpdateAbilityStatus + // does when a relay error trips the disable threshold. + require.NoError(t, UpdateAbilityStatus(channel.Id, false)) + + // The in-flight request now retries against a group that has become empty. + selected, err = GetChannel("chat", "gpt-test", 1, "") + assert.NoError(t, err, "an emptied group is an ordinary no-channel result, not a database consistency failure") + assert.Nil(t, selected) +}