| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export type SmartRoutingConfig = { |
| enabled: boolean |
| |
| simpleModel: string |
| |
| strongModel: string |
| |
| simpleMaxChars?: number |
| |
| simpleMaxWords?: number |
| } |
|
|
| export type RoutingDecision = { |
| model: string |
| complexity: 'simple' | 'strong' |
| |
| reason: string |
| } |
|
|
| export type RoutingInput = { |
| |
| userText: string |
| |
| |
| |
| |
| |
| |
| recentToolUses?: number |
| |
| |
| |
| |
| |
| turnNumber?: number |
| } |
|
|
| const DEFAULT_SIMPLE_MAX_CHARS = 160 |
| const DEFAULT_SIMPLE_MAX_WORDS = 28 |
|
|
| |
| |
| |
| |
| const STRONG_KEYWORDS = [ |
| 'plan', |
| 'design', |
| 'architect', |
| 'architecture', |
| 'refactor', |
| 'debug', |
| 'investigate', |
| 'analyze', |
| 'analyse', |
| 'implement', |
| 'optimize', |
| 'optimise', |
| 'review', |
| 'audit', |
| 'diagnose', |
| 'root cause', |
| 'root-cause', |
| 'why does', |
| 'why is', |
| 'how should', |
| 'why did', |
| 'propose', |
| 'trace', |
| 'reproduce', |
| ] |
|
|
| const STRONG_KEYWORD_RE = new RegExp( |
| `\\b(?:${STRONG_KEYWORDS.map(k => k.replace(/[-]/g, '[-\\s]')).join('|')})\\b`, |
| 'i', |
| ) |
|
|
| const CODE_FENCE_RE = /```[\s\S]*?```|`[^`\n]+`/ |
|
|
| function countWords(text: string): number { |
| const trimmed = text.trim() |
| if (!trimmed) return 0 |
| return trimmed.split(/\s+/).length |
| } |
|
|
| function hasMultiParagraph(text: string): boolean { |
| return /\n\s*\n/.test(text) |
| } |
|
|
| function hasCode(text: string): boolean { |
| return CODE_FENCE_RE.test(text) |
| } |
|
|
| function hasStrongKeyword(text: string): boolean { |
| return STRONG_KEYWORD_RE.test(text) |
| } |
|
|
| |
| |
| |
| |
| |
| export function routeModel( |
| input: RoutingInput, |
| config: SmartRoutingConfig, |
| ): RoutingDecision { |
| if (!config.enabled) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'smart-routing disabled', |
| } |
| } |
| if (!config.simpleModel || !config.strongModel) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'simpleModel or strongModel missing from config', |
| } |
| } |
| if (config.simpleModel === config.strongModel) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'simpleModel equals strongModel', |
| } |
| } |
|
|
| const text = input.userText ?? '' |
| const trimmed = text.trim() |
|
|
| if (!trimmed) { |
| |
| return { |
| model: config.simpleModel, |
| complexity: 'simple', |
| reason: 'empty user text', |
| } |
| } |
|
|
| |
| if (input.turnNumber === 1) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'first turn of session', |
| } |
| } |
|
|
| const maxChars = config.simpleMaxChars ?? DEFAULT_SIMPLE_MAX_CHARS |
| const maxWords = config.simpleMaxWords ?? DEFAULT_SIMPLE_MAX_WORDS |
|
|
| if (hasCode(trimmed)) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'contains code block or inline code', |
| } |
| } |
|
|
| if (hasStrongKeyword(trimmed)) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'contains reasoning/planning keyword', |
| } |
| } |
|
|
| if (hasMultiParagraph(trimmed)) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: 'multi-paragraph input', |
| } |
| } |
|
|
| if (trimmed.length > maxChars) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: `input > ${maxChars} chars`, |
| } |
| } |
|
|
| if (countWords(trimmed) > maxWords) { |
| return { |
| model: config.strongModel, |
| complexity: 'strong', |
| reason: `input > ${maxWords} words`, |
| } |
| } |
|
|
| return { |
| model: config.simpleModel, |
| complexity: 'simple', |
| reason: `short (${trimmed.length} chars, ${countWords(trimmed)} words)`, |
| } |
| } |
|
|