Troubleshooting

On this page

ImportError: sentence-transformers is required

Raised from EmbeddingProvider.__init__, which HierarchicalRouter() calls immediately at construction. Install it:

Bash
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 call router.get_route(name) and inspect score_bias — feedback-driven bias changes scoring results over time even for the same query.
  • Confirm you're comparing against the same RouterConfig — thresholds, weights, and top_k all materially change outcomes.
  • If comparing sync (route_query) vs async (route_async), be aware of the documented async/sync asymmetries (no _post_route on async cache hits, no retries on async execution).

LLM fallback scoring never seems to run

  • Confirm RouterConfig.llm_model is set — without it, no LLMScorer is constructed at all, regardless of ScoringConfig.llm_weight.
  • Confirm openai is actually installed and importable — LLMScorer._build_client silently swallows ImportError and 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.

Related pages