package openai import ( "crypto/sha256" "encoding/json" "fmt" "sort" "strings" "time" "unicode/utf8" edgeservice "iop/apps/edge/internal/service" "iop/packages/go/config" "iop/packages/go/streamgate" ) // chatRequestHasSchemeMetadata reports whether the request carries a non-empty // metadata.scheme output contract. Presence, not shape, is enough to decide // whether the schema_gate filter participates; JSON-schema validation itself is // the schema-contract Task's scope. func chatRequestHasSchemeMetadata(metadata json.RawMessage) bool { if len(metadata) == 0 { return false } var m map[string]json.RawMessage if err := json.Unmarshal(metadata, &m); err != nil { return false } raw, ok := m["scheme"] if !ok { return false } trimmed := strings.TrimSpace(string(raw)) return trimmed != "" && trimmed != "null" } // This file translates the request-stable stream_evidence_gate filter policy // (packages/go/config) into Core FilterRegistrations and FilterPolicyLayers, and // resolves required output-filter capabilities for provider admission. The Core // owns selector precedence, per-target Applies re-resolution, and eligibility; // this file only builds the generation-bound inputs and adapts the config // enums to the Core enums. // openAIOutputFilterContext carries the immutable request-start facts used by // selector resolution and endpoint-specific filter participation. type openAIOutputFilterContext struct { environment string endpoint string modelGroup string hasScheme bool requestRef string history openAIRepeatHistorySnapshot } const ( openAIRepeatHistoryAnchorThreshold = 2 openAIRepeatHistoryMaxFingerprints = 1024 openAIRepeatHistoryMaxActions = 256 ) type openAIRepeatChannel string const ( openAIRepeatChannelContent openAIRepeatChannel = "content" openAIRepeatChannelReasoning openAIRepeatChannel = "reasoning" ) type openAIRepeatHistoryAction struct { actionFingerprint streamgate.FixedFingerprint resultFingerprint streamgate.FixedFingerprint } type openAIRepeatHistoryCandidate struct { count int normalizedRunes int } // openAIRepeatHistorySnapshot is the immutable, raw-free semantic view passed // from an endpoint decoder to the request-local repeat filter. It intentionally // has no session, caller, TTL, raw text, tool arguments, or tool result fields. type openAIRepeatHistorySnapshot struct { assistantOccurrences map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int userOccurrences map[streamgate.FixedFingerprint]int assistantCandidates map[openAIRepeatChannel]map[streamgate.FixedFingerprint]openAIRepeatHistoryCandidate repeatedActions map[streamgate.FixedFingerprint]int hasCompletedProgress bool hasTerminalProgress bool } type openAIRepeatHistoryBuilder struct { assistantOccurrences map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int userOccurrences map[streamgate.FixedFingerprint]int normalizedTextRunes map[streamgate.FixedFingerprint]int completedActions []openAIRepeatHistoryAction hasTerminalProgress bool } func newOpenAIRepeatHistoryBuilder() *openAIRepeatHistoryBuilder { return &openAIRepeatHistoryBuilder{ assistantOccurrences: map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int{ openAIRepeatChannelContent: {}, openAIRepeatChannelReasoning: {}, }, userOccurrences: make(map[streamgate.FixedFingerprint]int), normalizedTextRunes: make(map[streamgate.FixedFingerprint]int), } } func normalizeOpenAIRepeatText(value string) string { return strings.Join(strings.Fields(value), " ") } func openAIRepeatTextFingerprint(value string) (streamgate.FixedFingerprint, bool) { normalized := normalizeOpenAIRepeatText(value) if normalized == "" { return streamgate.FixedFingerprint{}, false } return streamgate.FixedFingerprint(sha256.Sum256([]byte(normalized))), true } func (b *openAIRepeatHistoryBuilder) admitTextFingerprint(fingerprint streamgate.FixedFingerprint, normalizedRunes int) bool { if _, exists := b.normalizedTextRunes[fingerprint]; exists { return true } if len(b.normalizedTextRunes) >= openAIRepeatHistoryMaxFingerprints { return false } b.normalizedTextRunes[fingerprint] = normalizedRunes return true } func openAIRepeatActionFingerprint(name string, arguments json.RawMessage) (streamgate.FixedFingerprint, bool) { name = normalizeOpenAIRepeatText(name) if name == "" { return streamgate.FixedFingerprint{}, false } var canonical any if len(arguments) > 0 && json.Unmarshal(arguments, &canonical) == nil { if encoded, err := json.Marshal(canonical); err == nil { arguments = encoded } } normalizedArgs := normalizeOpenAIRepeatText(string(arguments)) return streamgate.FixedFingerprint(sha256.Sum256([]byte(name + "\x00" + normalizedArgs))), true } func (b *openAIRepeatHistoryBuilder) recordText(role string, channel openAIRepeatChannel, value string) { role = strings.ToLower(strings.TrimSpace(role)) if role != "assistant" && role != "user" { return } normalized := normalizeOpenAIRepeatText(value) if normalized == "" { return } fingerprint := streamgate.FixedFingerprint(sha256.Sum256([]byte(normalized))) if !b.admitTextFingerprint(fingerprint, utf8.RuneCountInString(normalized)) { return } switch role { case "assistant": if b.assistantOccurrences[channel] == nil { b.assistantOccurrences[channel] = make(map[streamgate.FixedFingerprint]int) } b.assistantOccurrences[channel][fingerprint]++ case "user": b.userOccurrences[fingerprint]++ } } func (b *openAIRepeatHistoryBuilder) recordCompletedAction(action, result streamgate.FixedFingerprint) { if action == (streamgate.FixedFingerprint{}) || result == (streamgate.FixedFingerprint{}) { return } if len(b.completedActions) >= openAIRepeatHistoryMaxActions { return } b.completedActions = append(b.completedActions, openAIRepeatHistoryAction{ actionFingerprint: action, resultFingerprint: result, }) } func (b *openAIRepeatHistoryBuilder) snapshot() openAIRepeatHistorySnapshot { snapshot := openAIRepeatHistorySnapshot{ assistantOccurrences: map[openAIRepeatChannel]map[streamgate.FixedFingerprint]int{ openAIRepeatChannelContent: {}, openAIRepeatChannelReasoning: {}, }, userOccurrences: map[streamgate.FixedFingerprint]int{}, assistantCandidates: map[openAIRepeatChannel]map[streamgate.FixedFingerprint]openAIRepeatHistoryCandidate{ openAIRepeatChannelContent: {}, openAIRepeatChannelReasoning: {}, }, repeatedActions: make(map[streamgate.FixedFingerprint]int), hasTerminalProgress: b.hasTerminalProgress, } for fingerprint, count := range b.userOccurrences { snapshot.userOccurrences[fingerprint] = count } totalAssistant := make(map[streamgate.FixedFingerprint]int) for channel, occurrences := range b.assistantOccurrences { for fingerprint, count := range occurrences { snapshot.assistantOccurrences[channel][fingerprint] = count totalAssistant[fingerprint] += count } } for channel, occurrences := range snapshot.assistantOccurrences { for fingerprint := range occurrences { if snapshot.userOccurrences[fingerprint] == 0 && totalAssistant[fingerprint] >= openAIRepeatHistoryAnchorThreshold { snapshot.assistantCandidates[channel][fingerprint] = openAIRepeatHistoryCandidate{ count: totalAssistant[fingerprint], normalizedRunes: b.normalizedTextRunes[fingerprint], } } } } if len(b.completedActions) >= 2 { previous := b.completedActions[len(b.completedActions)-2] current := b.completedActions[len(b.completedActions)-1] snapshot.hasCompletedProgress = previous.resultFingerprint != current.resultFingerprint if previous.actionFingerprint == current.actionFingerprint && previous.resultFingerprint == current.resultFingerprint { snapshot.repeatedActions[current.actionFingerprint] = 1 } } return snapshot } func (s openAIRepeatHistorySnapshot) assistantCandidate(channel openAIRepeatChannel, fingerprint streamgate.FixedFingerprint) (int, bool) { candidate, ok := s.assistantCandidates[channel][fingerprint] return candidate.count, ok } func (s openAIRepeatHistorySnapshot) assistantCandidateMetadata(channel openAIRepeatChannel, fingerprint streamgate.FixedFingerprint) (openAIRepeatHistoryCandidate, bool) { candidate, ok := s.assistantCandidates[channel][fingerprint] return candidate, ok } func (s openAIRepeatHistorySnapshot) repeatedAction(fingerprint streamgate.FixedFingerprint) (int, bool) { count, ok := s.repeatedActions[fingerprint] return count, ok } func (s openAIRepeatHistorySnapshot) completedProgress() bool { return s.hasCompletedProgress || s.hasTerminalProgress } // streamgateFilterEnforcement adapts a config enforcement string to the Core // enforcement enum, defaulting to blocking for empty/unknown input (validation // rejects unknown values before this point). func streamgateFilterEnforcement(s string) streamgate.FilterEnforcement { if s == config.StreamGateFilterEnforcementObserveOnly { return streamgate.FilterEnforcementObserveOnly } return streamgate.FilterEnforcementBlocking } // streamgateSelectorType adapts a config selector type to the Core selector // type. It returns false when the type is unknown. func streamgateSelectorType(s string) (streamgate.PolicySelectorType, bool) { switch s { case config.StreamGateFilterSelectorEnvironment: return streamgate.PolicySelectorEnvironment, true case config.StreamGateFilterSelectorModelGroup: return streamgate.PolicySelectorModelGroup, true case config.StreamGateFilterSelectorModel: return streamgate.PolicySelectorModel, true case config.StreamGateFilterSelectorProvider: return streamgate.PolicySelectorProvider, true default: return "", false } } // openAIOutputFilterRegistrations builds the Core registrations and policy // layers for the configured semantic output filters. A schema_gate filter is // only registered when the caller requested a schema contract; a request with // no scheme neither registers nor requires it. The returned slices are the // request-stable inputs to a generation-bound FilterRegistrySnapshot. func openAIOutputFilterRegistrations(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext) ([]streamgate.FilterRegistration, []streamgate.FilterPolicyLayer, error) { var ( regs []streamgate.FilterRegistration policies []streamgate.FilterPolicyLayer ) for _, fc := range gateCfg.Filters { if fc.Filter == config.StreamGateFilterSchemaGate && !fctx.hasScheme { continue } filter, err := newOpenAIOutputFilter( openAIOutputFilterKind(fc.Filter), fc.EffectiveHoldEvidenceRunes(), fc.Priority, fctx.requestRef, fctx.history, ) if err != nil { return nil, nil, err } filters := []*openAIOutputFilter{filter} if fc.Filter == config.StreamGateFilterRepeatGuard { actionFilter, actionErr := newOpenAIOutputFilter( openAIOutputFilterRepeatActionGuard, fc.EffectiveHoldEvidenceRunes(), fc.Priority, fctx.requestRef, fctx.history, ) if actionErr != nil { return nil, nil, actionErr } filters = append(filters, actionFilter) } timeout := time.Duration(fc.EffectiveTimeoutMS()) * time.Millisecond for _, registeredFilter := range filters { reg, err := streamgate.NewFilterRegistration( registeredFilter, fc.EffectiveCapability(), fc.EffectiveEnabled(), streamgateFilterEnforcement(fc.EffectiveEnforcement()), timeout, fc.Priority, ) if err != nil { return nil, nil, err } regs = append(regs, reg) for i, sel := range fc.Selectors { selType, ok := streamgateSelectorType(sel.Type) if !ok { return nil, nil, fmt.Errorf("openai output filter %q selector %d unknown type %q", fc.Filter, i, sel.Type) } enabled := fc.EffectiveEnabled() if sel.Enabled != nil { enabled = *sel.Enabled } enforcement := fc.EffectiveEnforcement() if sel.Enforcement != "" { enforcement = sel.Enforcement } layer, err := streamgate.NewFilterPolicyLayer( registeredFilter.ID(), selType, sel.Key, enabled, streamgateFilterEnforcement(enforcement), timeout, fc.Priority, ) if err != nil { return nil, nil, err } policies = append(policies, layer) } } } return regs, policies, nil } // openAIOutputFilterRequestContext builds the caller-neutral RequestFilterContext // used to resolve required capabilities and eligibility. It intentionally // carries no caller/agent product name: the same payload resolves identically // for raw HTTP, OpenAI SDK, or any other caller. func openAIOutputFilterRequestContext(fctx openAIOutputFilterContext) (streamgate.RequestFilterContext, error) { return streamgate.NewRequestFilterContext( streamGateConfigGeneration, "admission", fctx.environment, fctx.endpoint, openAIRebuildFamily, "", streamgate.CommitStateTransportUncommitted, false, false, "", ) } // blockingCapabilitiesForTarget resolves effective policy at the actual target // and excludes observe-only filters from admission. func blockingCapabilitiesForTarget(reqSnap streamgate.RequestFilterSnapshot, target streamgate.AttemptTarget) ([]string, error) { resolved, err := reqSnap.ResolveAttempt(target) if err != nil { return nil, err } set := make(map[string]struct{}, len(resolved)) for _, filter := range resolved { if filter.Enforcement() != streamgate.FilterEnforcementBlocking { continue } set[filter.Registration().RequiredCapabilityID()] = struct{}{} } required := make([]string, 0, len(set)) for capability := range set { required = append(required, capability) } sort.Strings(required) return required, nil } func candidateHasCapabilities(target streamgate.AttemptTarget, required []string) bool { for _, capability := range required { if !target.HasCapability(capability) { return false } } return true } // openAIStreamGateRequiredCapabilities returns the union of output-filter // capabilities that the given candidate target must advertise. Only // enabled+applicable filters at the target contribute. func openAIStreamGateRequiredCapabilities(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext, target streamgate.AttemptTarget) ([]string, error) { regs, policies, err := openAIOutputFilterRegistrations(gateCfg, fctx) if err != nil { return nil, err } snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regs, policies) if err != nil { return nil, err } reqCtx, err := openAIOutputFilterRequestContext(fctx) if err != nil { return nil, err } reqSnap, err := snap.BeginRequest(reqCtx) if err != nil { return nil, err } return blockingCapabilitiesForTarget(reqSnap, target) } // errStreamGateRequiredCapabilityUnsupported is the admission-time error a // handler maps to an OpenAI-compatible invalid_request_error (400) when a // required output-filter capability is unsupported by every candidate provider, // before any provider dispatch or recovery budget is consumed. var errStreamGateRequiredCapabilityUnsupported = fmt.Errorf("stream gate: required output filter capability unsupported by all candidates") // openAIStreamGateAdmitCandidates returns the subset of candidate targets whose // capability set covers every required output-filter capability resolved at each // candidate's own target context. It returns // errStreamGateRequiredCapabilityUnsupported when no candidate is eligible, so // the caller rejects the request with a pre-dispatch 400 instead of leaving a // required filter silently unenforced. func openAIStreamGateAdmitCandidates(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext, candidates []streamgate.AttemptTarget) ([]streamgate.AttemptTarget, error) { regs, policies, err := openAIOutputFilterRegistrations(gateCfg, fctx) if err != nil { return nil, err } // No configured output filters => no required capability => every candidate // is admissible; preserve the existing provider-pool admission unchanged. if len(regs) == 0 { return candidates, nil } snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regs, policies) if err != nil { return nil, err } reqCtx, err := openAIOutputFilterRequestContext(fctx) if err != nil { return nil, err } reqSnap, err := snap.BeginRequest(reqCtx) if err != nil { return nil, err } eligible := make([]streamgate.AttemptTarget, 0, len(candidates)) for _, candidate := range candidates { required, err := blockingCapabilitiesForTarget(reqSnap, candidate) if err != nil { return nil, err } if candidateHasCapabilities(candidate, required) { eligible = append(eligible, candidate) } } if len(eligible) == 0 { return nil, errStreamGateRequiredCapabilityUnsupported } return eligible, nil } // openAIStreamGateCandidatePredicate turns one immutable request policy into a // provider-pool admission predicate. Service invokes it before the first // dispatch and on every recovery/queue re-resolution, so provider-specific // selectors are evaluated against the actual selected target rather than a // caller-supplied model or agent identity. func openAIStreamGateCandidatePredicate(gateCfg config.StreamEvidenceGateConf, fctx openAIOutputFilterContext) (edgeservice.ProviderPoolCandidatePredicate, error) { regs, _, err := openAIOutputFilterRegistrations(gateCfg, fctx) if err != nil { return nil, err } if len(regs) == 0 { return nil, nil } return func(candidate edgeservice.ProviderPoolCandidate) bool { target, err := streamgate.NewAttemptTarget( fctx.modelGroup, candidate.ActualModel, candidate.ProviderID, candidate.ExecutionPath, candidate.LifecycleCapabilities, ) if err != nil { return false } eligible, err := openAIStreamGateAdmitCandidates(gateCfg, fctx, []streamgate.AttemptTarget{target}) return err == nil && len(eligible) == 1 }, nil }