iop/apps/edge/internal/openai/stream_gate_policy.go

284 lines
10 KiB
Go

package openai
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
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
}
// 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)
if err != nil {
return nil, nil, err
}
timeout := time.Duration(fc.EffectiveTimeoutMS()) * time.Millisecond
reg, err := streamgate.NewFilterRegistration(
filter, 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(
filter.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
}