180 lines
4.6 KiB
Go
180 lines
4.6 KiB
Go
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
|
|
}
|