package openai import ( "fmt" edgeservice "iop/apps/edge/internal/service" "iop/packages/go/config" "path/filepath" "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; CLI and 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 Adapter string Target string SessionID string TimeoutSec int MaxQueue int QueueTimeoutMS int WorkspaceRequired bool // 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 } // 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 s.findProviderPoolEntry(model) != nil { return routeDispatch{ SessionID: s.resolveSessionID(), TimeoutSec: s.resolveTimeoutSec(), ProviderPool: true, }, true } if route := s.resolveRoute(model); route != nil { 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, Adapter: adapter, Target: route.Target, SessionID: sessionID, TimeoutSec: timeoutSec, MaxQueue: route.MaxQueue, QueueTimeoutMS: route.QueueTimeoutMS, WorkspaceRequired: route.WorkspaceRequired, }, true } target := s.resolveTarget(model) if target == "" { return routeDispatch{}, false } return routeDispatch{ NodeRef: s.cfg.NodeRef, 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 } func validateWorkspaceForRoute(d routeDispatch, workspace string) error { if !d.WorkspaceRequired { return nil } if strings.TrimSpace(workspace) == "" { return fmt.Errorf("workspace is required for this model route") } if !filepath.IsAbs(workspace) { return fmt.Errorf("workspace must be an absolute path") } return nil }