package router import ( "context" "encoding/json" "fmt" "gophergate/internal/db" ) // Decision holds the result of a routing decision. type Decision struct { SelectedModel string `json:"selected_model"` Strategy string `json:"strategy"` // "heuristic" or "classifier" Reason string `json:"reason"` } // ClassifierFunc is the callback for classifier-based routing. type ClassifierFunc func(ctx context.Context, selectorModel, systemPrompt, userMessage string) (string, error) // Router resolves model groups to concrete models. type Router struct { groups map[string]db.ModelGroup classify ClassifierFunc } // New creates a Router. classify may be nil if no classifier groups exist. func New(groups []db.ModelGroup, classify ClassifierFunc) *Router { r := &Router{ groups: make(map[string]db.ModelGroup), classify: classify, } for _, g := range groups { r.groups[g.ID] = g } return r } // IsGroup returns true if the model name is a group ID. func (r *Router) IsGroup(modelID string) bool { _, ok := r.groups[modelID] return ok } // Route resolves a group to a concrete model. func (r *Router) Route(ctx context.Context, groupID string, userMessage string) (*Decision, error) { group, ok := r.groups[groupID] if !ok { return nil, fmt.Errorf("unknown model group: %s", groupID) } var targets []string if err := json.Unmarshal([]byte(group.Targets), &targets); err != nil || len(targets) == 0 { return nil, fmt.Errorf("invalid or empty targets for group %s", groupID) } switch group.Strategy { case "heuristic": return routeHeuristic(group, targets, userMessage) case "classifier": if r.classify == nil { return routeHeuristic(group, targets, userMessage) } return routeClassifier(ctx, r.classify, group, targets, userMessage) default: return nil, fmt.Errorf("unknown strategy: %s", group.Strategy) } } // Reload replaces the group definitions without recreating the router. func (r *Router) Reload(groups []db.ModelGroup) { r.groups = make(map[string]db.ModelGroup) for _, g := range groups { r.groups[g.ID] = g } }