feat: add hierarchical routing — groups can target other groups
RouteToConcrete() recursively resolves group chains until a concrete model is reached, with cycle detection and max depth (10) guard. Example: all-purpose -> fast-flow -> deepseek-v4-flash The dashboard log shows the full chain: 'deepseek-v4-flash (hierarchical: fast-flow (default (first target)) -> deepseek-v4-flash (default (first target)))'
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gophergate/internal/db"
|
"gophergate/internal/db"
|
||||||
)
|
)
|
||||||
@@ -76,6 +77,49 @@ func (r *Router) Route(ctx context.Context, groupID string, userMessage string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RouteToConcrete resolves a model name to a concrete model, following group
|
||||||
|
// chains recursively until a non-group target is reached. Returns the original
|
||||||
|
// name unchanged if it is not a group.
|
||||||
|
func (r *Router) RouteToConcrete(ctx context.Context, modelID string, userMessage string) (*Decision, error) {
|
||||||
|
const maxDepth = 10
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
current := modelID
|
||||||
|
var chain []*Decision
|
||||||
|
|
||||||
|
for depth := 0; depth < maxDepth; depth++ {
|
||||||
|
if !r.IsGroup(current) {
|
||||||
|
// Build a composite reason showing the chain traversed
|
||||||
|
reason := "direct"
|
||||||
|
if len(chain) > 0 {
|
||||||
|
parts := make([]string, len(chain))
|
||||||
|
for i, d := range chain {
|
||||||
|
parts[i] = d.SelectedModel + " (" + d.Reason + ")"
|
||||||
|
}
|
||||||
|
reason = strings.Join(parts, " -> ")
|
||||||
|
}
|
||||||
|
return &Decision{
|
||||||
|
SelectedModel: current,
|
||||||
|
Strategy: "hierarchical",
|
||||||
|
Reason: reason,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if visited[current] {
|
||||||
|
return nil, fmt.Errorf("routing cycle detected: group %s already visited", current)
|
||||||
|
}
|
||||||
|
visited[current] = true
|
||||||
|
|
||||||
|
decision, err := r.Route(ctx, current, userMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chain = append(chain, decision)
|
||||||
|
current = decision.SelectedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("routing depth exceeded: reached max depth of %d", maxDepth)
|
||||||
|
}
|
||||||
|
|
||||||
// Reload replaces the group definitions without recreating the router.
|
// Reload replaces the group definitions without recreating the router.
|
||||||
func (r *Router) Reload(groups []db.ModelGroup) {
|
func (r *Router) Reload(groups []db.ModelGroup) {
|
||||||
r.groups = make(map[string]db.ModelGroup)
|
r.groups = make(map[string]db.ModelGroup)
|
||||||
|
|||||||
@@ -541,18 +541,20 @@ func (s *Server) handleChatCompletions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if model is a group and route to a concrete model
|
// Resolve model groups to concrete models (hierarchical — groups can target groups)
|
||||||
modelGroup := ""
|
modelGroup := ""
|
||||||
if s.modelRouter != nil && s.modelRouter.IsGroup(modelID) {
|
if s.modelRouter != nil {
|
||||||
modelGroup = modelID
|
|
||||||
userMessage := extractUserMessage(req.Messages)
|
userMessage := extractUserMessage(req.Messages)
|
||||||
decision, err := s.modelRouter.Route(c.Request.Context(), modelID, userMessage)
|
decision, err := s.modelRouter.RouteToConcrete(c.Request.Context(), modelID, userMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("model routing failed: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("model routing failed: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if decision.SelectedModel != modelID {
|
||||||
|
modelGroup = modelID
|
||||||
|
}
|
||||||
modelID = decision.SelectedModel
|
modelID = decision.SelectedModel
|
||||||
log.Printf("[ROUTER] %s -> %s (%s: %s)", modelGroup, modelID, decision.Strategy, decision.Reason)
|
log.Printf("[ROUTER] %s (%s: %s)", modelID, decision.Strategy, decision.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert ChatCompletionRequest to UnifiedRequest
|
// Convert ChatCompletionRequest to UnifiedRequest
|
||||||
|
|||||||
Reference in New Issue
Block a user