iop/apps/edge/internal/openai/stream_gate_dispatcher.go
toki 7634ca8962 feat(stream-evidence-gate-core): complete stream evidence gate core implementation
- OpenAI request rebuilder with tool validation and provider tunnel
- Edge config runtime refresh for stream evidence gate
- Filter observation contract and runtime with sink/correlation
- Stream gate dispatcher, release sink, and vertical slice
- Recovery coordinator for evidence tail
- Parallel evaluation and commit boundary
- E2E test script for OpenAI vLLM
- Archive completed task groups to archive/2026/07
2026-07-28 04:08:12 +09:00

330 lines
10 KiB
Go

package openai
import (
"context"
"fmt"
"strings"
"sync"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/streamgate"
)
type openAIAdmissionKind string
const (
openAIAdmissionRun openAIAdmissionKind = "run"
openAIAdmissionTunnel openAIAdmissionKind = "tunnel"
openAIAdmissionPool openAIAdmissionKind = "pool"
)
// openAIAttemptAdmission is built fresh for every recovery dispatch. The auth
// callback deliberately lives here rather than on ingress/rebuilt snapshots.
type openAIAttemptAdmission struct {
kind openAIAdmissionKind
run edgeservice.SubmitRunRequest
tunnel edgeservice.SubmitProviderTunnelRequest
pool edgeservice.ProviderPoolDispatchRequest
authorize func(context.Context) (map[string]string, error)
}
func (a openAIAttemptAdmission) validate() error {
switch a.kind {
case openAIAdmissionRun:
if a.run.ModelGroupKey == "" && a.run.Target == "" {
return fmt.Errorf("OpenAI recovery run admission requires a model")
}
case openAIAdmissionTunnel:
if a.tunnel.Path == "" {
return fmt.Errorf("OpenAI recovery tunnel admission requires a path")
}
case openAIAdmissionPool:
if !a.pool.Run.ProviderPool || a.pool.Run.ModelGroupKey == "" {
return fmt.Errorf("OpenAI recovery pool admission is incomplete")
}
default:
return fmt.Errorf("unknown OpenAI recovery admission kind")
}
return nil
}
type openAIAttemptAdmissionBuilder func(
ctx context.Context,
request streamgate.RebuiltRequest,
body []byte,
) (openAIAttemptAdmission, error)
type openAIAttemptTransport struct {
path openAIAdmissionKind
run edgeservice.RunResult
tunnel edgeservice.ProviderTunnelResult
}
type openAIAttemptEventSourceFactory func(openAIAttemptTransport) (streamgate.NormalizedEventSource, error)
// openAIAttemptDispatcher adapts the three existing Edge admission surfaces
// to Core AttemptDispatcher. Provider/model/path values are never accepted
// from the rebuilder; they come exclusively from RunDispatch after admission.
type openAIAttemptDispatcher struct {
service runService
store *openAIRebuiltRequestStore
build openAIAttemptAdmissionBuilder
eventSource openAIAttemptEventSourceFactory
}
func newOpenAIAttemptDispatcher(
service runService,
store *openAIRebuiltRequestStore,
build openAIAttemptAdmissionBuilder,
eventSource openAIAttemptEventSourceFactory,
) (*openAIAttemptDispatcher, error) {
if service == nil || store == nil || build == nil || eventSource == nil {
return nil, fmt.Errorf("OpenAI attempt dispatcher dependencies are required")
}
return &openAIAttemptDispatcher{service: service, store: store, build: build, eventSource: eventSource}, nil
}
func (d *openAIAttemptDispatcher) DispatchAttempt(ctx context.Context, request streamgate.RebuiltRequest) (streamgate.AttemptBinding, error) {
if err := ctx.Err(); err != nil {
return streamgate.AttemptBinding{}, err
}
if err := request.Validate(); err != nil {
return streamgate.AttemptBinding{}, err
}
lease, err := d.store.take(request.RequestRef())
if err != nil {
return streamgate.AttemptBinding{}, err
}
owned := true
defer func() {
if owned {
lease.release()
}
}()
body, err := lease.body()
if err != nil {
return streamgate.AttemptBinding{}, err
}
admission, err := d.build(ctx, request, body)
if err != nil {
return streamgate.AttemptBinding{}, err
}
if err := admission.validate(); err != nil {
return streamgate.AttemptBinding{}, err
}
transport, dispatch, closeTransport, err := d.dispatch(ctx, admission)
if err != nil {
return streamgate.AttemptBinding{}, err
}
transportOwned := true
defer func() {
if transportOwned {
closeTransport()
}
}()
controller := &openAIAttemptController{
service: d.service,
dispatch: dispatch,
closeTransport: closeTransport,
lease: lease,
}
abortDispatched := func() {
owned = false
transportOwned = false
_ = controller.AbortAttempt(context.Background())
}
source, err := d.eventSource(transport)
if err != nil {
abortDispatched()
return streamgate.AttemptBinding{}, err
}
binding, err := streamgate.NewAttemptBinding(
openAIStreamGateSafeToken("attempt", dispatch.RunID),
actualOpenAIModel(dispatch),
actualOpenAIProvider(dispatch),
actualOpenAIExecutionPath(dispatch, transport.path),
source,
controller,
)
if err != nil {
abortDispatched()
return streamgate.AttemptBinding{}, err
}
owned = false
transportOwned = false
return binding, nil
}
func (d *openAIAttemptDispatcher) dispatch(
ctx context.Context,
admission openAIAttemptAdmission,
) (openAIAttemptTransport, edgeservice.RunDispatch, func(), error) {
switch admission.kind {
case openAIAdmissionRun:
handle, err := d.service.SubmitRun(ctx, admission.run)
if err != nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
}
return openAIAttemptTransport{path: openAIAdmissionRun, run: handle}, handle.Dispatch(), handle.Close, nil
case openAIAdmissionTunnel:
request := admission.tunnel
if admission.authorize != nil {
headers, err := admission.authorize(ctx)
if err != nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
}
request.Headers = headers
}
handle, err := d.service.SubmitProviderTunnel(ctx, request)
if err != nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
}
return openAIAttemptTransport{path: openAIAdmissionTunnel, tunnel: handle}, handle.Dispatch(), handle.Close, nil
case openAIAdmissionPool:
request := admission.pool
if admission.authorize != nil {
previous := request.PrepareTunnel
request.PrepareTunnel = func(tunnel edgeservice.SubmitProviderTunnelRequest) (edgeservice.SubmitProviderTunnelRequest, error) {
headers, err := admission.authorize(ctx)
if err != nil {
return tunnel, err
}
tunnel.Headers = headers
if previous != nil {
return previous(tunnel)
}
return tunnel, nil
}
}
result, err := d.service.SubmitProviderPool(ctx, request)
if err != nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
}
if result == nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool returned no result")
}
switch result.Path {
case edgeservice.ProviderPoolPathNormalized:
if result.Run == nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool normalized result is missing")
}
return openAIAttemptTransport{path: openAIAdmissionRun, run: result.Run}, result.DispatchInfo, result.Run.Close, nil
case edgeservice.ProviderPoolPathTunnel:
if result.Tunnel == nil {
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool tunnel result is missing")
}
return openAIAttemptTransport{path: openAIAdmissionTunnel, tunnel: result.Tunnel}, result.DispatchInfo, result.Tunnel.Close, nil
default:
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool returned an unknown path")
}
default:
return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("unknown OpenAI recovery admission kind")
}
}
func actualOpenAIModel(dispatch edgeservice.RunDispatch) string {
if model := strings.TrimSpace(dispatch.Target); model != "" {
return model
}
return strings.TrimSpace(dispatch.ModelGroupKey)
}
func actualOpenAIProvider(dispatch edgeservice.RunDispatch) string {
if provider := strings.TrimSpace(dispatch.ProviderID); provider != "" {
return provider
}
if provider := strings.TrimSpace(dispatch.Adapter); provider != "" {
return provider
}
return strings.TrimSpace(dispatch.NodeID)
}
func actualOpenAIExecutionPath(dispatch edgeservice.RunDispatch, path openAIAdmissionKind) string {
if executionPath := strings.TrimSpace(dispatch.ExecutionPath); executionPath != "" {
return executionPath
}
if path == openAIAdmissionTunnel {
return string(edgeservice.ProviderPoolPathTunnel)
}
return string(edgeservice.ProviderPoolPathNormalized)
}
type openAIAttemptController struct {
mu sync.Mutex
closed bool
service runService
dispatch edgeservice.RunDispatch
closeTransport func()
lease *openAIRebuiltLease
}
// claimOwnership atomically transitions the controller to closed exactly once
// and hands the caller the transport closer and rebuilt lease so both
// AbortAttempt and CloseAttempt release ownership at most once.
func (c *openAIAttemptController) claimOwnership() (func(), *openAIRebuiltLease, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil, nil, false
}
c.closed = true
closeTransport := c.closeTransport
lease := c.lease
c.closeTransport = nil
c.lease = nil
return closeTransport, lease, true
}
// AbortAttempt cancels the current provider run and releases its transport and
// rebuilt lease. It is idempotent and is the non-graceful teardown used for
// recovery replace, terminal error, and caller cancel.
func (c *openAIAttemptController) AbortAttempt(ctx context.Context) error {
if c == nil {
return nil
}
closeTransport, lease, claimed := c.claimOwnership()
if !claimed {
return nil
}
var cancelErr error
if c.dispatch.RunID != "" {
_, cancelErr = c.service.CancelRun(ctx, edgeservice.CancelRunRequest{
NodeRef: c.dispatch.NodeID, RunID: c.dispatch.RunID,
Adapter: c.dispatch.Adapter, Target: c.dispatch.Target,
SessionID: c.dispatch.SessionID,
})
}
if closeTransport != nil {
closeTransport()
}
lease.release()
return cancelErr
}
// CloseAttempt releases the current provider transport and rebuilt lease
// without issuing a provider-side cancel. It is the graceful teardown used
// after a normal (successful) terminal, where the provider run already
// completed, and is idempotent with AbortAttempt through the shared claim.
func (c *openAIAttemptController) CloseAttempt(ctx context.Context) error {
if c == nil {
return nil
}
closeTransport, lease, claimed := c.claimOwnership()
if !claimed {
return nil
}
if closeTransport != nil {
closeTransport()
}
lease.release()
return nil
}
var _ streamgate.AttemptDispatcher = (*openAIAttemptDispatcher)(nil)
var _ streamgate.AttemptController = (*openAIAttemptController)(nil)