Imported from sabadia/minerouter (
skills/minerouter-operations/SKILL.md). Install upstream withnpx skills add sabadia/minerouter --skill minerouter-operations. Copyright stays with the author.
MineRouter — Operations
Observability surfaces
GET /metrics— Prometheus. Key series (app/core/metrics.py):minerouter_requests_total{category,tier,cache}— request counts.minerouter_tier_resolutions_total{tier}— where routing resolved (embedding/reranker/llm/sticky/sticky_switch/vision/explicit_*/default).minerouter_cache_events_total{result}— cachehit/miss.minerouter_tokens_total{type,model,category,tier}— upstream tokens, attributable to the route that spent them. Cache hits are excluded (they replay already-counted usage).minerouter_cached_prompt_tokens_total{model}— prompt tokens served from the provider's prefix cache. Growth means prefix caching is working; a collapse means something broke it.minerouter_router_llm_tokens_total{type}— tokens spent by the routing classifier itself, kept separate so routing overhead is never mistaken for useful generation.minerouter_cost_usd_total{model,category}andminerouter_baseline_cost_usd_total{category}— actual spend vs. what the priciest model in the same pool would have cost. Requiresinput_cost_per_1k/output_cost_per_1kon the models.minerouter_upstream_attempts_total{model,outcome}— every attempt including retries and fallbacks. The gap versusminerouter_requests_totalis wasted upstream spend.minerouter_upstream_errors_total— failed dispatches surfaced to clients.minerouter_routing_seconds,minerouter_upstream_seconds— latency histograms.
GET /ready— strategy, embedding backend + dimension, rerank enabled, database connectivity, categories, models.- Logs — structured (JSON by default;
LOG_FORMAT=consolefor dev). Notable events:request_completed(one line per request: session, category, tier, model, tokens, cached tokens, cost, cache result, stream, latency),routing_decision,session_rerouted,context_ready,database_connect_failed/database_unavailable,redis_connected,routing_state_local_only,llm_router_failed,auth_open,vision_pool_unmarked,tool_pool_unmarked,tool_pool_empty,no_tool_capable_models,upstream_attempt_failed,dispatch_failed,stream_aborted. - Request log table (
request_logsin Postgres, if enabled) — per request: category, tier, model, strategy, score, tokens, cost, latency, cache_hit, stream. Streamed requests are now recorded too — usage is captured from the final stream chunk after the stream completes.
Tuning routing
Session affinity changes the trade-off. Classification now runs roughly once per
conversation, not once per turn, so escalating to a higher tier is cheap — while a wrong first
decision is expensive, because it sticks for session_affinity.ttl_seconds. The shipped thresholds
(0.38 / 0.08) are deliberately strict for this reason: buy accuracy on turn 1.
Use minerouter_tier_resolutions_total to see how often each tier fires:
- Healthy agent traffic is dominated by
sticky. Ifembedding/llmrivals it, conversations are not being recognised — check that clients keep a stable system prompt + first user message, or have them send an explicitX-MineRouter-Sessionheader. - Too many at
llm→ make surererank.enabled: true(free local tier-b absorbs most of these) andllm_router.memo_ttl_seconds > 0; then add clearer, more distinct categoryexamples. Lower the thresholds only ifminerouter_router_llm_tokens_totalis genuinely material. - Frequent
sticky_switch→ conversations are being re-pinned mid-flight, which costs a cold prefix cache each time. Raiseresticky_threshold. - Wrong categories chosen → improve/disambiguate
examples, or raise thresholds so ambiguous cases escalate to the reranker/LLM. - Everything at
default→ the LLM router is failing (see below) or thresholds are too strict.
Measuring token savings
Is routing actually saving money? Compare the two cost series (needs per-model pricing in
config.yml):
sum(rate(minerouter_cost_usd_total[1h])) / sum(rate(minerouter_baseline_cost_usd_total[1h]))
Below 1.0 means routing is cheaper than always using the priciest model in each pool.
Is the upstream prefix cache working? For agent workloads this dominates everything else:
sum(rate(minerouter_cached_prompt_tokens_total[1h])) / sum(rate(minerouter_tokens_total{type="prompt"}[1h]))
A collapse here means conversations are switching models mid-flight (check
minerouter_tier_resolutions_total{tier="sticky_switch"}) or session affinity is off.
What is routing overhead costing?
minerouter_router_llm_tokens_total ÷ minerouter_tokens_total — if this is not tiny, lower
escalation (see Tuning routing) or raise llm_router.memo_ttl_seconds.
Is retry fan-out burning tokens?
minerouter_upstream_attempts_total ÷ minerouter_requests_total should sit near 1.0. Well above
that means requests are re-sending the whole conversation across retries/fallbacks — lower
dispatch.num_retries / dispatch.max_fallbacks, or fix the failing provider.
minerouter_cache_events_total{result="hit"}÷ total = cache hit ratio (each hit is 0 upstream tokens). Note tool-calling and continuation turns are never cached by design.- Per-model/category cost analysis from
request_logs, e.g.:SELECT category, model_group, count(*), sum(total_tokens), sum(cost), avg(latency_ms) FROM request_logs GROUP BY 1,2 ORDER BY 3 DESC;
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
Everything routes to default/chat, tier default |
LLM router has no credentials (llm_router_failed in logs) → set ROUTER_API_KEY/ROUTER_API_BASE, or use strategy: embedding_only. |
| Coding/reasoning prompts misrouted | Category examples too sparse/overlapping → add distinct examples; tune high_confidence_threshold/ambiguity_margin. |
| Cache never hits | Category not cacheable; similarity_threshold too high; cache backend down (pg needs DB, redis needs Redis). |
| Cache returns wrong/stale answers | Lower ttl_seconds; enable verify_with_reranker (+ rerank.enabled) to reject false positives; raise similarity_threshold. |
| Vision request fails/ignored | routing.vision.enabled? vision category models supports_vision? (vision_pool_unmarked warning) — route to a real vision model. |
401 Unauthorized |
Missing/invalid Authorization: Bearer <key>; confirm the key is in server.api_keys. In Swagger /docs, click Authorize and paste the key. |
| Langfuse errors / requests failing when tracing on | The langfuse package (v2) is a bundled dependency; if it's missing, MineRouter logs langfuse_unavailable and disables tracing instead of breaking requests. Ensure langfuse>=2.6,<3 is installed (v3 is OTEL-based and incompatible with the classic callback). |
502 upstream error |
Provider/credentials/endpoint down; all fallbacks exhausted — check the model's api_base/api_key and provider status; minerouter_upstream_errors_total climbing. |
| DB features not working | database_unavailable/database_connect_failed → check DATABASE_URL + SSL; routing still works, logging/pg-cache degrade. |
| Token usage jumped after putting MineRouter in front of an agent | Check in order: (1) minerouter_cached_prompt_tokens_total flat → the upstream prefix cache is being broken. This is the usual cause: enable routing.session_affinity, and check for sticky_switch churn. (2) minerouter_upstream_attempts_total ≫ minerouter_requests_total → retry fan-out; lower dispatch.num_retries/max_fallbacks. (3) minerouter_router_llm_tokens_total material vs minerouter_tokens_total → escalation overhead; enable the reranker and the router memo. (4) The agent re-prompting because tools was dropped — see the row below. |
tools silently dropped, model replies in prose |
Only happens when LiteLLM knows the target model and knows it lacks tool support. Models it does not recognise (any openai/<name> against a custom api_base) keep tools untouched. Mark genuinely non-tool models supports_tools: false; dispatch.critical_params turns the remaining cases into a visible 502 instead of silent degradation. |
| Agent re-runs the same tool forever | A cached response containing tool_calls was replayed. Fixed by design now (tool/continuation turns are never cached, and the cache key includes a context_digest + category). If seen on an old deployment, clear semantic_cache. |
| Model changes mid-conversation | routing.session_affinity.enabled: false, Redis unreachable across replicas (routing_state_local_only — the in-process store is not shared), or resticky_threshold too low. Clients can pin explicitly with the X-MineRouter-Session header. |
| Streamed requests show no tokens | Upstream ignored stream_options.include_usage. MineRouter requests it automatically and strips the extra chunk unless the client asked for it. |
| High latency | Cloud embedding/reranker backends add network hops; reranker adds CPU; slow upstream. Prefer local fastembed; check minerouter_upstream_seconds. |
| Changed embedding model, weird cache/routing | Embedding dimension changed → stored pg/redis cache vectors are stale/incompatible; clear them and restart to rebuild prototypes. |
Graceful-degradation behaviour (by design)
- DB unreachable → gateway routes normally; request logging + pg cache are skipped.
- Redis unreachable → session affinity and the classification memo fall back to in-process stores
(
routing_state_local_only). Works single-process; not shared across replicas. - LLM router unreachable in
hybrid→ falls back to the best embedding guess, not a blind default. - Provider-unsupported params → dropped automatically (
dispatch.drop_unsupported_params), exceptdispatch.critical_params(tools,tool_choice,response_format), which raise aDispatchErrorinstead. Silently dropping those changes behaviour and costs the caller turns.
See also: minerouter-configuration (all tunables) and minerouter-deployment (probes, scaling).