feat(auto_router): keyword tier overrides and semantic keyword matching for the complexity router - #32829
Conversation
Greptile SummaryAdds two capabilities to the complexity auto-router: deterministic keyword-to-tier overrides (
Confidence Score: 5/5Safe to merge — all changed paths are additive or narrowly scoped fixes with no regressions on existing routing behavior. The lexical escalation logic is correct for all four ComplexityTier values. The semantic override correctly bypasses the library's internal encoding to carry caller metadata. The cache-clear fix is a well-targeted improvement with a regression test that directly models the previously broken scenario. Backend tests use deterministic fake embeddings with no real network calls, and frontend tests cover both the payload builder and the client-side semantic config guard. No existing tests were weakened. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/router_strategy/complexity_router/complexity_router.py | Adds lexical and semantic keyword-tier override resolution; lazy SemanticRouter init is cached correctly; override is evaluated before the scorer in async_pre_routing_hook |
| litellm/router_strategy/complexity_router/config.py | Adds KeywordTierRule model, TIER_SEVERITY_ORDER tuple, and three new fields with a second model_validator that guards semantic matching preconditions |
| litellm/proxy/management_endpoints/model_management_endpoints.py | Fixes clear_cache to pop only DB-model entries from auto_routers/complexity_routers instead of blanket .clear(), preserving config-defined routers |
| tests/test_litellm/router_strategy/test_complexity_router.py | 423 lines of new tests covering lexical escalation, semantic routing via injected fake embeddings, config validation, caching, and edge-case branches |
| tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py | Updated TestClearCache to use real dicts and adds TestClearCachePreservesConfigRouters regression test; mock changes correctly reflect the new targeted-clear behavior |
| ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx | Refactored into submitRecommendedRouter/submitSemanticRouter; adds keyword tier rule and semantic matching state; client-side guard mirrors backend validator |
| ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts | New pure builder and guard function; id field stripped from keyword tier rules before sending to backend; semantic fields only included when toggle is on |
| ui/litellm-dashboard/src/components/add_model/KeywordTierRules.tsx | New component for managing keyword-to-tier override rules; uses Date.now()-based ids stripped before submission |
| ui/litellm-dashboard/src/components/add_model/SemanticKeywordMatching.tsx | New component for semantic keyword matching settings; correctly exports DEFAULT_MATCH_THRESHOLD for use in parent state initialisation |
| ui/litellm-dashboard/src/components/add_model/handle_add_auto_router_submit.tsx | Cleans up dead custom-embedding-model branching; unused response variable removed |
Reviews (4): Last reviewed commit: "feat(auto_router): keyword tier override..." | Re-trigger Greptile
| routelayer = SemanticRouter( | ||
| routes=routes, | ||
| encoder=LiteLLMRouterEncoder( | ||
| litellm_router_instance=self.litellm_router_instance, | ||
| model_name=embedding_model, | ||
| score_threshold=self.config.match_threshold, | ||
| ), | ||
| auto_sync="local", | ||
| ) |
There was a problem hiding this comment.
auto_sync="local" instructs the semantic_router library to persist route embeddings to a local JSON file on first use. In containerised/serverless deployments this can fail outright on read-only filesystems, silently write to ephemeral storage that disappears on restart, or race when multiple replicas start simultaneously. The routes are always reconstructed deterministically from the config, so there is no value in persisting them; setting auto_sync to None or omitting it disables the file-I/O path entirely.
| routelayer = SemanticRouter( | |
| routes=routes, | |
| encoder=LiteLLMRouterEncoder( | |
| litellm_router_instance=self.litellm_router_instance, | |
| model_name=embedding_model, | |
| score_threshold=self.config.match_threshold, | |
| ), | |
| auto_sync="local", | |
| ) | |
| routelayer = SemanticRouter( | |
| routes=routes, | |
| encoder=LiteLLMRouterEncoder( | |
| litellm_router_instance=self.litellm_router_instance, | |
| model_name=embedding_model, | |
| score_threshold=self.config.match_threshold, | |
| ), | |
| auto_sync=None, | |
| ) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
3a030a6 to
5fb064a
Compare
5fb064a to
43ded6d
Compare
|
@greptileai thanks for the review. Fixed the frontend guard; pushing back on the Frontend guard (fixed):
def _write_config(self, config: ConfigParameter):
logger.warning("No config is written for LocalIndex.")
Happy to add a code comment noting the no-op |
|
Both points are well-argued — withdrawing the You're right that my comment conflated The frontend guard fix (a Tip: You can customize Greptile's behavior for this repo with |
|
@greptile-apps whats the updated rating |
|
Updated Confidence Score: 4/5 Both issues that dropped the original score to 3/5 have been resolved:
The remaining half-point gap: the optional code comment explaining |
43ded6d to
4cf05f7
Compare
4cf05f7 to
5ae72c8
Compare
PR overviewThis pull request adds keyword tier overrides and semantic keyword matching to the complexity router, with changes around router configuration and runtime matching behavior. It appears to let routing rules use lexical and semantic keyword criteria to influence model tier selection. There are still two open security concerns in the new routing path. The highest-risk issue is that semantic index construction can occur synchronously on the first request after a router update, allowing a team admin to trigger unbudgeted embedding calls and block the event loop. A related configuration issue also leaves keyword rules unbounded, which can let very large keyword sets degrade proxy worker availability; one earlier issue has already been addressed, but these remaining denial-of-service and budget-bypass paths keep the PR at elevated risk. Open issues (2)
Fixed/addressed: 1 · PR risk: 7/10 |
5ae72c8 to
ae89643
Compare
|
Thanks for catching this — confirmed and fixed in You're right that the blanket Fixed by scoping the clear to only the router entries backed by a DB model that's actually part of this reload, keyed by db_model_names = {
model.get("model_name") for model in current_models if model.get("model_info", {}).get("db_model", False)
}
for model_name in db_model_names:
llm_router.auto_routers.pop(model_name, None)
llm_router.complexity_routers.pop(model_name, None)Config-defined routers are never touched now; only the DB-backed ones get cleared (so they can be correctly re-populated by the reload). Added a dedicated regression test, |
36b841f to
ed32f0d
Compare
|
@greptile-apps check again and gimme a score homie |
4daa81d to
d8c46e0
Compare
|
@greptile-apps new score? |
d8c46e0 to
dc9ee1d
Compare
| ) | ||
| for tier in ordered_tiers | ||
| ] | ||
| routelayer = SemanticRouter( |
There was a problem hiding this comment.
High: Request-time index construction bypasses budgets
SemanticRouter initializes its index synchronously here, calling the encoder for dimensions and all rule utterances without the caller metadata forwarded below. A team admin can update a team-owned router to reset this lazy cache, then send its first request to repeatedly consume unbudgeted embedding spend and block the event loop during provider calls. Build the index asynchronously under a per-router lock with attributed metadata, or initialize it outside the request path; also bound the number and size of rule utterances.
dc9ee1d to
664eab7
Compare
| class KeywordTierRule(BaseModel): | ||
| """A deterministic override: if any keyword matches, route to this tier.""" | ||
|
|
||
| keywords: List[str] = Field( |
There was a problem hiding this comment.
Medium: Unbounded keyword matching can exhaust proxy workers
A team admin can save tens of thousands of keywords and send nonmatching requests, forcing _lexical_tier_override to compile and evaluate a regex for every keyword synchronously on the event loop; this can delay requests for other tenants. Add conservative limits on rule count, keywords per rule, and keyword length, and preferably precompile lexical matchers when the router is initialized.
…ng for the complexity router Add deterministic keyword-to-tier overrides and optional embedding-based (semantic) keyword matching to the complexity router, and surface both in the Add Auto Router UI behind a Router Type selector: "Auto-Router v2 [Recommended]" (complexity tiers + keyword overrides + semantic matching, the default) and "Semantic Router [to be deprecated]" (the existing utterance-based router, unchanged). Keyword-to-tier overrides resolve to the highest tier matched rather than the first keyword matched, so match order no longer affects the routing decision. Backend: - config: KeywordTierRule model plus keyword_tier_rules, semantic_keyword_matching, embedding_model, and match_threshold on ComplexityRouterConfig, with a validator requiring an embedding model and rules when semantic matching is on - complexity_router: evaluate keyword rules before scoring; lexical matches escalate to the most-severe matched tier (order-independent), and semantic mode reuses LiteLLMRouterEncoder + SemanticRouter to match paraphrases by cosine similarity, falling back to the scorer when nothing matches - model management: clear complexity_routers on cache reload so config edits take effect Frontend: - Add Auto Router tab restores the Router Type radio (Auto-Router v2 recommended by default, Semantic Router still available) and sends keyword_tier_rules plus the semantic settings on the recommended path, instead of flattening keywords into custom_technical_keywords - client-side guard blocks submit when semantic matching is enabled without an embedding model or without any keyword tier rules, mirroring the backend validator - moved the "How Classification Works" explainer below Custom Technical Keywords and above Keyword Tier Overrides - remove the Test Connection action from the recommended flow, which can't build a valid pre-save payload for a router (leaves a TODO for a JSON preview / config test follow-up) Tests cover lexical escalation, semantic matching via the real library with injected embeddings, the semantic config guard, config validation, the reload-clear regression, and the frontend payload builder
664eab7 to
3c714ed
Compare
92dfbdb
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Captured at commit
5ae72c8676against real Anthropic and Voyage APIs (no mocks)Lexical keyword tier overrides on a router with rules
hi->SIMPLE andbeep->COMPLEX (semantic off). Each keyword routes to its tier, and a prompt matching both escalates to the most-severe tier rather than the first rule in the listSemantic keyword matching on a router with
semantic_keyword_matching: true,embedding_model: voyage-3-5,match_threshold: 0.5. Paraphrases with no literal keyword still route via embeddingsCommand shape used for each:
Tested end-to-end from Claude Code pointed at the router as its default model (
ANTHROPIC_BASE_URL-> the proxy,ANTHROPIC_MODEL=smart-router); requests reached the proxy over the Anthropic Messages API (/v1/messages, including streaming) and were classified and routed correctly.UI: Models + Endpoints -> Add Model -> Add Auto Router, Router Type set to "Auto-Router v2 [Recommended]" (default)
Type
🆕 New Feature
Changes
Adds two capabilities to the complexity auto-router and surfaces them in the Add Auto Router UI behind a Router Type selector
Backend introduces
keyword_tier_rules(deterministic keyword-to-tier overrides evaluated before the weighted scorer) and optionalsemantic_keyword_matchingbacked by an embedding model and match threshold. When multiple lexical rules match a prompt, routing escalates to the most-severe matched tier so behavior no longer depends on the order rules were authored in. Semantic mode reuses the existingLiteLLMRouterEncoderandSemanticRoutermachinery to match paraphrases by cosine similarity and falls back to the scorer when nothing clears the threshold. The cache-reload path now clearscomplexity_routersalongsideauto_routersso config edits take effect without a restartFrontend restores the Router Type radio: "Auto-Router v2 [Recommended]" is the default and sends
keyword_tier_rulesplus the semantic settings directly instead of flattening rule keywords intocustom_technical_keywords; "Semantic Router [to be deprecated]" keeps the existing utterance-based flow unchanged. A client-side guard blocks submitting semantic matching without an embedding model or without any keyword tier rules, mirroring the backend validator. The "How Classification Works" explainer moved below Custom Technical Keywords and above Keyword Tier Overrides. The Test Connection action is dropped from the recommended flow since it cannot build a valid pre-save payload for a router (a TODO is left for a future JSON preview or config-test action)Tests cover lexical escalation, semantic matching through the real library with injected embeddings, the semantic config guard, config validation, the reload-clear regression, and the frontend payload builder