Skip to content

feat(web): a deployment with no local runtime says so once, instead of warning forever - #963

Merged
baakhoff merged 2 commits into
mainfrom
feat/web-hosted-only-models
Sep 20, 2026
Merged

baakhoff merged 2 commits into
mainfrom
feat/web-hosted-only-models

Conversation

@baakhoff

Copy link
Copy Markdown
Owner

Part of #962Part 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 — and
the 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 a
runtime existed. Nothing is wrong on such a deployment, and there is nothing for the operator to
fix.

  • New useLocalRuntime() hook (src/lib/useLocalRuntime.ts) over
    GET /platform/v1/llm/local-runtime{state, url_configured}, with LocalRuntimeState /
    LocalRuntimeStatus in contracts.ts and api.localRuntime() in api.ts.
  • absent — the Models page's local half collapses into one line ("Local AI is not
    configured 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".
  • The embedding picker drops its Local (Ollama) optgroup, so a model that cannot run cannot be
    chosen, 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.
  • The chat picker drops the Local heading and the warning, and keeps the core default row
    ungrouped — that default may itself be hosted, and it is the only way back to it.
  • The first-run welcome stops telling a hosted-only deployment to pull a local model.
  • A module's model slot that has nothing left to offer says why (ModulesScreen) instead of
    showing 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 (with
    models.isError kept alongside it for an older core), because /llm/models answers [] with a
    200 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 unknown
    answer deliberately reads as ok: hiding the local half from a deployment that has a runtime
    would be the worse failure.

format.ts:49's "Local (Ollama)" label is only ever rendered for hosted-filtered lists; the
one local-labelled surface was the optgroup above.

One defect found while testing and fixed here: settled must latch (isFetched, not
!isPending). The components it gates subscribe to the same query, and an errored query is
permanently 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 by
a test that fails on the old expression.

Version bump

web 0.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-runtime and never from /llm/models),
ChatLocalRuntime.test.tsx (picker heading, core-default row, the unreachable warning with a
200 model list, the welcome copy), ModuleModelsLocalRuntime.test.tsx.

Ran locally, all green: npx tsc -b, npm run lint (0 errors; 11 pre-existing
react-refresh/only-export-components warnings, none in touched files), npm test105
files / 1177 tests passed
. Plus uv run pytest tests/test_check_docs_links.py tests/test_no_nul_bytes.py for 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 run
locally; 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 both
branches are combined:

  • the endpoint's real payload parses against LocalRuntimeStatus (state, url_configured) and
    the three state strings match the core's exactly;
  • GET /llm/models really does answer 200 + [] when absent/unreachable — the unreachable
    warning and the suppressed "None yet" line both depend on it;
  • a hosted-only boot shows the collapsed card and no warning anywhere on Models or Chat.

Docs

docs/user/configuration.md — a new "A deployment with no local AI" section: what the Models
page, 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.md are unchanged — the page is already listed. Lane A owns
docs/reference/, docs/services/ and docs/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.

NikolaI Baakh 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant