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.
150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package model
|
|
|
|
import (
|
|
"slices"
|
|
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/dto"
|
|
)
|
|
|
|
var filterEvalOrder = []dto.ChannelFilterKind{
|
|
dto.FilterAttemptedChannel,
|
|
dto.FilterRequestPath,
|
|
dto.FilterTaskPluginIdentity,
|
|
dto.FilterResponsesWebSocket,
|
|
}
|
|
|
|
// ChannelSatisfiesFilters reports whether ch passes every filter.
|
|
// On false, it returns the kind of the first violated filter (request_path
|
|
// then task_plugin_identity) for error attribution.
|
|
func ChannelSatisfiesFilters(ch *Channel, modelName string, filters []dto.ChannelFilter) (bool, dto.ChannelFilterKind) {
|
|
if ch == nil {
|
|
return false, ""
|
|
}
|
|
for _, kind := range filterEvalOrder {
|
|
for _, filter := range filters {
|
|
if filter.Kind != kind {
|
|
continue
|
|
}
|
|
if !channelMatchesFilter(ch, modelName, filter) {
|
|
return false, kind
|
|
}
|
|
}
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
// filterCandidateIDs applies filters to a cached candidate id list.
|
|
// Caller must hold channelSyncLock (read lock). The input slice is never mutated.
|
|
// A missing id in channelsIDM is kept for request_path (downstream consistency
|
|
// error) and dropped for task_plugin_identity, matching the previous filters.
|
|
func filterCandidateIDs(ids []int, modelName string, filters []dto.ChannelFilter) (kept []int, emptiedBy dto.ChannelFilterKind) {
|
|
return filterCandidateIDsObserved(ids, modelName, filters, nil, "")
|
|
}
|
|
|
|
func filterCandidateIDsObserved(ids []int, modelName string, filters []dto.ChannelFilter, trace *SelectionTrace, group string) (kept []int, emptiedBy dto.ChannelFilterKind) {
|
|
if trace != nil {
|
|
for _, id := range ids {
|
|
ch, exists := channelsIDM[id]
|
|
reason := "eligible"
|
|
for _, kind := range filterEvalOrder {
|
|
fs := filtersByKind(filters, kind)
|
|
if len(fs) > 0 && !candidatePassesKindFilters(ch, exists, modelName, kind, fs) {
|
|
reason = "excluded_" + string(kind)
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
reason = "missing_channel"
|
|
}
|
|
trace.Add(ch, id, group, modelName, reason)
|
|
}
|
|
}
|
|
if len(ids) == 0 {
|
|
return ids, ""
|
|
}
|
|
kept = ids
|
|
for _, kind := range filterEvalOrder {
|
|
kindFilters := filtersByKind(filters, kind)
|
|
if len(kindFilters) == 0 {
|
|
continue
|
|
}
|
|
next := make([]int, 0, len(kept))
|
|
for _, id := range kept {
|
|
channel, exists := channelsIDM[id]
|
|
if candidatePassesKindFilters(channel, exists, modelName, kind, kindFilters) {
|
|
next = append(next, id)
|
|
}
|
|
}
|
|
if len(kept) > 0 && len(next) == 0 {
|
|
return next, kind
|
|
}
|
|
kept = next
|
|
}
|
|
return kept, ""
|
|
}
|
|
|
|
func filtersByKind(filters []dto.ChannelFilter, kind dto.ChannelFilterKind) []dto.ChannelFilter {
|
|
var matched []dto.ChannelFilter
|
|
for _, filter := range filters {
|
|
if filter.Kind == kind {
|
|
matched = append(matched, filter)
|
|
}
|
|
}
|
|
return matched
|
|
}
|
|
|
|
func candidatePassesKindFilters(ch *Channel, exists bool, modelName string, kind dto.ChannelFilterKind, filters []dto.ChannelFilter) bool {
|
|
if kind == dto.FilterRequestPath && !exists {
|
|
return true
|
|
}
|
|
if !exists || ch == nil {
|
|
return false
|
|
}
|
|
for _, filter := range filters {
|
|
if !channelMatchesFilter(ch, modelName, filter) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func channelMatchesFilter(ch *Channel, modelName string, filter dto.ChannelFilter) bool {
|
|
switch filter.Kind {
|
|
case dto.FilterAttemptedChannel:
|
|
return !slices.Contains(filter.ExcludedChannelIDs, ch.Id)
|
|
case dto.FilterRequestPath:
|
|
if filter.RequestPath == "" {
|
|
return true
|
|
}
|
|
if !constant.IsAdvancedCustomChannel(ch.Type) {
|
|
return true
|
|
}
|
|
config := ch.GetOtherSettings().AdvancedCustom
|
|
return config != nil && config.SupportsPathForModel(filter.RequestPath, modelName)
|
|
case dto.FilterTaskPluginIdentity:
|
|
if ch.Type == constant.ChannelTypeTaskPlugin {
|
|
key := ch.GetSetting().TaskPluginKey
|
|
return filter.TaskPluginKey != "" && (key == filter.TaskPluginKey || slices.Contains(filter.TaskPluginKeys, key))
|
|
}
|
|
return filter.TaskPluginKey == "" || slices.Contains(filter.TaskPluginChannelTypes, ch.Type)
|
|
case dto.FilterResponsesWebSocket:
|
|
if !ch.GetSetting().ResponsesWebSocketEnabled {
|
|
return false
|
|
}
|
|
switch ch.Type {
|
|
case constant.ChannelTypeOpenAI, constant.ChannelTypeCodex, constant.ChannelTypeSub2API, constant.ChannelTypeNewAPI:
|
|
return true
|
|
case constant.ChannelTypeAdvancedCustom:
|
|
// The session forwards native Responses events without protocol
|
|
// conversion, so only a converter-free /v1/responses route qualifies.
|
|
route, ok := ch.GetOtherSettings().AdvancedCustom.MatchPathForModel("/v1/responses", modelName)
|
|
return ok && route.IsNative()
|
|
default:
|
|
return false
|
|
}
|
|
default:
|
|
return true
|
|
}
|
|
}
|