From 93688aea8de93ecf21a675aeb431bfe4ed7b5085 Mon Sep 17 00:00:00 2001 From: OpenClaw Assistant Date: Sat, 21 Feb 2026 19:58:10 +0000 Subject: [PATCH] fix(complexity_router): Address Greptile review round 2 1. **Empty user message handling** - Changed from falsy check to None check to properly distinguish 'no user message' from 'empty string message' 2. **ReDoS prevention** - Changed 'first.*then' to 'first.*?then' (non-greedy) to prevent regex backtracking on pathological inputs 3. **Documentation sync** - Updated README.md to match actual config values: - Tier boundaries: 0.15/0.35/0.60 (not 0.25/0.50/0.75) - Dimension weights: tokenCount=0.10, codePresence=0.30, technicalTerms=0.25, simpleIndicators=0.05, multiStepPatterns=0.03, questionComplexity=0.02 4. **Missing UI component** - Added ComplexityRouterConfig.tsx with: - Tier-to-model dropdown selectors - Descriptions and examples for each tier - How classification works explanation 5. **Inline import comment** - Added explanation for why ComplexityRouter import is inline (matches AutoRouter pattern, avoids circular imports) --- litellm/router.py | 2 + .../complexity_router/README.md | 38 ++--- .../complexity_router/complexity_router.py | 11 +- .../add_model/ComplexityRouterConfig.tsx | 136 ++++++++++++++++++ 4 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx diff --git a/litellm/router.py b/litellm/router.py index a15e91f5bd0..45030d7991d 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -6301,6 +6301,8 @@ class Router: This will initialize the complexity-router and add it to the complexity-routers dictionary. """ + # Import here to match AutoRouter pattern and avoid circular imports + # (ComplexityRouter is a CustomLogger subclass that imports litellm internals) from litellm.router_strategy.complexity_router.complexity_router import ComplexityRouter complexity_router_config: Optional[ diff --git a/litellm/router_strategy/complexity_router/README.md b/litellm/router_strategy/complexity_router/README.md index c892e51b4a8..2f92c41d164 100644 --- a/litellm/router_strategy/complexity_router/README.md +++ b/litellm/router_strategy/complexity_router/README.md @@ -17,22 +17,22 @@ The router scores each request across 7 dimensions: | Dimension | Description | Weight | |-----------|-------------|--------| -| `tokenCount` | Short prompts = simple, long = complex | 0.15 | -| `codePresence` | Code keywords (function, class, etc.) | 0.20 | +| `tokenCount` | Short prompts = simple, long = complex | 0.10 | +| `codePresence` | Code keywords (function, class, etc.) | 0.30 | | `reasoningMarkers` | "step by step", "think through", etc. | 0.25 | -| `technicalTerms` | Domain complexity indicators | 0.15 | -| `simpleIndicators` | "what is", "define" (negative weight) | 0.15 | -| `multiStepPatterns` | "first...then", numbered steps | 0.05 | -| `questionComplexity` | Multiple question marks | 0.05 | +| `technicalTerms` | Domain complexity indicators | 0.25 | +| `simpleIndicators` | "what is", "define" (negative weight) | 0.05 | +| `multiStepPatterns` | "first...then", numbered steps | 0.03 | +| `questionComplexity` | Multiple question marks | 0.02 | The weighted sum is mapped to tiers using configurable boundaries: | Tier | Score Range | Typical Use | |------|-------------|-------------| -| SIMPLE | < 0.25 | Basic questions, greetings | -| MEDIUM | 0.25 - 0.50 | Standard queries | -| COMPLEX | 0.50 - 0.75 | Technical, multi-part requests | -| REASONING | > 0.75 | Chain-of-thought, analysis | +| SIMPLE | < 0.15 | Basic questions, greetings | +| MEDIUM | 0.15 - 0.35 | Standard queries | +| COMPLEX | 0.35 - 0.60 | Technical, multi-part requests | +| REASONING | > 0.60 | Chain-of-thought, analysis | ## Configuration @@ -68,9 +68,9 @@ model_list: # Tier boundaries (normalized scores) tier_boundaries: - simple_medium: 0.25 - medium_complex: 0.50 - complex_reasoning: 0.75 + simple_medium: 0.15 + medium_complex: 0.35 + complex_reasoning: 0.60 # Token count thresholds token_thresholds: @@ -79,13 +79,13 @@ model_list: # Dimension weights (must sum to ~1.0) dimension_weights: - tokenCount: 0.15 - codePresence: 0.20 + tokenCount: 0.10 + codePresence: 0.30 reasoningMarkers: 0.25 - technicalTerms: 0.15 - simpleIndicators: 0.15 - multiStepPatterns: 0.05 - questionComplexity: 0.05 + technicalTerms: 0.25 + simpleIndicators: 0.05 + multiStepPatterns: 0.03 + questionComplexity: 0.02 # Override default keyword lists code_keywords: diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index 2e3f32dc533..0821ad9dd1a 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -93,8 +93,9 @@ class ComplexityRouter(CustomLogger): self.simple_keywords = self.config.simple_keywords or DEFAULT_SIMPLE_KEYWORDS # Pre-compile regex patterns for efficiency + # Use non-greedy .*? to prevent ReDoS on pathological inputs self._multi_step_patterns = [ - re.compile(r"first.*then", re.IGNORECASE), + re.compile(r"first.*?then", re.IGNORECASE), re.compile(r"step\s*\d", re.IGNORECASE), re.compile(r"\d+\.\s"), re.compile(r"[a-z]\)\s", re.IGNORECASE), @@ -359,19 +360,19 @@ class ComplexityRouter(CustomLogger): return None # Extract the last user message and the last system prompt - user_message = "" - system_prompt = None + user_message: Optional[str] = None + system_prompt: Optional[str] = None for msg in reversed(messages): role = msg.get("role", "") content = msg.get("content", "") if isinstance(content, str): - if role == "user" and not user_message: + if role == "user" and user_message is None: user_message = content elif role == "system" and system_prompt is None: system_prompt = content - if not user_message: + if user_message is None: verbose_router_logger.debug( "ComplexityRouter: No user message found, skipping routing" ) diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx new file mode 100644 index 00000000000..a20cb969e33 --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx @@ -0,0 +1,136 @@ +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Select as AntdSelect, Card, Divider, Space, Tooltip, Typography } from "antd"; +import React from "react"; +import { ModelGroup } from "../playground/llm_calls/fetch_models"; + +const { Text } = Typography; + +interface ComplexityTiers { + SIMPLE: string; + MEDIUM: string; + COMPLEX: string; + REASONING: string; +} + +interface ComplexityRouterConfigProps { + modelInfo: ModelGroup[]; + value: ComplexityTiers; + onChange: (tiers: ComplexityTiers) => void; +} + +const TIER_DESCRIPTIONS: Record = { + SIMPLE: { + label: "Simple", + description: "Basic questions, greetings, simple factual queries", + examples: '"Hello!", "What is Python?", "Thanks!"', + }, + MEDIUM: { + label: "Medium", + description: "Standard queries requiring some reasoning or explanation", + examples: '"Explain how REST APIs work", "Debug this error"', + }, + COMPLEX: { + label: "Complex", + description: "Technical, multi-part requests requiring deep knowledge", + examples: '"Design a microservices architecture", "Implement a rate limiter"', + }, + REASONING: { + label: "Reasoning", + description: "Chain-of-thought, analysis, explicit reasoning requests", + examples: '"Think step by step...", "Analyze the pros and cons..."', + }, +}; + +const ComplexityRouterConfig: React.FC = ({ modelInfo, value, onChange }) => { + // Prepare model options for dropdowns + const modelOptions = modelInfo.map((model) => ({ + value: model.model_group, + label: model.model_group, + })); + + const handleTierChange = (tier: keyof ComplexityTiers, model: string) => { + onChange({ + ...value, + [tier]: model, + }); + }; + + return ( +
+ + + Complexity Tier Configuration + + + + + + + + The complexity router automatically classifies requests by complexity using rule-based scoring (no API calls, + <1ms latency). Configure which model handles each tier. + + + + {(Object.keys(TIER_DESCRIPTIONS) as Array).map((tier, index) => { + const tierInfo = TIER_DESCRIPTIONS[tier]; + return ( +
+ {index > 0 && } +
+
+ + {tierInfo.label} Tier + + + + +
+ + Examples: {tierInfo.examples} + + handleTierChange(tier, model)} + placeholder={`Select model for ${tierInfo.label.toLowerCase()} queries`} + showSearch + style={{ width: "100%" }} + options={modelOptions} + /> +
+
+ ); + })} +
+ + + + + + How Classification Works + + + The router scores each request across 7 dimensions: token count, code presence, reasoning markers, technical + terms, simple indicators, multi-step patterns, and question complexity. The weighted score determines the + tier: + +
    +
  • + SIMPLE: Score < 0.15 +
  • +
  • + MEDIUM: Score 0.15 - 0.35 +
  • +
  • + COMPLEX: Score 0.35 - 0.60 +
  • +
  • + REASONING: Score > 0.60 (or 2+ reasoning markers) +
  • +
+
+
+ ); +}; + +export default ComplexityRouterConfig;