feat(routing): router dispatch wrapper
Composes Fetcher + Policy + Logger + CompleteFunc into a single Run method. Falls open to Claude on local-model errors; defaults to local when brain is unreachable. Skill packages will receive Router.Run as their CompleteFunc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
80
internal/routing/router.go
Normal file
80
internal/routing/router.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// CompleteFunc matches the signature used by every skill package's Config.
|
||||
type CompleteFunc func(ctx context.Context, model, system, user string) (string, int64, error)
|
||||
|
||||
// RunInput captures the per-call inputs the dispatch wrapper needs.
|
||||
type RunInput struct {
|
||||
Skill string
|
||||
System string
|
||||
User string
|
||||
SessionID string
|
||||
ProjectRoot string
|
||||
}
|
||||
|
||||
// Router composes a pass-rate fetcher, a decision policy, a session logger,
|
||||
// and a LiteLLM client. Skill packages receive Router.Run as their CompleteFunc.
|
||||
type Router struct {
|
||||
Fetcher *Fetcher
|
||||
Logger *Logger
|
||||
Policy Policy
|
||||
LocalModel string
|
||||
ClaudeModel string
|
||||
Complete CompleteFunc
|
||||
}
|
||||
|
||||
// Run executes one skill call: decides local vs claude, calls LiteLLM, logs the
|
||||
// decision. On local-side error, falls open by retrying once on the Claude model.
|
||||
func (r *Router) Run(ctx context.Context, in RunInput) (string, int64, error) {
|
||||
pr, ferr := r.Fetcher.Get(ctx, in.Skill)
|
||||
if ferr != nil {
|
||||
slog.Warn("router: pass-rate unreachable, defaulting to local", "skill", in.Skill, "err", ferr)
|
||||
pr = nil
|
||||
}
|
||||
hash := CanonicalHash(in.System, in.User)
|
||||
decision := r.Policy.Decide(pr, hash)
|
||||
|
||||
model := r.ClaudeModel
|
||||
if decision == DecideLocal {
|
||||
model = r.LocalModel
|
||||
}
|
||||
|
||||
out, ms, err := r.Complete(ctx, model, in.System, in.User)
|
||||
_ = r.Logger.LogDecision(ctx, LogEntry{
|
||||
SessionID: in.SessionID,
|
||||
Skill: in.Skill,
|
||||
Decision: decision.String(),
|
||||
Message: fmt.Sprintf("model=%s, pass_rate=%s", model, formatPassRate(pr)),
|
||||
ProjectRoot: in.ProjectRoot,
|
||||
DurationMs: ms,
|
||||
Failed: err != nil,
|
||||
})
|
||||
|
||||
if err != nil && decision == DecideLocal {
|
||||
slog.Warn("router: local failed, falling open to claude", "skill", in.Skill, "err", err)
|
||||
out, ms, err = r.Complete(ctx, r.ClaudeModel, in.System, in.User)
|
||||
_ = r.Logger.LogDecision(ctx, LogEntry{
|
||||
SessionID: in.SessionID,
|
||||
Skill: in.Skill,
|
||||
Decision: "claude_fallback",
|
||||
Message: fmt.Sprintf("model=%s, after-local-error", r.ClaudeModel),
|
||||
ProjectRoot: in.ProjectRoot,
|
||||
DurationMs: ms,
|
||||
Failed: err != nil,
|
||||
})
|
||||
}
|
||||
return out, ms, err
|
||||
}
|
||||
|
||||
func formatPassRate(pr *float64) string {
|
||||
if pr == nil {
|
||||
return "null"
|
||||
}
|
||||
return fmt.Sprintf("%.2f", *pr)
|
||||
}
|
||||
Reference in New Issue
Block a user