iop/apps/edge/internal/openai/route_resolution.go
toki 495996fee4 feat(openai): 핫패스 에이전트 실행 경로를 확장한다
Anthropic·Chat 게이트와 관찰·종료 제어를 통합하고 관련 계약·검증 산출물을 반영한다.
2026-08-06 00:09:24 +09:00

256 lines
7.3 KiB
Go

package openai
import (
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
"strings"
)
// routeUsesProviderTunnel reports whether the resolved dispatch targets an
// OpenAI-compatible provider that serves raw tunnel passthrough. Provider-pool
// catalog routes and openai_compat/vllm type routes qualify; other
// legacy adapters keep the normalized RunEvent path.
func routeUsesProviderTunnel(d routeDispatch) bool {
if d.ProviderPool {
return true
}
switch strings.TrimSpace(d.Adapter) {
case "openai_compat", "vllm":
return true
default:
return false
}
}
func routeSupportsNativeToolCalls(dispatch routeDispatch) bool {
if dispatch.ProviderPool {
return true
}
switch strings.TrimSpace(dispatch.Adapter) {
case "openai_compat", "vllm", "ollama":
return true
default:
return false
}
}
func (s *Server) resolveAdapter() string {
if s.cfg.Adapter != "" {
return s.cfg.Adapter
}
return "ollama"
}
func (s *Server) resolveTarget(model string) string {
if s.cfg.Target != "" {
return s.cfg.Target
}
return strings.TrimSpace(model)
}
// routeDispatch holds fully-resolved dispatch parameters for a single request.
type routeDispatch struct {
NodeRef string
ProviderID string
UsageAttribution string
Adapter string
Target string
SessionID string
TimeoutSec int
MaxQueue int
QueueTimeoutMS int
// ProviderPool is true when the request model matched a provider-pool
// catalog entry. Adapter and Target are empty; the service layer resolves
// them per-candidate and rewrites Target after admission.
ProviderPool bool
Managed bool
ModelGroupKey string
RouteID string
CredentialSlotRef string
ProfileID string
UpstreamModel string
ResourceSelector string
RouteRevision uint64
CredentialRevision uint64
PrincipalRef string
ProjectionGeneration uint64
ManagedPredicate edgeservice.ProviderPoolCandidatePredicate
IsPreset bool
PresetID string
ExternalModelID string
Preset config.ExecutionPreset
PresetResolvedBindings map[string]routeDispatch
}
func (d routeDispatch) credentialBinding() *edgeservice.CredentialBinding {
if !d.Managed {
return nil
}
return &edgeservice.CredentialBinding{
PrincipalRef: d.PrincipalRef, CredentialSlotRef: d.CredentialSlotRef,
RouteID: d.RouteID, ProfileID: d.ProfileID, CredentialRevision: d.CredentialRevision,
RouteRevision: d.RouteRevision, ProjectionGeneration: d.ProjectionGeneration,
}
}
func (d routeDispatch) CandidatePredicate() edgeservice.ProviderPoolCandidatePredicate {
return d.ManagedPredicate
}
func (d routeDispatch) effectiveModelGroupKey(requestModel string) string {
if key := strings.TrimSpace(d.ModelGroupKey); key != "" {
return key
}
return strings.TrimSpace(requestModel)
}
// resolveRoute returns the first catalog entry whose Model matches model.
// Entries with an empty Target are skipped.
func (s *Server) resolveRoute(model string) *config.OpenAIRouteEntry {
model = strings.TrimSpace(model)
if model == "" {
return nil
}
for i := range s.cfg.ModelRoutes {
r := &s.cfg.ModelRoutes[i]
if strings.TrimSpace(r.Model) == model && r.Target != "" {
return r
}
}
return nil
}
// findProviderPoolEntry returns the catalog entry matching model, or nil.
func (s *Server) findProviderPoolEntry(model string) *config.ModelCatalogEntry {
model = strings.TrimSpace(model)
if model == "" {
return nil
}
modelCatalog := s.modelCatalogSnapshot()
for i := range modelCatalog {
if modelCatalog[i].ID == model {
entry := modelCatalog[i]
return &entry
}
}
return nil
}
// resolveRouteDispatch returns fully-resolved dispatch params for model.
// Priority: provider-pool catalog → legacy model_routes → single-target fallback.
// Returns (dispatch, true) on success; (zero, false) when no target can be resolved.
func (s *Server) resolveRouteDispatch(model string) (routeDispatch, bool) {
// Provider-pool catalog takes highest priority.
if catalogEntry := s.findProviderPoolEntry(model); catalogEntry != nil {
if catalogEntry.ExecutionPreset != "" {
preset, ok := s.ExecutionPreset(catalogEntry.ExecutionPreset)
if !ok {
return routeDispatch{}, false
}
refs := preset.CanonicalModelReferences()
bindings := make(map[string]routeDispatch, len(refs))
for _, ref := range refs {
if ref == model {
return routeDispatch{}, false
}
if len(s.modelCatalogSnapshot()) > 0 {
if s.findProviderPoolEntry(ref) == nil && s.resolveRoute(ref) == nil {
return routeDispatch{}, false
}
}
refDispatch, ok := s.resolveRouteDispatch(ref)
if !ok {
return routeDispatch{}, false
}
bindings[ref] = refDispatch
}
selectorDispatch := bindings[preset.Selector.Model]
return routeDispatch{
NodeRef: selectorDispatch.NodeRef,
ProviderID: selectorDispatch.ProviderID,
UsageAttribution: catalogEntry.EffectiveUsageAttribution(),
Adapter: selectorDispatch.Adapter,
Target: selectorDispatch.Target,
SessionID: s.resolveSessionID(),
TimeoutSec: s.resolveTimeoutSec(),
MaxQueue: selectorDispatch.MaxQueue,
QueueTimeoutMS: selectorDispatch.QueueTimeoutMS,
ProviderPool: true,
IsPreset: true,
PresetID: catalogEntry.ExecutionPreset,
ExternalModelID: model,
Preset: preset,
PresetResolvedBindings: bindings,
}, true
}
return routeDispatch{
UsageAttribution: catalogEntry.EffectiveUsageAttribution(),
SessionID: s.resolveSessionID(),
TimeoutSec: s.resolveTimeoutSec(),
ProviderPool: true,
}, true
}
if route := s.resolveRoute(model); route != nil {
providerID := strings.TrimSpace(route.ProviderID)
if providerID == "" {
providerID = strings.TrimSpace(s.cfg.ProviderID)
}
adapter := route.Adapter
if adapter == "" {
adapter = s.resolveAdapter()
}
nodeRef := route.NodeRef
if nodeRef == "" {
nodeRef = s.cfg.NodeRef
}
sessionID := route.SessionID
if sessionID == "" {
sessionID = s.resolveSessionID()
}
timeoutSec := route.TimeoutSec
if timeoutSec <= 0 {
timeoutSec = s.resolveTimeoutSec()
}
return routeDispatch{
NodeRef: nodeRef,
ProviderID: providerID,
UsageAttribution: config.UsageAttributionProvider,
Adapter: adapter,
Target: route.Target,
SessionID: sessionID,
TimeoutSec: timeoutSec,
MaxQueue: route.MaxQueue,
QueueTimeoutMS: route.QueueTimeoutMS,
}, true
}
target := s.resolveTarget(model)
if target == "" {
return routeDispatch{}, false
}
return routeDispatch{
NodeRef: s.cfg.NodeRef,
ProviderID: strings.TrimSpace(s.cfg.ProviderID),
UsageAttribution: config.UsageAttributionProvider,
Adapter: s.resolveAdapter(),
Target: target,
SessionID: s.resolveSessionID(),
TimeoutSec: s.resolveTimeoutSec(),
}, true
}
func (s *Server) resolveSessionID() string {
if s.cfg.SessionID != "" {
return s.cfg.SessionID
}
return edgeservice.DefaultSessionID
}
func (s *Server) resolveTimeoutSec() int {
if s.cfg.TimeoutSec > 0 {
return s.cfg.TimeoutSec
}
return edgeservice.DefaultTimeoutSec
}