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 filter.TaskPluginKey == "" { return ch.Type != constant.ChannelTypeTaskPlugin } if ch.Type == constant.ChannelTypeTaskPlugin || ch.Type == constant.ChannelTypeNewAPI { // A New API channel serves every plugin it is extended with; the // pinned plugin or any shared-model candidate may execute there. setting := ch.GetSetting() return setting.BindsTaskPlugin(filter.TaskPluginKey) || slices.ContainsFunc(filter.TaskPluginKeys, setting.BindsTaskPlugin) } return 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 } }