Troubleshooting
On this page
ImportError: sentence-transformers is required
Raised from EmbeddingProvider.__init__, which HierarchicalRouter() calls immediately at construction. Install it:
pip install sentence-transformers
ValueError: Query cannot be empty.
route_query rejects empty/whitespace-only queries before doing any work. Validate input at your application boundary before calling route_query.
ValueError: Group '<name>' already registered. Use update_group() to replace it.
register_group refuses to silently overwrite an existing group. If you intend to replace it (e.g. re-registering after editing routes in bulk), use router.update_group(group) instead — it also clears the decision cache automatically.
ValueError: Route '<name>' already exists in group '<group>'. Use update_route() to replace it.
Same pattern at the route level — use group.update_route(route) for an intentional replace, or group.remove_route(name) first if you want a hard reset.
RuntimeError: Route '<name>' is disabled.
Raised by Route.execute/execute_async if a disabled route is executed directly. Under normal route_query flow this shouldn't surface (the cache-hit path checks route.enabled before executing, and disabled routes are excluded from RoutingPipeline's candidate collection via get_routes(enabled_only=True)) — if you see this, you're likely calling route.execute(...) directly on a route you've disabled, or executing a route object you hold a stale reference to. Call route.enable() first, or don't disable routes you intend to execute manually.
LookupError: No route matched query (top score: ...)
Only raised if RouterConfig.raise_on_no_match=True and no fallback handler is set. Either lower ScoringConfig.fallback_confidence_threshold, add more/better examples to your routes, set a fallback handler via router.set_fallback(...), or catch this exception at your call site if raising is intentional.
ZeroDivisionError when constructing HierarchicalRouter or HybridScorer
HybridScorer.__init__ divides each weight by semantic_weight + keyword_weight + llm_weight (with llm_weight forced to 0 if no LLMScorer is configured). If you've set ScoringConfig with all relevant weights at 0, this raises. Ensure at least one of semantic_weight/keyword_weight (and llm_weight, if you've configured an LLM model) is nonzero.
Routing seems inconsistent between runs / different from what I expect
- Check whether the decision cache is serving a stale answer:
router.cache_stats(), or callrouter.get_route(name)and inspectscore_bias— feedback-driven bias changes scoring results over time even for the same query. - Confirm you're comparing against the same
RouterConfig— thresholds, weights, andtop_kall materially change outcomes. - If comparing sync (
route_query) vs async (route_async), be aware of the documented async/sync asymmetries (no_post_routeon async cache hits, no retries on async execution).
LLM fallback scoring never seems to run
- Confirm
RouterConfig.llm_modelis set — without it, noLLMScoreris constructed at all, regardless ofScoringConfig.llm_weight. - Confirm
openaiis actually installed and importable —LLMScorer._build_clientsilently swallowsImportErrorand leaves the scorer as a permanent no-op (self._client = None) rather than raising. - The LLM scorer is only invoked when the best raw semantic+keyword score across all candidates is below
ScoringConfig.fallback_confidence_threshold— if your semantic/keyword signals are already strong, the LLM tier is deliberately skipped to save cost.
Async routing doesn't retry failed handlers
This is expected behavior in the analyzed source — _execute_single_async/_execute_sequential_async/_execute_parallel_async do not wrap calls in _with_retry. Only the synchronous route_query/route() path retries. See ExecutionEngine.
Streaming yields "[ERROR] No route candidates found." or "[ERROR] Route not found."
These are literal yielded strings (not exceptions) from ExecutionEngine.stream/stream_async, signaling that no candidate cleared confidence, or the top candidate's route was somehow removed from route_map between selection and execution. Check for this [ERROR] prefix in your streaming consumer if you need to distinguish it from genuine handler output.
RouterStreamExecutor always falls back / never finds a handler when wired to HierarchicalRouter
HierarchicalRouter.route() returns a RoutingResult, which does not expose .handler or .best — RouterStreamExecutor._get_handler expects one of those (or a dict with a "handler" key). See the detailed compatibility warning in RouterStreamExecutor. You'll need a thin adapter object between HierarchicalRouter and RouterStreamExecutor, or use HierarchicalRouter.stream/stream_async directly instead.
Embedding cache / memory grows unbounded over a long-running process
EmbeddingProvider's internal embedding cache (distinct from CacheManager's route-decision cache) has no TTL or size limit — it only clears via an explicit embedder.clear_cache() call. Monitor embedder.cache_size (accessible via the property, though HierarchicalRouter doesn't expose this directly — you'd access router._embedder.cache_size) and clear periodically if this matters for your deployment.
If this page doesn't cover your issue, check the relevant API Reference page for the component involved — many pages include explicit warnings about behaviors that are easy to misread from the docstrings alone.