Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
584 lines
22 KiB
Go
584 lines
22 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/dto"
|
|
"github.com/QuantumNous/new-api/i18n"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/pkg/jsplugin"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func GetChannelConstraints(c *gin.Context) *dto.ChannelConstraints {
|
|
if c == nil {
|
|
return &dto.ChannelConstraints{}
|
|
}
|
|
if existing, ok := common.GetContextKeyType[*dto.ChannelConstraints](c, constant.ContextKeyChannelConstraints); ok && existing != nil {
|
|
return existing
|
|
}
|
|
constraints := &dto.ChannelConstraints{}
|
|
common.SetContextKey(c, constant.ContextKeyChannelConstraints, constraints)
|
|
return constraints
|
|
}
|
|
|
|
func AppendTaskPluginIdentityFilter(c *gin.Context, pluginKey string) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
channelTypes, pluginKeys := pinnedTaskPluginIdentities(c, pluginKey)
|
|
GetChannelConstraints(c).AddFilter(dto.ChannelFilter{
|
|
Kind: dto.FilterTaskPluginIdentity,
|
|
TaskPluginKey: pluginKey,
|
|
TaskPluginChannelTypes: channelTypes,
|
|
TaskPluginKeys: pluginKeys,
|
|
})
|
|
}
|
|
|
|
type RetryParam struct {
|
|
Ctx *gin.Context
|
|
TokenGroup string
|
|
ModelName string
|
|
RequestPath string
|
|
Retry *int
|
|
resetNextTry bool
|
|
}
|
|
|
|
func (p *RetryParam) GetRetry() int {
|
|
if p.Retry == nil {
|
|
return 0
|
|
}
|
|
return *p.Retry
|
|
}
|
|
|
|
func (p *RetryParam) SetRetry(retry int) {
|
|
p.Retry = &retry
|
|
}
|
|
|
|
func (p *RetryParam) IncreaseRetry() {
|
|
if p.resetNextTry {
|
|
p.resetNextTry = false
|
|
return
|
|
}
|
|
if p.Retry == nil {
|
|
p.Retry = new(int)
|
|
}
|
|
*p.Retry++
|
|
}
|
|
|
|
func (p *RetryParam) ResetRetryNextTry() {
|
|
p.resetNextTry = true
|
|
}
|
|
|
|
// groupPoolTier is a set of groups sharing one priority. Tiers are tried in
|
|
// ascending priority order, and the groups inside a tier are load-balanced.
|
|
type groupPoolTier struct {
|
|
priority int
|
|
groups []string
|
|
}
|
|
|
|
var groupPoolRoundRobinCounters sync.Map
|
|
|
|
// buildGroupPoolTiers resolves the token's routing pool into ordered tiers.
|
|
// Entries the user group cannot select are dropped, and the token's own group is
|
|
// placed ahead of the configured pool when it is missing from it.
|
|
func buildGroupPoolTiers(c *gin.Context, primary, userGroup string) []groupPoolTier {
|
|
value, exists := common.GetContextKey(c, constant.ContextKeyTokenGroupPool)
|
|
if !exists {
|
|
return nil
|
|
}
|
|
entries, ok := value.([]model.TokenGroupEntry)
|
|
if !ok || len(entries) == 0 {
|
|
return nil
|
|
}
|
|
maxCount := setting.GetMaxTokenAutoGroups()
|
|
ordered := make([]model.TokenGroupEntry, 0, len(entries)+1)
|
|
seen := make(map[string]struct{}, len(entries)+1)
|
|
hasPrimary := false
|
|
for _, entry := range entries {
|
|
if len(ordered) >= maxCount {
|
|
break
|
|
}
|
|
if !IsUserSelectableGroup(userGroup, entry.Group) {
|
|
continue
|
|
}
|
|
if _, duplicate := seen[entry.Group]; duplicate {
|
|
continue
|
|
}
|
|
seen[entry.Group] = struct{}{}
|
|
if entry.Group == primary {
|
|
hasPrimary = true
|
|
}
|
|
ordered = append(ordered, entry)
|
|
}
|
|
if len(ordered) == 0 {
|
|
return nil
|
|
}
|
|
if !hasPrimary && primary != "" && primary != "auto" {
|
|
minPriority := ordered[0].Priority
|
|
for _, entry := range ordered {
|
|
if entry.Priority < minPriority {
|
|
minPriority = entry.Priority
|
|
}
|
|
}
|
|
ordered = append([]model.TokenGroupEntry{{Group: primary, Priority: minPriority - 1}}, ordered...)
|
|
}
|
|
sort.SliceStable(ordered, func(i, j int) bool { return ordered[i].Priority < ordered[j].Priority })
|
|
tiers := make([]groupPoolTier, 0, len(ordered))
|
|
for _, entry := range ordered {
|
|
if len(tiers) > 0 && tiers[len(tiers)-1].priority == entry.Priority {
|
|
tiers[len(tiers)-1].groups = append(tiers[len(tiers)-1].groups, entry.Group)
|
|
continue
|
|
}
|
|
tiers = append(tiers, groupPoolTier{priority: entry.Priority, groups: []string{entry.Group}})
|
|
}
|
|
return tiers
|
|
}
|
|
|
|
func failedRequestGroups(c *gin.Context) map[string]bool {
|
|
failed := make(map[string]bool)
|
|
groups, ok := common.GetContextKeyType[[]string](c, constant.ContextKeyTokenFailedGroups)
|
|
if !ok {
|
|
return failed
|
|
}
|
|
for _, group := range groups {
|
|
failed[group] = true
|
|
}
|
|
return failed
|
|
}
|
|
|
|
// MarkRequestGroupFailed records that a group already failed during the current
|
|
// request so the next selection switches away from it instead of retrying it.
|
|
func MarkRequestGroupFailed(c *gin.Context, group string) {
|
|
if c == nil || group == "" {
|
|
return
|
|
}
|
|
groups, _ := common.GetContextKeyType[[]string](c, constant.ContextKeyTokenFailedGroups)
|
|
for _, existing := range groups {
|
|
if existing == group {
|
|
return
|
|
}
|
|
}
|
|
common.SetContextKey(c, constant.ContextKeyTokenFailedGroups, append(groups, group))
|
|
}
|
|
|
|
func nextTierIndex(key string, size int) int {
|
|
if size <= 1 {
|
|
return 0
|
|
}
|
|
value, _ := groupPoolRoundRobinCounters.LoadOrStore(key, new(uint64))
|
|
counter, ok := value.(*uint64)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return int(atomic.AddUint64(counter, 1)-1) % size
|
|
}
|
|
|
|
func selectFromGroupPool(param *RetryParam, tiers []groupPoolTier, filters []dto.ChannelFilter) (*model.Channel, string) {
|
|
failed := failedRequestGroups(param.Ctx)
|
|
// Each retry advances one priority tier, so a retryable failure reaches the
|
|
// next available group instead of repeating the group that just failed.
|
|
start := param.GetRetry()
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
if start > len(tiers)-1 {
|
|
start = len(tiers) - 1
|
|
}
|
|
for _, tier := range tiers[start:] {
|
|
eligible := make([]string, 0, len(tier.groups))
|
|
for _, group := range tier.groups {
|
|
if failed[group] || !IsGroupAvailable(group) {
|
|
continue
|
|
}
|
|
eligible = append(eligible, group)
|
|
}
|
|
if len(eligible) == 0 {
|
|
continue
|
|
}
|
|
start := nextTierIndex(tier.groups[0], len(eligible))
|
|
for offset := 0; offset < len(eligible); offset++ {
|
|
group := eligible[(start+offset)%len(eligible)]
|
|
if channel, _ := selectWithTrace(param, group, 0); channel != nil {
|
|
logger.LogInfo(param.Ctx, fmt.Sprintf("group pool selected: group=%s channel=%d", group, channel.Id))
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroup, group)
|
|
return channel, group
|
|
}
|
|
}
|
|
}
|
|
// Availability is a heuristic, so when every tier looks degraded fall back to
|
|
// the highest-priority group this request has not tried yet rather than
|
|
// failing without an attempt.
|
|
for _, tier := range tiers {
|
|
for _, group := range tier.groups {
|
|
if failed[group] {
|
|
continue
|
|
}
|
|
if channel, _ := selectWithTrace(param, group, 0); channel != nil {
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroup, group)
|
|
return channel, group
|
|
}
|
|
}
|
|
}
|
|
return nil, param.TokenGroup
|
|
}
|
|
|
|
// CacheGetRandomSatisfiedChannel tries to get a random channel that satisfies the requirements.
|
|
// 尝试获取一个满足要求的随机渠道。
|
|
//
|
|
// For ordered Auto groups or a fixed token group with a backup group:
|
|
// 对于有序 Auto 分组,或配置了备用分组的固定令牌分组:
|
|
//
|
|
// - Each group will exhaust all its priorities before moving to the next group.
|
|
// 每个分组会用完所有优先级后才会切换到下一个分组。
|
|
//
|
|
// - Uses ContextKeyAutoGroupIndex to track current group index.
|
|
// 使用 ContextKeyAutoGroupIndex 跟踪当前分组索引。
|
|
//
|
|
// - Uses ContextKeyAutoGroupRetryIndex to track the global Retry count when current group started.
|
|
// 使用 ContextKeyAutoGroupRetryIndex 跟踪当前分组开始时的全局重试次数。
|
|
//
|
|
// - priorityRetry = Retry - startRetryIndex, represents the priority level within current group.
|
|
// priorityRetry = Retry - startRetryIndex,表示当前分组内的优先级级别。
|
|
//
|
|
// - When GetRandomSatisfiedChannel returns nil (priorities exhausted), moves to next group.
|
|
// 当 GetRandomSatisfiedChannel 返回 nil(优先级用完)时,切换到下一个分组。
|
|
//
|
|
// Example flow (2 groups, each with 2 priorities, RetryTimes=3):
|
|
// 示例流程(2个分组,每个有2个优先级,RetryTimes=3):
|
|
//
|
|
// Retry=0: GroupA, priority0 (startRetryIndex=0, priorityRetry=0)
|
|
// 分组A, 优先级0
|
|
//
|
|
// Retry=1: GroupA, priority1 (startRetryIndex=0, priorityRetry=1)
|
|
// 分组A, 优先级1
|
|
//
|
|
// Retry=2: GroupA exhausted → GroupB, priority0 (startRetryIndex=2, priorityRetry=0)
|
|
// 分组A用完 → 分组B, 优先级0
|
|
//
|
|
// Retry=3: GroupB, priority1 (startRetryIndex=2, priorityRetry=1)
|
|
// 分组B, 优先级1
|
|
func CacheGetRandomSatisfiedChannel(param *RetryParam) (*model.Channel, string, error) {
|
|
if RequestRoutingPolicy(param.Ctx) != nil { return selectTokenRoutingPool(param) }
|
|
var channel *model.Channel
|
|
var err error
|
|
selectGroup := param.TokenGroup
|
|
userGroup := common.GetContextKeyString(param.Ctx, constant.ContextKeyUserGroup)
|
|
filters := GetChannelConstraints(param.Ctx).Filters
|
|
crossGroupRetry := common.GetContextKeyBool(param.Ctx, constant.ContextKeyTokenCrossGroupRetry)
|
|
orderedGroups := make([]string, 0)
|
|
tiers := make([]groupPoolTier, 0)
|
|
|
|
if param.TokenGroup == "auto" {
|
|
orderedGroups = GetRequestAutoGroups(param.Ctx, userGroup)
|
|
if len(orderedGroups) == 0 {
|
|
return nil, selectGroup, errors.New("auto groups is not enabled")
|
|
}
|
|
} else if crossGroupRetry {
|
|
if _, exists := common.GetContextKey(param.Ctx, constant.ContextKeyTokenGroupPool); exists {
|
|
tiers = buildGroupPoolTiers(param.Ctx, param.TokenGroup, userGroup)
|
|
}
|
|
if len(tiers) == 0 {
|
|
if value, exists := common.GetContextKey(param.Ctx, constant.ContextKeyTokenAutoGroups); exists {
|
|
if backupGroups, ok := value.([]string); ok {
|
|
orderedGroups = append(orderedGroups, param.TokenGroup)
|
|
for _, group := range FilterUserTokenAutoGroups(userGroup, backupGroups) {
|
|
if group != param.TokenGroup {
|
|
orderedGroups = append(orderedGroups, group)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(tiers) > 0 {
|
|
logger.LogInfo(param.Ctx, fmt.Sprintf("group pool selection: tokenGroup=%s tiers=%d retry=%d", param.TokenGroup, len(tiers), param.GetRetry()))
|
|
channel, selectGroup = selectFromGroupPool(param, tiers, filters)
|
|
return channel, selectGroup, nil
|
|
}
|
|
|
|
if len(orderedGroups) > 0 {
|
|
logger.LogInfo(param.Ctx, fmt.Sprintf("legacy group selection: groups=%v retry=%d", orderedGroups, param.GetRetry()))
|
|
// startGroupIndex: the group index to start searching from
|
|
// startGroupIndex: 开始搜索的分组索引
|
|
startGroupIndex := 0
|
|
|
|
if lastGroupIndex, exists := common.GetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex); exists {
|
|
if idx, ok := lastGroupIndex.(int); ok {
|
|
startGroupIndex = idx
|
|
}
|
|
}
|
|
|
|
for i := startGroupIndex; i < len(orderedGroups); i++ {
|
|
autoGroup := orderedGroups[i]
|
|
// Calculate priorityRetry for current group
|
|
// 计算当前分组的 priorityRetry
|
|
priorityRetry := param.GetRetry()
|
|
// If moved to a new group, reset priorityRetry and update startRetryIndex
|
|
// 如果切换到新分组,重置 priorityRetry 并更新 startRetryIndex
|
|
if i > startGroupIndex {
|
|
priorityRetry = 0
|
|
}
|
|
logger.LogDebug(param.Ctx, "Auto selecting group: %s, priorityRetry: %d", autoGroup, priorityRetry)
|
|
|
|
channel, _ = selectWithTrace(param, autoGroup, priorityRetry)
|
|
if channel == nil {
|
|
// Current group has no available channel for this model, try next group
|
|
// 当前分组没有该模型的可用渠道,尝试下一个分组
|
|
logger.LogDebug(param.Ctx, "No available channel in group %s for model %s at priorityRetry %d, trying next group", autoGroup, param.ModelName, priorityRetry)
|
|
// 重置状态以尝试下一个分组
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupRetryIndex, 0)
|
|
// Reset retry counter so outer loop can continue for next group
|
|
// 重置重试计数器,以便外层循环可以为下一个分组继续
|
|
param.SetRetry(0)
|
|
continue
|
|
}
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroup, autoGroup)
|
|
selectGroup = autoGroup
|
|
logger.LogDebug(param.Ctx, "Auto selected group: %s", autoGroup)
|
|
|
|
// Prepare state for next retry
|
|
// 为下一次重试准备状态
|
|
if crossGroupRetry && priorityRetry >= common.RetryTimes {
|
|
// Current group has exhausted all retries, prepare to switch to next group
|
|
// This request still uses current group, but next retry will use next group
|
|
// 当前分组已用完所有重试次数,准备切换到下一个分组
|
|
// 本次请求仍使用当前分组,但下次重试将使用下一个分组
|
|
logger.LogDebug(param.Ctx, "Current group %s retries exhausted (priorityRetry=%d >= RetryTimes=%d), preparing switch to next group for next retry", autoGroup, priorityRetry, common.RetryTimes)
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
|
|
// Reset retry counter so outer loop can continue for next group
|
|
// 重置重试计数器,以便外层循环可以为下一个分组继续
|
|
param.SetRetry(0)
|
|
param.ResetRetryNextTry()
|
|
} else {
|
|
// Stay in current group, save current state
|
|
// 保持在当前分组,保存当前状态
|
|
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i)
|
|
}
|
|
break
|
|
}
|
|
} else {
|
|
channel, err = selectWithTrace(param, param.TokenGroup, param.GetRetry())
|
|
if err != nil {
|
|
return nil, param.TokenGroup, err
|
|
}
|
|
}
|
|
return channel, selectGroup, nil
|
|
}
|
|
|
|
func pinnedTaskPluginIdentities(c *gin.Context, expected string) ([]int, []string) {
|
|
if c == nil || expected == "" {
|
|
return nil, nil
|
|
}
|
|
if value, exists := c.Get(jsplugin.ContextKeyPinnedEndpoint); exists {
|
|
pinned, ok := value.(jsplugin.PinnedEndpoint)
|
|
if ok && pinned.Generation != nil && len(pinned.Candidates) > 1 {
|
|
expectedFound := false
|
|
channelTypes := make([]int, 0, len(pinned.Candidates))
|
|
pluginKeys := make([]string, 0, len(pinned.Candidates))
|
|
seen := make(map[int]struct{}, len(pinned.Candidates))
|
|
for _, candidate := range pinned.Candidates {
|
|
if candidate.Plugin == nil {
|
|
continue
|
|
}
|
|
if candidate.Plugin.Meta.Key == expected {
|
|
expectedFound = true
|
|
}
|
|
pluginKeys = append(pluginKeys, candidate.Plugin.Meta.Key)
|
|
for _, channelType := range candidate.Plugin.Meta.ChannelTypes {
|
|
if channelType == 0 || channelType == constant.ChannelTypeTaskPlugin {
|
|
continue
|
|
}
|
|
if _, duplicate := seen[channelType]; duplicate {
|
|
continue
|
|
}
|
|
if plugin, indexed := pinned.Generation.GetByChannelType(channelType); indexed && plugin == candidate.Plugin {
|
|
seen[channelType] = struct{}{}
|
|
channelTypes = append(channelTypes, channelType)
|
|
}
|
|
}
|
|
}
|
|
if expectedFound {
|
|
return channelTypes, pluginKeys
|
|
}
|
|
}
|
|
}
|
|
value, exists := c.Get(jsplugin.ContextKeyPinnedPlugin)
|
|
pinned, ok := value.(jsplugin.PinnedPlugin)
|
|
if !exists || !ok || pinned.Generation == nil || pinned.Plugin == nil || pinned.Plugin.Meta.Key != expected {
|
|
return nil, nil
|
|
}
|
|
channelTypes := make([]int, 0, len(pinned.Plugin.Meta.ChannelTypes))
|
|
for _, channelType := range pinned.Plugin.Meta.ChannelTypes {
|
|
if channelType == 0 || channelType == constant.ChannelTypeTaskPlugin {
|
|
continue
|
|
}
|
|
channelTypes = append(channelTypes, channelType)
|
|
}
|
|
return channelTypes, []string{expected}
|
|
}
|
|
|
|
// ChannelSelectError explains why SelectChannelForRequest found no channel.
|
|
// Callers render it for their transport: the HTTP distributor localizes
|
|
// MessageID with its own helpers and the Responses WebSocket relay wraps it in
|
|
// a NewAPIError. Message is set instead of MessageID when the text is a fixed
|
|
// error code that clients match on.
|
|
type ChannelSelectError struct {
|
|
StatusCode int
|
|
Code types.ErrorCode
|
|
MessageID string
|
|
Params map[string]any
|
|
Message string
|
|
// FilterKind and Channel identify a candidate rejected by request filters.
|
|
FilterKind dto.ChannelFilterKind
|
|
Channel *model.Channel
|
|
// NoAvailableChannel marks the "no channel for this group and model"
|
|
// outcome so the distributor can name the claiming task plugin.
|
|
NoAvailableChannel bool
|
|
}
|
|
|
|
// SelectChannelForRequest resolves the channel for one attempt with the rules
|
|
// shared by the HTTP distributor and the Responses WebSocket relay: a pinned
|
|
// channel wins, then session affinity (first attempt only), then a random
|
|
// eligible channel; every candidate must satisfy the request's channel
|
|
// filters. The group the channel was chosen from is returned for auto-group
|
|
// callers. The caller still applies SetupContextForSelectedChannel.
|
|
func SelectChannelForRequest(c *gin.Context, modelName string, retry *RetryParam) (*model.Channel, string, *ChannelSelectError) {
|
|
c.Set("original_model", modelName)
|
|
constraints := GetChannelConstraints(c)
|
|
if pin, found, overridden := constraints.ResolvedPin(); found {
|
|
for _, lost := range overridden {
|
|
logger.LogWarn(c, fmt.Sprintf(
|
|
"channel pin overridden: winning_source=%s winning_channel_id=%d overridden_source=%s overridden_channel_id=%d",
|
|
pin.Source, pin.ChannelId, lost.Source, lost.ChannelId,
|
|
))
|
|
}
|
|
channel, err := model.CacheGetChannel(pin.ChannelId)
|
|
if err != nil {
|
|
recordDirectSelection(c, nil, pin.ChannelId, retry.TokenGroup, modelName, "pinned_channel_missing")
|
|
return nil, "", pinnedChannelUnavailable(pin, http.StatusBadRequest, i18n.MsgDistributorInvalidChannelId)
|
|
}
|
|
if channel.Status != common.ChannelStatusEnabled {
|
|
recordDirectSelection(c, channel, channel.Id, retry.TokenGroup, modelName, "channel_disabled")
|
|
return nil, "", pinnedChannelUnavailable(pin, http.StatusForbidden, i18n.MsgDistributorChannelDisabled)
|
|
}
|
|
if ok, kind := model.ChannelSatisfiesFilters(channel, modelName, constraints.Filters); !ok {
|
|
recordDirectSelection(c, channel, channel.Id, retry.TokenGroup, modelName, "excluded_"+string(kind))
|
|
return nil, "", &ChannelSelectError{
|
|
StatusCode: http.StatusBadRequest, Code: types.ErrorCode(kind), MessageID: i18n.MsgDistributorNoAvailableChannel,
|
|
Params: map[string]any{"Group": common.GetContextKeyString(c, constant.ContextKeyUsingGroup), "Model": modelName},
|
|
FilterKind: kind, Channel: channel,
|
|
}
|
|
}
|
|
recordDirectSelection(c, channel, channel.Id, retry.TokenGroup, modelName, "pinned_channel")
|
|
return channel, "", nil
|
|
}
|
|
|
|
usingGroup := retry.TokenGroup
|
|
if RequestRoutingPolicy(c) != nil {
|
|
if channel, group := PreferredPoolChannel(c, modelName); channel != nil && retry.GetRetry() == 0 { return channel,group,nil }
|
|
channel, group, err := selectTokenRoutingPool(retry)
|
|
if err != nil || channel == nil { return nil,group,&ChannelSelectError{StatusCode:http.StatusServiceUnavailable,Code:types.ErrorCodeModelNotFound,Message:"No eligible channel in the group pool",NoAvailableChannel:true} }
|
|
return channel,group,nil
|
|
}
|
|
var channel *model.Channel
|
|
var selectGroup string
|
|
if retry.GetRetry() == 0 {
|
|
if preferredChannelID, found := GetPreferredChannelByAffinity(c, modelName, usingGroup); found {
|
|
affinityUsable := false
|
|
preferred, err := model.CacheGetChannel(preferredChannelID)
|
|
affinitySatisfied := false
|
|
if err == nil && preferred != nil && preferred.Status == common.ChannelStatusEnabled {
|
|
affinitySatisfied, _ = model.ChannelSatisfiesFilters(preferred, modelName, constraints.Filters)
|
|
}
|
|
if affinitySatisfied {
|
|
if usingGroup == "auto" {
|
|
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
|
|
for _, g := range GetRequestAutoGroups(c, userGroup) {
|
|
if model.IsChannelEnabledForGroupModel(g, modelName, preferred.Id) {
|
|
selectGroup = g
|
|
common.SetContextKey(c, constant.ContextKeyAutoGroup, g)
|
|
channel = preferred
|
|
affinityUsable = true
|
|
MarkChannelAffinityUsed(c, g, preferred.Id)
|
|
break
|
|
}
|
|
}
|
|
} else if model.IsChannelEnabledForGroupModel(usingGroup, modelName, preferred.Id) {
|
|
channel = preferred
|
|
selectGroup = usingGroup
|
|
affinityUsable = true
|
|
MarkChannelAffinityUsed(c, usingGroup, preferred.Id)
|
|
}
|
|
}
|
|
reason := "affinity_unavailable"
|
|
if affinityUsable {
|
|
reason = "affinity_hit"
|
|
}
|
|
recordDirectSelection(c, preferred, preferredChannelID, usingGroup, modelName, reason)
|
|
if !affinityUsable && !ShouldKeepChannelAffinityOnChannelDisabled() {
|
|
ClearCurrentChannelAffinityCache(c)
|
|
}
|
|
if !affinityUsable && RequestPolicy(c).SessionMode == "strict" {
|
|
return nil, "", &ChannelSelectError{StatusCode: http.StatusServiceUnavailable, Message: "strict_session_binding_unavailable"}
|
|
}
|
|
}
|
|
}
|
|
|
|
if channel == nil {
|
|
var err error
|
|
channel, selectGroup, err = CacheGetRandomSatisfiedChannel(retry)
|
|
if err != nil {
|
|
showGroup := usingGroup
|
|
if usingGroup == "auto" {
|
|
showGroup = fmt.Sprintf("auto(%s)", selectGroup)
|
|
}
|
|
return nil, selectGroup, &ChannelSelectError{
|
|
StatusCode: http.StatusServiceUnavailable, Code: types.ErrorCodeModelNotFound, MessageID: i18n.MsgDistributorGetChannelFailed,
|
|
Params: map[string]any{"Group": showGroup, "Model": modelName, "Error": err.Error()},
|
|
}
|
|
}
|
|
if channel == nil {
|
|
return nil, selectGroup, &ChannelSelectError{
|
|
StatusCode: http.StatusServiceUnavailable, Code: types.ErrorCodeModelNotFound, MessageID: i18n.MsgDistributorNoAvailableChannel,
|
|
Params: map[string]any{"Group": usingGroup, "Model": modelName}, NoAvailableChannel: true,
|
|
}
|
|
}
|
|
}
|
|
if ok, kind := model.ChannelSatisfiesFilters(channel, modelName, constraints.Filters); !ok {
|
|
return nil, selectGroup, &ChannelSelectError{
|
|
StatusCode: http.StatusServiceUnavailable, Code: types.ErrorCodeModelNotFound, MessageID: i18n.MsgDistributorNoAvailableChannel,
|
|
Params: map[string]any{"Group": common.GetContextKeyString(c, constant.ContextKeyUsingGroup), "Model": modelName},
|
|
FilterKind: kind, Channel: channel, NoAvailableChannel: true,
|
|
}
|
|
}
|
|
return channel, selectGroup, nil
|
|
}
|
|
|
|
// Origin-task pins report a fixed code so task polling can tell a retired
|
|
// channel from a malformed request.
|
|
func pinnedChannelUnavailable(pin dto.ChannelPin, statusCode int, messageID string) *ChannelSelectError {
|
|
if pin.Source == dto.PinSourceOriginTask {
|
|
return &ChannelSelectError{StatusCode: http.StatusBadRequest, Code: "origin_task_channel_disabled", Message: "origin_task_channel_disabled"}
|
|
}
|
|
return &ChannelSelectError{StatusCode: statusCode, MessageID: messageID}
|
|
}
|
|
|
|
// AppendUsedChannel records an attempted channel in the request's channel
|
|
// trail, which the retry log and the consume log's admin_info both read.
|
|
func AppendUsedChannel(c *gin.Context, channelID int) {
|
|
c.Set("use_channel", append(c.GetStringSlice("use_channel"), fmt.Sprintf("%d", channelID)))
|
|
}
|