diff --git a/internal/db/db.go b/internal/db/db.go index 8203da3a..2618357d 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -195,20 +195,21 @@ func (db *DB) RunMigrations() error { // Seed default model groups defaultGroups := []struct { - id, strategy, targets string - logicLevel *int + id, strategy, targets, selectorModel string + complexityThreshold, logicLevel *int primaryUse *string }{ - {"deepseek-auto", "heuristic", `["deepseek-chat","deepseek-reasoner"]`, nil, nil}, - {"openai-auto", "heuristic", `["gpt-4o-mini","gpt-4o"]`, nil, nil}, - {"gemini-auto", "heuristic", `["gemini-2.0-flash","gemini-2.5-pro"]`, nil, nil}, - {"heavy-logic", "heuristic", `["grok-4.3","kimi-k2.5","deepseek-v4-pro"]`, intPtr(9), strPtr("Complex Coding, Logic, Agents.")}, - {"standard-pro", "heuristic", `["gpt-5.4-mini","gemini-3-flash"]`, 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.")}, + {"deepseek-auto", "heuristic", `["deepseek-chat","deepseek-reasoner"]`, "", nil, 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, nil}, + {"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"]`, "", nil, intPtr(5), strPtr("General Assistant, Long Docs.")}, + {"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 { - db.Exec(`INSERT OR IGNORE INTO model_groups (id, strategy, targets, logic_level, primary_use) VALUES (?, ?, ?, ?, ?)`, - g.id, g.strategy, g.targets, g.logicLevel, g.primaryUse) + 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, nilStr(g.selectorModel), g.complexityThreshold, g.logicLevel, g.primaryUse) } return nil @@ -312,3 +313,11 @@ type ModelGroup struct { func intPtr(v int) *int { 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 +} diff --git a/internal/router/classifier.go b/internal/router/classifier.go index 49d54f97..17b18923 100644 --- a/internal/router/classifier.go +++ b/internal/router/classifier.go @@ -16,11 +16,19 @@ const classifierSystemPrompt = `You are a task complexity classifier. Rate the f Reply with ONLY the number. No explanation.` func routeClassifier(ctx context.Context, classify ClassifierFunc, group db.ModelGroup, targets []string, userMessage string) (*Decision, error) { + // Determine the rating scale maxRating := len(targets) if 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) ratingStr, err := classify(ctx, getSelectorModel(group, targets), prompt, userMessage) if err != nil { @@ -36,7 +44,18 @@ func routeClassifier(ctx context.Context, classify ClassifierFunc, group db.Mode 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{ SelectedModel: targets[idx], Strategy: "classifier",