284 lines
10 KiB
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
|
|
}
|