feat: classifier bucket mapping + dispatcher seed group
Classifier: When complexity_threshold is set (e.g. 10), uses it as the rating scale and maps ratings proportionally to target buckets instead of 1:1. Formula: idx = rating * len(targets) / (threshold + 1). With threshold=10 and 3 targets: 1-3→target[0], 4-7→target[1], 8-10→target[2]. Seed: Added 'dispatcher' group (classifier, threshold=10, selector=deepseek-v4-flash) that auto-routes to fast-flow/standard-pro/heavy-logic by complexity score. Combined with hierarchical routing, this enables two-level dispatch: dispatcher scores 1-10 → routes to tier group → tier picks concrete model.
This commit is contained in:
+19
-10
@@ -195,20 +195,21 @@ func (db *DB) RunMigrations() error {
|
|||||||
|
|
||||||
// Seed default model groups
|
// Seed default model groups
|
||||||
defaultGroups := []struct {
|
defaultGroups := []struct {
|
||||||
id, strategy, targets string
|
id, strategy, targets, selectorModel string
|
||||||
logicLevel *int
|
complexityThreshold, logicLevel *int
|
||||||
primaryUse *string
|
primaryUse *string
|
||||||
}{
|
}{
|
||||||
{"deepseek-auto", "heuristic", `["deepseek-chat","deepseek-reasoner"]`, nil, nil},
|
{"deepseek-auto", "heuristic", `["deepseek-chat","deepseek-reasoner"]`, "", nil, nil, nil},
|
||||||
{"openai-auto", "heuristic", `["gpt-4o-mini","gpt-4o"]`, nil, nil},
|
{"openai-auto", "heuristic", `["gpt-4o-mini","gpt-4o"]`, "", nil, nil, nil},
|
||||||
{"gemini-auto", "heuristic", `["gemini-2.0-flash","gemini-2.5-pro"]`, nil, nil},
|
{"gemini-auto", "heuristic", `["gemini-2.0-flash","gemini-2.5-pro"]`, "", nil, nil, nil},
|
||||||
{"heavy-logic", "heuristic", `["grok-4.3","kimi-k2.5","deepseek-v4-pro"]`, intPtr(9), strPtr("Complex Coding, Logic, Agents.")},
|
{"heavy-logic", "heuristic", `["grok-4.3","kimi-k2.5","deepseek-v4-pro"]`, "", nil, intPtr(9), strPtr("Complex Coding, Logic, Agents.")},
|
||||||
{"standard-pro", "heuristic", `["gpt-5.4-mini","gemini-3-flash"]`, intPtr(5), strPtr("General Assistant, Long Docs.")},
|
{"standard-pro", "heuristic", `["gpt-5.4-mini","gemini-3-flash"]`, "", nil, intPtr(5), strPtr("General Assistant, Long Docs.")},
|
||||||
{"fast-flow", "heuristic", `["deepseek-v4-flash","gpt-5.4-nano"]`, intPtr(2), strPtr("Classification, JSON, Basic Q&A.")},
|
{"fast-flow", "heuristic", `["deepseek-v4-flash","gpt-5.4-nano"]`, "", nil, intPtr(2), strPtr("Classification, JSON, Basic Q&A.")},
|
||||||
|
{"dispatcher", "classifier", `["fast-flow","standard-pro","heavy-logic"]`, "deepseek-v4-flash", intPtr(10), nil, strPtr("Auto-dispatches to tier groups by complexity.")},
|
||||||
}
|
}
|
||||||
for _, g := range defaultGroups {
|
for _, g := range defaultGroups {
|
||||||
db.Exec(`INSERT OR IGNORE INTO model_groups (id, strategy, targets, logic_level, primary_use) VALUES (?, ?, ?, ?, ?)`,
|
db.Exec(`INSERT OR IGNORE INTO model_groups (id, strategy, targets, selector_model, complexity_threshold, logic_level, primary_use) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
g.id, g.strategy, g.targets, g.logicLevel, g.primaryUse)
|
g.id, g.strategy, g.targets, nilStr(g.selectorModel), g.complexityThreshold, g.logicLevel, g.primaryUse)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -312,3 +313,11 @@ type ModelGroup struct {
|
|||||||
|
|
||||||
func intPtr(v int) *int { return &v }
|
func intPtr(v int) *int { return &v }
|
||||||
func strPtr(v string) *string { return &v }
|
func strPtr(v string) *string { return &v }
|
||||||
|
|
||||||
|
// nilStr returns a *string for non-empty strings, nil for empty.
|
||||||
|
func nilStr(v string) *string {
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,11 +16,19 @@ const classifierSystemPrompt = `You are a task complexity classifier. Rate the f
|
|||||||
Reply with ONLY the number. No explanation.`
|
Reply with ONLY the number. No explanation.`
|
||||||
|
|
||||||
func routeClassifier(ctx context.Context, classify ClassifierFunc, group db.ModelGroup, targets []string, userMessage string) (*Decision, error) {
|
func routeClassifier(ctx context.Context, classify ClassifierFunc, group db.ModelGroup, targets []string, userMessage string) (*Decision, error) {
|
||||||
|
// Determine the rating scale
|
||||||
maxRating := len(targets)
|
maxRating := len(targets)
|
||||||
if maxRating < 2 {
|
if maxRating < 2 {
|
||||||
maxRating = 2
|
maxRating = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When complexity_threshold is set, use it as a wider scale (e.g., 1-10)
|
||||||
|
// and map ratings proportionally to target buckets.
|
||||||
|
bucketMode := group.ComplexityThreshold != nil && *group.ComplexityThreshold > 0
|
||||||
|
if bucketMode {
|
||||||
|
maxRating = *group.ComplexityThreshold
|
||||||
|
}
|
||||||
|
|
||||||
prompt := fmt.Sprintf(classifierSystemPrompt, maxRating, maxRating)
|
prompt := fmt.Sprintf(classifierSystemPrompt, maxRating, maxRating)
|
||||||
ratingStr, err := classify(ctx, getSelectorModel(group, targets), prompt, userMessage)
|
ratingStr, err := classify(ctx, getSelectorModel(group, targets), prompt, userMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,7 +44,18 @@ func routeClassifier(ctx context.Context, classify ClassifierFunc, group db.Mode
|
|||||||
rating = maxRating
|
rating = maxRating
|
||||||
}
|
}
|
||||||
|
|
||||||
idx := rating - 1 // 0-based index into targets
|
var idx int
|
||||||
|
if bucketMode {
|
||||||
|
// Proportional mapping: wider scale → N target buckets
|
||||||
|
// e.g., threshold=10, 3 targets: 1-3→0, 4-7→1, 8-10→2
|
||||||
|
idx = rating * len(targets) / (maxRating + 1)
|
||||||
|
if idx >= len(targets) {
|
||||||
|
idx = len(targets) - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idx = rating - 1 // 1:1 mapping
|
||||||
|
}
|
||||||
|
|
||||||
return &Decision{
|
return &Decision{
|
||||||
SelectedModel: targets[idx],
|
SelectedModel: targets[idx],
|
||||||
Strategy: "classifier",
|
Strategy: "classifier",
|
||||||
|
|||||||
Reference in New Issue
Block a user