feat(web): a deployment with no local runtime says so once, instead of warning forever - #963
Merged
Merged
Conversation
added 2 commits
September 20, 2026 08:04
…f warning forever The shell had two inline strings for a dead local runtime and rendered every other Ollama control — the pull card, the catalog, the KV-cache card, the context-window card, the `Local (Ollama)` optgroup — as though a runtime always existed. On a deployment that deliberately runs none, "is the ollama service up?" describes a fault that is not there and cannot be fixed. The state now comes from `GET /platform/v1/llm/local-runtime` and never from a failing model list: that query answers `[]` with a 200 in both unhappy states, so its failure no longer carries the information. When the state is `absent` the local half of the Models page collapses into one line, the five Ollama cards are removed rather than disabled, the embedding picker drops its local group so an unrunnable model cannot be chosen, the chat picker drops its Local heading while keeping the core-default row, the first-run welcome stops offering a pull, and a module model slot with nothing to offer says why. `unreachable` keeps today's warning; an older core without the endpoint keeps today's behaviour exactly. Part of #962.
The seam this lane could not exercise: every local-runtime test here mocks `api.localRuntime()`, so `LocalRuntimeStatus` — the zod schema the real response is parsed through — was never run on a single payload. A drift in either direction would be invisible, and invisible in the worst way: the hook turns a parse failure into `ok`, so a hosted-only deployment would quietly go back to rendering the full local half of the Models page instead of collapsing it, with every suite green. Three bodies, copied verbatim from the core's own route assertions in `services/core-app/tests/test_llm_routes.py` (and confirmed against the running endpoint during the merge-set review), plus the negative: an unrecognised state must throw rather than widen the enum, because falling back to today's behaviour is the deliberate answer for a state the shell has not been taught. No production change. `web` stays 0.151.0. Part of #962.
baakhoff
force-pushed
the
feat/web-hosted-only-models
branch
from
September 20, 2026 08:04
4a7914b to
baaa490
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #962 — Part 2 (web). Lane A ships the core contract and the deployment surface in
parallel; this branch builds against the contract as specified in the issue.
What changed
OLLAMA_URL=""is a supported deployment mode — hosted chat, hosted embeddings, no Ollama — andthe shell treated it as a fault. Two inline strings said "The local runtime is unreachable — is
the ollama service up?" / "local runtime unreachable", while the pull card, the catalog, the
KV-cache card, the context-window card and the
Local (Ollama)optgroup rendered as though aruntime existed. Nothing is wrong on such a deployment, and there is nothing for the operator to
fix.
useLocalRuntime()hook (src/lib/useLocalRuntime.ts) overGET /platform/v1/llm/local-runtime→{state, url_configured}, withLocalRuntimeState/LocalRuntimeStatusincontracts.tsandapi.localRuntime()inapi.ts.absent— the Models page's local half collapses into one line ("Local AI is notconfigured on this deployment … hosted models are unaffected"). The catalog, the download tray,
the local model list, the context-window card and the KV-cache card are removed from the
render, not disabled: a disabled control still reads as "something here is broken".
Local (Ollama)optgroup, so a model that cannot run cannot bechosen, and its help text names the consequence (every option is hosted → the corpus leaves the
machine whichever you pick). Its Settings button is withheld for a stale local id, whose
sheet is all Ollama knobs.
Localheading and the warning, and keeps the core default rowungrouped — that default may itself be hosted, and it is the only way back to it.
ModulesScreen) instead ofshowing a lone "Core default" and looking broken. The two ungrouped model selects
(
AutomationsScreen,SuggestionReviewModal) already read correctly with an empty local list —"Core default" plus a Hosted optgroup — and are unchanged.
unreachable— today's warning, unchanged. Now driven by the runtime's own state (withmodels.isErrorkept alongside it for an older core), because/llm/modelsanswers[]with a200 in that state after core-app,web,infra: a hosted-only deployment runs no local runtime — the chart refuses it, the core 500s in it #962 and would otherwise never warn again. The "None yet. Pull one
above" line is suppressed in that state, where an empty 200 used to be impossible.
ok, and an older core that 404s the endpoint — today's behaviour, unchanged. An unknownanswer deliberately reads as
ok: hiding the local half from a deployment that has a runtimewould be the worse failure.
format.ts:49's"Local (Ollama)"label is only ever rendered for hosted-filtered lists; theone local-labelled surface was the optgroup above.
One defect found while testing and fixed here:
settledmust latch (isFetched, not!isPending). The components it gates subscribe to the same query, and an errored query ispermanently stale — so each newly-mounted subscriber refetched, flipped the query back to
pending, and unmounted itself again. A page that oscillates forever on an older core. Pinned bya test that fails on the old expression.
Version bump
web0.150.0→0.151.0 (MINOR) — user-visible change.package.json+package-lock.json.Tests
New:
useLocalRuntime.test.tsx(three states, the older-core fallback, the latch guard),ModelsScreenLocalRuntime.test.tsx(the three cases explicitly, the optgroup in both directions,and four pins that the state comes from
/llm/local-runtimeand never from/llm/models),ChatLocalRuntime.test.tsx(picker heading, core-default row, the unreachable warning with a200 model list, the welcome copy),
ModuleModelsLocalRuntime.test.tsx.Ran locally, all green:
npx tsc -b,npm run lint(0 errors; 11 pre-existingreact-refresh/only-export-componentswarnings, none in touched files),npm test— 105files / 1177 tests passed. Plus
uv run pytest tests/test_check_docs_links.py tests/test_no_nul_bytes.pyfor the docs edit.No test exclusions were needed — the web suite is jsdom-only, no docker-dependent file was
skipped. Per the docker-light rule no
task smoke, no compose boot, no image build was runlocally; the hosted gates are authoritative.
Covered only by mocks
This branch cannot exercise the real endpoint — lane A ships it — so every behaviour above is
driven by a mocked
api.localRuntime(). What the merge-set review should verify once bothbranches are combined:
LocalRuntimeStatus(state,url_configured) andthe three state strings match the core's exactly;
GET /llm/modelsreally does answer 200 +[]when absent/unreachable — the unreachablewarning and the suppressed "None yet" line both depend on it;
Docs
docs/user/configuration.md— a new "A deployment with no local AI" section: what the Modelspage, the embedding picker, the chat picker and a module's model slots show, and the difference
between "not configured" (nothing is wrong) and "unreachable" (something is down). No new page,
so
docs/index.md/docs/_Sidebar.mdare unchanged — the page is already listed. Lane A ownsdocs/reference/,docs/services/anddocs/infrastructure/; nothing here touches them.Kubernetes parity
None to do — this is core shell rendering, identical on Docker and Kubernetes. The state is
read over the platform API from the browser; no container runtime is involved on either arm. The
parity that matters is lane A's (the ADR-0134 seam and the chart), and the collapsed page is what
a hosted-only deployment shows on both.