Skip to content

fix(distributed): backend discovery hid worker-installed backends behind the controller's filesystem#10967

Merged
mudler merged 1 commit into
masterfrom
fix/installed-state-cluster-aware
Jul 19, 2026
Merged

fix(distributed): backend discovery hid worker-installed backends behind the controller's filesystem#10967
mudler merged 1 commit into
masterfrom
fix/installed-state-cluster-aware

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

On a distributed controller, backend discovery filters on installed-state derived from the controller's own filesystem. A backend lives on the worker node that runs it, so anything an admin installed on a GPU worker reads as "not installed" and disappears from the listing.

Installed is set at core/gallery/gallery.go:552 via getGalleryElements's callback, which for backends is systemBackends.Exists(backend.GetName()) — a pure local directory scan.

Why this surfaces now

#10947 fixed the sibling filter on these same endpoints: capability was also evaluated against the controller, so GPU-only meta backends were dropped entirely on a GPU-less pod. That fix made this one sharper rather than causing it. Before, the backend was hidden by the capability filter; now it becomes listable and the installed-state filter drops it anyway, so the dropdown is still empty.

Two filters, one root cause, one previously fixed.

Fix

The controller cannot derive worker install state locally, but it already aggregates exactly this: nodes.DistributedBackendManager.ListBackends() fans out over NATS, skips pending/offline/draining nodes, and builds SystemBackend.Nodes[] with node_name/node_status — which is what GET /backends renders today. Discovery now reuses that active BackendManager rather than growing a second path.

Mirrors #10947's structure one-for-one: a ClusterInstalledProvider, a resolveClusterInstalled that degrades to local-only on a nil provider or an error, and routes.ClusterInstalledProviderFor(app) returning nil outside distributed mode.

Three surfaces shared the root cause and route through one helper now:

  • GET /backends/available (installed is now cluster-wide)
  • GET /api/fine-tuning/backends
  • GET /api/quantization/backends

Matching is on b.Name, identical to the existing local systemBackends.Exists(GetName()) semantics — no new matching rules introduced.

Response shape

Stays a boolean, not an installed-on-N-of-M count. The fine-tune and quantization responses carry no install field at all — they are dropdown feeds, where a count would be noise. /backends/available already exposes a boolean, and the authoritative per-node breakdown is one endpoint over at GET /backends nodes[], with node-level action at POST /api/nodes/:id/backends/install. A count would mean a schema change plus UI work across three surfaces to express something already available elsewhere.

Compatibility

A nil provider (single-node) leaves the local filesystem as the only source and reproduces today's listing exactly; a registry error degrades to that same listing rather than blanking the catalog or 500-ing, following the #10947 precedent. Both are covered by specs.

Contract audit across the whole repo including tests/e2e/ (no build tags there): the three endpoint constructors are referenced only from core/http/routes/{finetuning,quantization,localai}.go and the two #10947 test files. No e2e spec pins them; the React UI consumes them by URL only.

Testing

Red first, with providers accepted but ignored so the failures are behavioral rather than compile errors:

Summarizing 3 Failures:
  [FAIL] fine-tune backends [It] lists a backend installed only on a worker node
  [FAIL] quantization backends [It] lists a backend installed only on a worker node
  [FAIL] GET /backends/available [It] reports a worker-installed backend as installed
Ran 211 of 211 Specs — 208 Passed | 3 Failed

Green:

ok  core/http/endpoints/localai  25.997s
ok  core/http/routes              0.302s

Full pre-commit gate: make lint 0 issues; coverage-check: OK — coverage rose to 53.3% (baseline 48.5%). Baseline untouched.

Fixes #10953


🤖 Generated with Claude Code

Backend discovery endpoints filter on installed-state, which on a
distributed controller derives from the controller's own filesystem. A
backend lives on the worker node that runs it, so every backend an admin
installed on a GPU worker read as "not installed" and vanished from the
listing. #10947 fixed the sibling capability filter on the same endpoints,
so a fine-tuning-capable GPU worker now made the backend listable while
the installed-state filter still dropped it: the dropdown stayed empty.

The controller cannot derive this locally, but it already aggregates the
per-node view that GET /backends renders, so discovery reuses the active
BackendManager rather than growing a second path. Three surfaces shared the
root cause and route through the same helper now:

  - GET /backends/available (Installed is now cluster-wide)
  - GET /api/fine-tuning/backends
  - GET /api/quantization/backends

The response stays a boolean rather than an installed-on-N-of-M count:
per-node install state is already served by GET /backends nodes[], and
per-node control by POST /api/nodes/:id/backends/install, so a summary is
all these dropdowns need.

A nil provider (single-node) leaves the local filesystem as the only source
and reproduces today's listing exactly, and a registry error degrades to
that same listing instead of blanking the catalog.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 golangci-lint
@mudler
mudler merged commit 9d82c37 into master Jul 19, 2026
68 checks passed
@mudler
mudler deleted the fix/installed-state-cluster-aware branch July 19, 2026 23:09
mudler added a commit that referenced this pull request Jul 20, 2026
… coverage-ratchet fixes (#10989)

* fix(http): make /readyz reflect startup readiness instead of always 200

/readyz was registered as a static handler returning 200 unconditionally,
so it carried no information: it was green whenever it could be reached at
all. Readiness could not distinguish "serving" from "still starting", and
any future change that started the HTTP listener earlier would silently
turn the probe into a lie.

Track startup completion on the Application (atomic flag, flipped at the
very end of New() on the success path only) and have the readiness handler
consult it per request, returning 503 with a small JSON body while startup
is in progress. A nil readiness source fails open so embedders keep the
historical behaviour.

/healthz is deliberately left readiness-independent. Liveness and readiness
answer different questions, and failing liveness during a long preload makes
an orchestrator restart the pod mid-download so the preload never finishes.

This matters because since #10949 the startup preload materializes
HuggingFace artifacts for managed backends: tens of GB for a large model
(31 GB observed on a live cluster). Both probes stay in quietPaths and stay
exempt from auth.

Note the listener is still started only after New() returns, so today the
not-ready state is not observable over HTTP. Moving the listener earlier is
a separate, deliberate decision and is not made here.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]

* chore(gitignore): anchor the mock-backend pattern so its source dir is traversable

The bare `mock-backend` pattern matched the *directory*
tests/e2e/mock-backend/, not just the binary built into it. Git will not
descend into an ignored directory even for tracked files, so
`git add tests/e2e/mock-backend/main.go` required -f. This was hit while
working on #10970.

Anchor it to the artifact's full path. The built binary stays ignored (it is
also covered by tests/e2e/mock-backend/.gitignore) while the source directory
becomes traversable again.

Verified with `git check-ignore -v`: a new source file under
tests/e2e/mock-backend/ is no longer ignored, and the binary produced by
`make build-mock-backend` still is.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]

* chore(coverage): raise the coverage ratchet from 48.5% to 54.2%

The committed baseline had drifted well below reality: it still read 48.5%
while a full instrumented run measures 54.2%. A stale-low baseline makes the
gate meaningless — coverage could regress by more than 5 percentage points
and still pass.

Raising a ratchet is a deliberate act, not something to fold into an
unrelated fix, so it gets its own commit. The headroom was earned by tests
landed in #10946, #10947, #10948, #10949, #10956, #10967, #10968, #10970 and
#10975.

Measured with `make test-coverage` on this branch (the same instrumented run
`make test-coverage-baseline` uses: ginkgo over ./pkg and ./core plus the
in-process tests/e2e suite, --covermode=atomic, --coverpkg over core/... and
pkg/..., generated protobuf excluded). The run completed with exit 0 and zero
spec failures; the total was then written with the exact command the
test-coverage-baseline target uses:

  go tool cover -func=coverage/coverage.out \
    | awk '/^total:/{gsub(/%/,"",$NF); print $NF}' > coverage-baseline.txt

Verified afterwards with scripts/coverage-check.sh, which reports OK.

Note the measured figure includes the readiness specs added earlier on this
branch, so it is a demonstrated floor rather than an estimate.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
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.

distributed: backend list endpoints filter on installed-state from the controller's own filesystem

2 participants