From 2251db4dbcc22c7dddca7aaf273cd99069c42e57 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai <157192462+saurabhhhcodes@users.noreply.github.com> Date: Thu, 28 May 2026 06:00:34 +0530 Subject: [PATCH] feat: add smart routing pattern detector --- src/index.ts | 8 + src/router/patternDetector.ts | 269 +++++++++++++++++++++++++++++++++ tests/pattern-detector.test.js | 66 ++++++++ 3 files changed, 343 insertions(+) create mode 100644 src/router/patternDetector.ts create mode 100644 tests/pattern-detector.test.js diff --git a/src/index.ts b/src/index.ts index c769e99..ecdd1b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,14 @@ import { contextRegistry } from "./introspection/contextRegistry"; import { ModelRouter } from "./router/modelRouter"; import { ModelRouterOptions, ApiKeyConfig } from "./router/types"; import { apiKeyManager } from "./router/apiKeyManager"; +export { + detectByPatterns, + DEFAULT_PATTERN_TASKS, + PatternDefinition, + PatternDetectionResult, + PatternDetectorOptions, + PatternTaskType +} from "./router/patternDetector"; let globalBudgetManager: BudgetManager | null = null; let globalModelRouter: ModelRouter | null = null; diff --git a/src/router/patternDetector.ts b/src/router/patternDetector.ts new file mode 100644 index 0000000..45bebd7 --- /dev/null +++ b/src/router/patternDetector.ts @@ -0,0 +1,269 @@ +/** + * Bounded regex-based task detector for smart model routing. + */ + +export type PatternTaskType = + | "code_generation" + | "code_review" + | "math_reasoning" + | "complex_reasoning" + | "document_analysis" + | "creative_writing" + | "translation" + | "simple_chat" + | "data_extraction" + | "chinese_language"; + +export interface PatternDefinition { + taskType: PatternTaskType | string; + model: string; + reason: string; + priority: number; + patterns: RegExp[]; + keywords?: string[]; +} + +export interface PatternDetectorOptions { + patterns?: PatternDefinition[]; + maxPromptLength?: number; + timeoutMs?: number; + minimumConfidence?: number; +} + +export interface PatternDetectionResult { + taskType: string; + confidence: number; + selectedModel?: string; + reason: string; + matchedPatterns: string[]; + matchedKeywords: string[]; + timedOut: boolean; + elapsedMs: number; +} + +const DEFAULT_MAX_PROMPT_LENGTH = 20_000; +const DEFAULT_TIMEOUT_MS = 25; +const DEFAULT_MINIMUM_CONFIDENCE = 0.35; + +export const DEFAULT_PATTERN_TASKS: PatternDefinition[] = [ + { + taskType: "chinese_language", + model: "moonshot-v1-32k", + reason: "Kimi is optimized for Chinese language tasks", + priority: 100, + patterns: [/[\u4e00-\u9fff]/u], + keywords: ["chinese", "mandarin", "中文", "汉语", "普通话"] + }, + { + taskType: "code_generation", + model: "claude-3-5-sonnet-20241022", + reason: "Claude is preferred for implementation and code generation", + priority: 95, + patterns: [ + /\b(?:write|create|build|implement|generate|develop)\b[\s\S]{0,80}\b(?:code|function|class|component|api|script|program)\b/i, + /\b(?:function|class|component|api|script)\b[\s\S]{0,80}\b(?:javascript|typescript|python|java|go|rust|react|node)\b/i + ], + keywords: ["write code", "create function", "implement", "build", "develop", "program"] + }, + { + taskType: "code_review", + model: "claude-3-5-sonnet-20241022", + reason: "Claude is preferred for code review, debugging, and refactoring", + priority: 90, + patterns: [ + /\b(?:review|debug|refactor|optimize|improve)\b[\s\S]{0,80}\b(?:code|function|class|component|implementation)\b/i, + /\b(?:find|fix)\b[\s\S]{0,80}\b(?:bug|bugs|issue|regression|defect)\b/i + ], + keywords: ["review code", "find bugs", "debug", "refactor", "optimize"] + }, + { + taskType: "math_reasoning", + model: "o1-mini", + reason: "o1-mini is preferred for mathematical reasoning", + priority: 85, + patterns: [ + /\b(?:solve|calculate|compute|evaluate|derive)\b[\s\S]{0,80}\b(?:equation|formula|integral|derivative|probability|interest|matrix)\b/i, + /(?:\d+\s*[+\-*/^=]\s*){2,}/ + ], + keywords: ["calculate", "solve", "compute", "equation", "formula", "math"] + }, + { + taskType: "complex_reasoning", + model: "o1", + reason: "o1 is preferred for complex reasoning and logical analysis", + priority: 80, + patterns: [ + /\b(?:step[\s-]*by[\s-]*step|reasoning|logical analysis|deduce|infer|prove)\b/i, + /\b(?:analyze|reason|diagnose)\b[\s\S]{0,80}\b(?:root cause|tradeoff|strategy|system failure|business problem)\b/i + ], + keywords: ["analyze", "reason", "logic", "deduce", "infer", "prove", "derive"] + }, + { + taskType: "document_analysis", + model: "gemini-2.5-pro", + reason: "Gemini is preferred for long document analysis", + priority: 75, + patterns: [ + /\b(?:summarize|analyse|analyze|review|extract)\b[\s\S]{0,80}\b(?:document|pdf|paper|contract|report|transcript)\b/i, + /\b(?:long|large|multi-page|hundred-page)\b[\s\S]{0,80}\b(?:document|pdf|paper|report)\b/i + ], + keywords: ["summarize document", "analyze document", "analyze pdf", "extract from", "review document"] + }, + { + taskType: "creative_writing", + model: "gpt-4o", + reason: "GPT-4o is preferred for creative writing tasks", + priority: 65, + patterns: [ + /\b(?:write|create|draft)\b[\s\S]{0,80}\b(?:story|poem|blog post|article|copy|product description)\b/i, + /\b(?:creative writing|engaging content|marketing copy)\b/i + ], + keywords: ["write story", "blog post", "creative", "copywriting", "article"] + }, + { + taskType: "translation", + model: "gpt-4o-mini", + reason: "GPT-4o mini is cost-effective for translation tasks", + priority: 60, + patterns: [ + /\b(?:translate|translation|convert)\b[\s\S]{0,80}\b(?:to|from|into)\b/i, + /\b(?:spanish|french|german|hindi|japanese|korean|english)\b[\s\S]{0,80}\b(?:translation|translate)\b/i + ], + keywords: ["translate", "translation", "convert to"] + }, + { + taskType: "data_extraction", + model: "gpt-4o-mini", + reason: "GPT-4o mini is preferred for structured data extraction", + priority: 55, + patterns: [ + /\b(?:extract|parse|pull|get)\b[\s\S]{0,80}\b(?:data|emails?|dates?|json|fields?|values?)\b/i, + /\b(?:scrape|parse json|get data from)\b/i + ], + keywords: ["extract", "parse", "get data from", "scrape", "pull data"] + }, + { + taskType: "simple_chat", + model: "gpt-4o-mini", + reason: "GPT-4o mini is preferred for lightweight chat", + priority: 40, + patterns: [/^\s*(?:hi|hello|hey|thanks|thank you)\b/i, /\bhow[\s\S]{0,20}are[\s\S]{0,20}you\b/i], + keywords: ["hello", "hi", "how are you", "thanks", "thank you", "help"] + } +]; + +/** + * Detect a prompt's task type with bounded regex and keyword matching. + */ +export function detectByPatterns( + prompt: string, + options: PatternDetectorOptions = {} +): PatternDetectionResult { + const startedAt = Date.now(); + const timeoutMs = Math.max(0, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); + const maxPromptLength = Math.max(0, options.maxPromptLength ?? DEFAULT_MAX_PROMPT_LENGTH); + const minimumConfidence = options.minimumConfidence ?? DEFAULT_MINIMUM_CONFIDENCE; + + if (typeof prompt !== "string" || prompt.trim() === "") { + return createUnknownResult("Prompt is empty or not a string", false, startedAt); + } + + const source = prompt.slice(0, maxPromptLength); + const lowerSource = source.toLowerCase(); + const definitions = [...(options.patterns ?? DEFAULT_PATTERN_TASKS)].sort( + (a, b) => b.priority - a.priority + ); + + let timedOut = false; + let best: PatternDetectionResult | null = null; + + for (const definition of definitions) { + if (Date.now() - startedAt > timeoutMs) { + timedOut = true; + break; + } + + const matchedPatterns: string[] = []; + for (const pattern of definition.patterns) { + if (Date.now() - startedAt > timeoutMs) { + timedOut = true; + break; + } + pattern.lastIndex = 0; + if (pattern.test(source)) { + matchedPatterns.push(pattern.toString()); + } + } + + const matchedKeywords = (definition.keywords ?? []) + .filter((keyword) => lowerSource.includes(keyword.toLowerCase())); + const confidence = calculateConfidence(matchedPatterns.length, matchedKeywords.length); + + if (confidence <= 0) { + continue; + } + + const candidate: PatternDetectionResult = { + taskType: definition.taskType, + confidence, + selectedModel: definition.model, + reason: definition.reason, + matchedPatterns, + matchedKeywords, + timedOut, + elapsedMs: Date.now() - startedAt + }; + + if ( + !best || + candidate.confidence > best.confidence || + (candidate.confidence === best.confidence && + definition.priority > getPriority(best.taskType, definitions)) + ) { + best = candidate; + } + + if (timedOut) { + break; + } + } + + if (!best || best.confidence < minimumConfidence) { + return createUnknownResult( + timedOut ? "Pattern detection timed out before a confident match" : "No confident pattern match", + timedOut, + startedAt + ); + } + + return { + ...best, + timedOut, + elapsedMs: Date.now() - startedAt + }; +} + +function calculateConfidence(patternMatches: number, keywordMatches: number): number { + const score = patternMatches * 0.56 + keywordMatches * 0.12; + return Number(Math.min(0.99, score).toFixed(2)); +} + +function getPriority(taskType: string, definitions: PatternDefinition[]): number { + return definitions.find((definition) => definition.taskType === taskType)?.priority ?? -1; +} + +function createUnknownResult( + reason: string, + timedOut: boolean, + startedAt: number +): PatternDetectionResult { + return { + taskType: "unknown", + confidence: 0, + reason, + matchedPatterns: [], + matchedKeywords: [], + timedOut, + elapsedMs: Date.now() - startedAt + }; +} diff --git a/tests/pattern-detector.test.js b/tests/pattern-detector.test.js new file mode 100644 index 0000000..4a7bc31 --- /dev/null +++ b/tests/pattern-detector.test.js @@ -0,0 +1,66 @@ +/** + * Pattern detector tests. + * + * Run: node tests/pattern-detector.test.js + */ + +const { detectByPatterns } = require("../dist/index.js"); + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +try { + const code = detectByPatterns("Please implement a TypeScript API endpoint for uploads"); + assert(code.taskType === "code_generation", "should detect code generation"); + assert(code.selectedModel === "claude-3-5-sonnet-20241022", "code should route to Claude"); + assert(code.matchedPatterns.length > 0, "code should include matched regex"); + + const review = detectByPatterns("Find bugs in this component and refactor code safely"); + assert(review.taskType === "code_review", "should detect code review"); + + const math = detectByPatterns("Can you calculate the probability and solve equation x + 2 = 5?"); + assert(math.taskType === "math_reasoning", "should detect math"); + assert(math.selectedModel === "o1-mini", "math should route to o1-mini"); + + const document = detectByPatterns("Summarize this large PDF report and extract key findings"); + assert(document.taskType === "document_analysis", "should detect document analysis"); + assert(document.selectedModel === "gemini-2.5-pro", "documents should route to Gemini"); + + const chinese = detectByPatterns("请把这段中文总结一下"); + assert(chinese.taskType === "chinese_language", "Chinese text should get priority"); + assert(chinese.selectedModel === "moonshot-v1-32k", "Chinese should route to Kimi"); + + const extraction = detectByPatterns("Parse JSON and extract email fields into a table"); + assert(extraction.taskType === "data_extraction", "should detect data extraction"); + + const custom = detectByPatterns("Escalate this urgent support incident", { + patterns: [ + { + taskType: "support_triage", + model: "gpt-4o-mini", + reason: "Support triage is lightweight", + priority: 1, + patterns: [/urgent[\s\S]{0,40}support/i], + keywords: ["incident"], + }, + ], + }); + assert(custom.taskType === "support_triage", "custom patterns should be supported"); + assert(custom.matchedKeywords.includes("incident"), "custom keywords should be reported"); + + const bounded = detectByPatterns(`${"noise ".repeat(1000)} implement code`, { + maxPromptLength: 20, + }); + assert(bounded.taskType === "unknown", "maxPromptLength should bound scanning"); + + const empty = detectByPatterns(" "); + assert(empty.taskType === "unknown", "empty prompt should return unknown"); + + console.log("Pattern detector tests passed."); +} catch (error) { + console.error("Pattern detector tests failed:", error.message); + process.exit(1); +}