import { classify } from "./classify.js"; import { DEFAULT_CONFIG } from "./config.js"; import { resolveModel } from "./models.js"; import { collectPromptSignals, repositorySignalScore } from "./signals.js"; import type { ModelProfile, RouteDecision, RouteInput, ThinkingLevel } from "./types.js"; const profileRank: Record = { fast: 0, balanced: 1, advanced: 2 }; function profileFor(kind: RouteDecision["kind"], score: number, advancedThreshold: number): ModelProfile { if (kind === "simple" && score < 35) return "fast"; if (kind === "explanation" && score < 30) return "fast"; if (kind === "architecture" || kind === "debugging" || (kind === "research" && score >= advancedThreshold)) return "advanced"; return "balanced"; } function thinkingFor(kind: RouteDecision["kind"], score: number, defaultThinking: ThinkingLevel): ThinkingLevel { if (kind === "simple") return "low"; if (kind === "explanation") return score >= 30 ? "medium" : "low"; if (kind === "bugfix") return score >= 45 ? "medium" : "low"; if (kind === "feature") return defaultThinking; if (kind === "refactor") return "high"; if (kind === "architecture" || kind === "debugging" || kind === "research") return score > 90 ? "xhigh" : "high"; return "medium"; } function resolveProfile(profile: ModelProfile, input: RouteInput): { profile: ModelProfile; model?: string; fallback: boolean } { const config = input.config ?? DEFAULT_CONFIG; const available = input.availableModels ?? []; const requested = resolveModel(config.models[profile], available); if (requested) return { profile, model: requested.id, fallback: false }; if (profile !== "balanced") { const balanced = resolveModel(config.models.balanced, available); if (balanced) return { profile: "balanced", model: balanced.id, fallback: true }; } const current = input.currentModel ? resolveModel(input.currentModel, available) : undefined; if (current) return { profile, model: current.id, fallback: true }; return { profile, fallback: true }; } export function route(input: RouteInput): RouteDecision { const config = input.config ?? DEFAULT_CONFIG; const classification = classify(input.prompt); const promptSignals = collectPromptSignals(input.prompt); const repository = { ...input.repository, promptFileCount: input.repository?.promptFileCount ?? promptSignals.mentionedFileCount }; const score = Math.min(100, classification.score + repositorySignalScore(repository)); const reasons = [...classification.reasons, `repository modifier: +${repositorySignalScore(repository)}`]; let desiredProfile = input.override?.profile ?? profileFor(classification.kind, score, config.advancedThreshold); let thinking = input.override?.thinking ?? thinkingFor(classification.kind, score, config.defaultThinking); let selection = resolveProfile(desiredProfile, input); let overridden = false; if (!config.enabled && !input.override) { selection = { profile: desiredProfile, model: input.currentModel, fallback: true }; reasons.push("routing disabled"); } else if (input.override) { overridden = true; if (input.override.model) selection = { profile: desiredProfile, model: input.override.model, fallback: false }; reasons.push("explicit override"); } else if (config.switchPolicy === "stable" && input.session?.current && selection.profile !== input.session.current.profile) { const current = input.session.current; const escalates = profileRank[selection.profile] > profileRank[current.profile] && score >= config.advancedThreshold; const withinBand = Math.abs(score - current.score) <= config.hysteresis; const switchingDownWithoutBoundary = profileRank[selection.profile] < profileRank[current.profile] && !input.session.taskBoundary; if (!escalates && (withinBand || switchingDownWithoutBoundary)) { const currentModel = current.model ? resolveModel(current.model, input.availableModels ?? []) : undefined; if (currentModel) { desiredProfile = current.profile; thinking = current.thinking; selection = { profile: current.profile, model: currentModel.id, fallback: false }; reasons.push("session stability retained current route"); } else { selection = { ...selection, fallback: true }; reasons.push("session model unavailable; retained resolved route"); } } } reasons.push(`profile: ${selection.profile}`, `thinking: ${thinking}`); if (selection.fallback) reasons.push("safe fallback: configured model unavailable"); return { kind: classification.kind, score, profile: selection.profile, model: selection.model, thinking, reasons, fallback: selection.fallback, overridden, }; }