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)
This commit is contained in:
OpenClaw Assistant 2026-02-21 19:58:10 +00:00
parent c73b8ec385
commit 93688aea8d
4 changed files with 163 additions and 24 deletions

View file

@ -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[

View file

@ -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:

View file

@ -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"
)

View file

@ -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<keyof ComplexityTiers, { label: string; description: string; examples: string }> = {
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<ComplexityRouterConfigProps> = ({ 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 (
<div className="w-full max-w-none">
<Space align="center" style={{ marginBottom: 16 }}>
<Typography.Title level={4} style={{ margin: 0 }}>
Complexity Tier Configuration
</Typography.Title>
<Tooltip title="Map each complexity tier to a model. Simple queries use cheaper/faster models, complex queries use more capable models.">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</Space>
<Text type="secondary" style={{ display: "block", marginBottom: 24 }}>
The complexity router automatically classifies requests by complexity using rule-based scoring (no API calls,
&lt;1ms latency). Configure which model handles each tier.
</Text>
<Card>
{(Object.keys(TIER_DESCRIPTIONS) as Array<keyof ComplexityTiers>).map((tier, index) => {
const tierInfo = TIER_DESCRIPTIONS[tier];
return (
<div key={tier}>
{index > 0 && <Divider style={{ margin: "16px 0" }} />}
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Text strong style={{ fontSize: 16 }}>
{tierInfo.label} Tier
</Text>
<Tooltip title={tierInfo.description}>
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Text type="secondary" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
Examples: {tierInfo.examples}
</Text>
<AntdSelect
value={value[tier]}
onChange={(model) => handleTierChange(tier, model)}
placeholder={`Select model for ${tierInfo.label.toLowerCase()} queries`}
showSearch
style={{ width: "100%" }}
options={modelOptions}
/>
</div>
</div>
);
})}
</Card>
<Divider />
<Card className="bg-gray-50">
<Text strong style={{ display: "block", marginBottom: 8 }}>
How Classification Works
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
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:
</Text>
<ul style={{ marginTop: 8, marginBottom: 0, paddingLeft: 20, fontSize: 13, color: "rgba(0, 0, 0, 0.45)" }}>
<li>
<strong>SIMPLE</strong>: Score &lt; 0.15
</li>
<li>
<strong>MEDIUM</strong>: Score 0.15 - 0.35
</li>
<li>
<strong>COMPLEX</strong>: Score 0.35 - 0.60
</li>
<li>
<strong>REASONING</strong>: Score &gt; 0.60 (or 2+ reasoning markers)
</li>
</ul>
</Card>
</div>
);
};
export default ComplexityRouterConfig;