# Reliability: don't let a bad LLM take mcpctl down The homelab model changes often (fast ↔ thinking, model swaps, backends that drift or go down). mcpctl must stay responsive and honest through all of it. ## Principle **LLM-*optional* operations must be time-bounded, fall back deterministically, and report the degradation — never hang and never degrade silently.** - **Bounded:** every optional LLM call is wrapped in [`withTimeout`](../src/mcplocal/src/util/with-timeout.ts) (Promise.race + an `AbortSignal` so fetch-based providers actually cancel). A thinking model that streams for minutes can never block the caller. - **Deterministic fallback:** when the LLM times out or errors, use the non-LLM path (priority/keyword ordering, byte-range pages). - **Loud, not silent:** log the reason (`[gate] …`, `[pagination] …`) and tell the user. `begin_session` prepends `⚠ Smart prompt-selection unavailable ()…` and sets `degraded: true` + `degradedReason` on the audit `gate_decision` event. Applied in: the gate's `begin_session` prompt selection (`proxymodel/plugins/gate.ts`, cap `MCPCTL_GATE_LLM_TIMEOUT_MS`, default 8s) and pagination's smart index (`llm/pagination.ts`, `MCPCTL_PAGINATION_LLM_TIMEOUT_MS`, default 10s). `read_prompts` is LLM-free by design. Note: the gate's prompt-ranking uses the **heavy client provider's own model** — it deliberately does *not* force the project's vLLM model onto it (doing so made every selection fail silently when the model wasn't anthropic-servable). ## LLM-*essential* operations Chat needs the LLM — it can't fall back. It must **fail fast with a clear, actionable message** instead of a vague one. Chat surfaces the upstream status + body (which names the model + reason), e.g. `LLM returned no completion (HTTP 400): {"error":"model deepseek-v4-flash not found"}`, rather than "Adapter returned no choice".