fix(distributed): backend discovery hid worker-installed backends behind the controller's filesystem#10967
Merged
Merged
Conversation
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
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>
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.
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.
Installedis set atcore/gallery/gallery.go:552viagetGalleryElements's callback, which for backends issystemBackends.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 buildsSystemBackend.Nodes[]withnode_name/node_status— which is whatGET /backendsrenders today. Discovery now reuses that activeBackendManagerrather than growing a second path.Mirrors #10947's structure one-for-one: a
ClusterInstalledProvider, aresolveClusterInstalledthat degrades to local-only on a nil provider or an error, androutes.ClusterInstalledProviderFor(app)returning nil outside distributed mode.Three surfaces shared the root cause and route through one helper now:
GET /backends/available(installedis now cluster-wide)GET /api/fine-tuning/backendsGET /api/quantization/backendsMatching is on
b.Name, identical to the existing localsystemBackends.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/availablealready exposes a boolean, and the authoritative per-node breakdown is one endpoint over atGET /backendsnodes[], with node-level action atPOST /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 fromcore/http/routes/{finetuning,quantization,localai}.goand 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:
Green:
Full pre-commit gate:
make lint0 issues;coverage-check: OK — coverage rose to 53.3% (baseline 48.5%). Baseline untouched.Fixes #10953
🤖 Generated with Claude Code