feat(advanced-custom): remove fallback option and enhance path matching for advanced custom routes

This commit is contained in:
CaIon
2026-06-18 20:29:24 +08:00
parent 3f2c0aeda7
commit 55b00fcf09
22 changed files with 378 additions and 329 deletions
+43 -4
View File
@@ -11,12 +11,16 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/setting/ratio_setting"
)
var group2model2channels map[string]map[string][]int // enabled channel
var channelsIDM map[int]*Channel // all channels include disabled
// channel2advancedCustomConfig caches parsed Advanced Custom (type 58) configs so
// path-aware selection avoids re-parsing JSON per request. Refreshed on full sync.
var channel2advancedCustomConfig map[int]*dto.AdvancedCustomConfig
var channelSyncLock sync.RWMutex
func InitChannelCache() {
@@ -24,10 +28,16 @@ func InitChannelCache() {
return
}
newChannelId2channel := make(map[int]*Channel)
newChannel2advancedCustomConfig := make(map[int]*dto.AdvancedCustomConfig)
var channels []*Channel
DB.Find(&channels)
for _, channel := range channels {
newChannelId2channel[channel.Id] = channel
if channel.Type == constant.ChannelTypeAdvancedCustom {
if config := channel.GetOtherSettings().AdvancedCustom; config != nil {
newChannel2advancedCustomConfig[channel.Id] = config
}
}
}
var abilities []*Ability
DB.Find(&abilities)
@@ -82,6 +92,7 @@ func InitChannelCache() {
}
}
channelsIDM = newChannelId2channel
channel2advancedCustomConfig = newChannel2advancedCustomConfig
channelSyncLock.Unlock()
common.SysLog("channels synced from database")
}
@@ -94,22 +105,22 @@ func SyncChannelCache(frequency int) {
}
}
func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
func GetRandomSatisfiedChannel(group string, model string, retry int, requestPath string) (*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, requestPath)
}
channelSyncLock.RLock()
defer channelSyncLock.RUnlock()
// First, try to find channels with the exact model name.
channels := group2model2channels[group][model]
channels := filterChannelsByRequestPath(group2model2channels[group][model], requestPath)
// If no channels found, try to find channels with the normalized model name.
if len(channels) == 0 {
normalizedModel := ratio_setting.FormatMatchingModelName(model)
channels = group2model2channels[group][normalizedModel]
channels = filterChannelsByRequestPath(group2model2channels[group][normalizedModel], requestPath)
}
if len(channels) == 0 {
@@ -191,6 +202,34 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
return nil, errors.New("channel not found")
}
// filterChannelsByRequestPath restricts candidates by request path. Only Advanced
// Custom (type 58) channels are path-checked: they are kept only when one of their
// configured routes matches requestPath. All other channel types always pass.
// When requestPath is empty (non-relay callers) filtering is skipped.
// Caller must hold channelSyncLock (read lock). The cached slice is never mutated.
func filterChannelsByRequestPath(channels []int, requestPath string) []int {
if requestPath == "" || len(channels) == 0 {
return channels
}
filtered := make([]int, 0, len(channels))
for _, channelId := range channels {
channel, ok := channelsIDM[channelId]
if !ok {
// keep it so the downstream consistency error is raised as before
filtered = append(filtered, channelId)
continue
}
if channel.Type != constant.ChannelTypeAdvancedCustom {
filtered = append(filtered, channelId)
continue
}
if config := channel2advancedCustomConfig[channelId]; config != nil && config.SupportsPath(requestPath) {
filtered = append(filtered, channelId)
}
}
return filtered
}
func CacheGetChannel(id int) (*Channel, error) {
if !common.MemoryCacheEnabled {
return GetChannelById(id, true)