Skip to content

Commit 02bb715

Browse files
authored
fix(distributed): pass ExternalURI through NATS backend install (#9446)
When installing a backend with a custom OCI URI in distributed mode, the URI was captured in ManagementOp.ExternalURI by the HTTP handler but never forwarded to workers. BackendInstallRequest had no URI field, so workers fell through to the gallery lookup and failed with "no backend found with name <custom-name>". Add URI/Name/Alias fields to BackendInstallRequest and thread them from ManagementOp through DistributedBackendManager.InstallBackend() and the RemoteUnloaderAdapter. On the worker side, route to InstallExternalBackend when URI is set instead of InstallBackendFromGallery. Update all remaining InstallBackend call sites (UpgradeBackend, reconciler pending-op drain, router auto-install) to pass empty strings for the new params. Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Russell Sim <rsl@simopolis.xyz>
1 parent 8ab56e2 commit 02bb715

8 files changed

Lines changed: 32 additions & 15 deletions

File tree

core/cli/worker.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/mudler/LocalAI/core/cli/workerregistry"
2222
"github.com/mudler/LocalAI/core/config"
2323
"github.com/mudler/LocalAI/core/gallery"
24+
"github.com/mudler/LocalAI/core/services/galleryop"
2425
"github.com/mudler/LocalAI/core/services/messaging"
2526
"github.com/mudler/LocalAI/core/services/nodes"
2627
"github.com/mudler/LocalAI/core/services/storage"
@@ -597,12 +598,20 @@ func (s *backendSupervisor) installBackend(req messaging.BackendInstallRequest)
597598
// Try to find the backend binary
598599
backendPath := s.findBackend(req.Backend)
599600
if backendPath == "" {
600-
// Backend not found locally — try auto-installing from gallery
601-
xlog.Info("Backend not found locally, attempting gallery install", "backend", req.Backend)
602-
if err := gallery.InstallBackendFromGallery(
603-
context.Background(), galleries, s.systemState, s.ml, req.Backend, nil, false,
604-
); err != nil {
605-
return "", fmt.Errorf("installing backend from gallery: %w", err)
601+
if req.URI != "" {
602+
xlog.Info("Backend not found locally, attempting external install", "backend", req.Backend, "uri", req.URI)
603+
if err := galleryop.InstallExternalBackend(
604+
context.Background(), galleries, s.systemState, s.ml, nil, req.URI, req.Name, req.Alias,
605+
); err != nil {
606+
return "", fmt.Errorf("installing backend from gallery: %w", err)
607+
}
608+
} else {
609+
xlog.Info("Backend not found locally, attempting gallery install", "backend", req.Backend)
610+
if err := gallery.InstallBackendFromGallery(
611+
context.Background(), galleries, s.systemState, s.ml, req.Backend, nil, false,
612+
); err != nil {
613+
return "", fmt.Errorf("installing backend from gallery: %w", err)
614+
}
606615
}
607616
// Re-register after install and retry
608617
gallery.RegisterBackends(s.systemState, s.ml)

core/http/endpoints/localai/nodes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ func InstallBackendOnNodeEndpoint(unloader nodes.NodeCommandSender) echo.Handler
376376
if err := c.Bind(&req); err != nil || req.Backend == "" {
377377
return c.JSON(http.StatusBadRequest, nodeError(http.StatusBadRequest, "backend name required"))
378378
}
379-
reply, err := unloader.InstallBackend(nodeID, req.Backend, "", req.BackendGalleries)
379+
reply, err := unloader.InstallBackend(nodeID, req.Backend, "", req.BackendGalleries, "", "", "")
380380
if err != nil {
381381
xlog.Error("Failed to install backend on node", "node", nodeID, "backend", req.Backend, "error", err)
382382
return c.JSON(http.StatusInternalServerError, nodeError(http.StatusInternalServerError, "failed to install backend on node"))

core/services/messaging/subjects.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,13 @@ func SubjectNodeBackendInstall(nodeID string) string {
124124
// BackendInstallRequest is the payload for a backend.install NATS request.
125125
type BackendInstallRequest struct {
126126
Backend string `json:"backend"`
127-
ModelID string `json:"model_id,omitempty"` // unique model identifier — each model gets its own gRPC process
127+
ModelID string `json:"model_id,omitempty"`
128128
BackendGalleries string `json:"backend_galleries,omitempty"`
129+
// URI is set for external installs (OCI image, URL, or path). When non-empty
130+
// the worker routes to InstallExternalBackend instead of the gallery lookup.
131+
URI string `json:"uri,omitempty"`
132+
Name string `json:"name,omitempty"`
133+
Alias string `json:"alias,omitempty"`
129134
}
130135

131136
// BackendInstallReply is the response from a backend.install NATS request.

core/services/nodes/managers_distributed.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ func (d *DistributedBackendManager) InstallBackend(ctx context.Context, op *gall
293293
backendName := op.GalleryElementName
294294

295295
_, err := d.enqueueAndDrainBackendOp(ctx, OpBackendInstall, backendName, galleriesJSON, func(node BackendNode) error {
296-
reply, err := d.adapter.InstallBackend(node.ID, backendName, "", string(galleriesJSON))
296+
reply, err := d.adapter.InstallBackend(node.ID, backendName, "", string(galleriesJSON), op.ExternalURI, op.ExternalName, op.ExternalAlias)
297297
if err != nil {
298298
return err
299299
}
@@ -311,7 +311,7 @@ func (d *DistributedBackendManager) UpgradeBackend(ctx context.Context, name str
311311
galleriesJSON, _ := json.Marshal(d.backendGalleries)
312312

313313
_, err := d.enqueueAndDrainBackendOp(ctx, OpBackendUpgrade, name, galleriesJSON, func(node BackendNode) error {
314-
reply, err := d.adapter.InstallBackend(node.ID, name, "", string(galleriesJSON))
314+
reply, err := d.adapter.InstallBackend(node.ID, name, "", string(galleriesJSON), "", "", "")
315315
if err != nil {
316316
return err
317317
}

core/services/nodes/reconciler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func (rc *ReplicaReconciler) drainPendingBackendOps(ctx context.Context) {
188188
case OpBackendDelete:
189189
_, applyErr = rc.adapter.DeleteBackend(op.NodeID, op.Backend)
190190
case OpBackendInstall, OpBackendUpgrade:
191-
reply, err := rc.adapter.InstallBackend(op.NodeID, op.Backend, "", string(op.Galleries))
191+
reply, err := rc.adapter.InstallBackend(op.NodeID, op.Backend, "", string(op.Galleries), "", "", "")
192192
if err != nil {
193193
applyErr = err
194194
} else if !reply.Success {

core/services/nodes/router.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ func (r *SmartRouter) installBackendOnNode(ctx context.Context, node *BackendNod
504504
return "", fmt.Errorf("no NATS connection for backend installation")
505505
}
506506

507-
reply, err := r.unloader.InstallBackend(node.ID, backendType, modelID, r.galleriesJSON)
507+
reply, err := r.unloader.InstallBackend(node.ID, backendType, modelID, r.galleriesJSON, "", "", "")
508508
if err != nil {
509509
return "", err
510510
}

core/services/nodes/router_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ type fakeUnloader struct {
244244
unloadErr error
245245
}
246246

247-
func (f *fakeUnloader) InstallBackend(_, _, _, _ string) (*messaging.BackendInstallReply, error) {
247+
func (f *fakeUnloader) InstallBackend(_, _, _, _, _, _, _ string) (*messaging.BackendInstallReply, error) {
248248
return f.installReply, f.installErr
249249
}
250250

core/services/nodes/unloader.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type backendStopRequest struct {
1717
// NodeCommandSender abstracts NATS-based commands to worker nodes.
1818
// Used by HTTP endpoint handlers to avoid coupling to the concrete RemoteUnloaderAdapter.
1919
type NodeCommandSender interface {
20-
InstallBackend(nodeID, backendType, modelID, galleriesJSON string) (*messaging.BackendInstallReply, error)
20+
InstallBackend(nodeID, backendType, modelID, galleriesJSON, uri, name, alias string) (*messaging.BackendInstallReply, error)
2121
DeleteBackend(nodeID, backendName string) (*messaging.BackendDeleteReply, error)
2222
ListBackends(nodeID string) (*messaging.BackendListReply, error)
2323
StopBackend(nodeID, backend string) error
@@ -72,14 +72,17 @@ func (a *RemoteUnloaderAdapter) UnloadRemoteModel(modelName string) error {
7272
// The worker installs the backend from gallery (if not already installed),
7373
// starts the gRPC process, and replies when ready.
7474
// Timeout: 5 minutes (gallery install can take a while).
75-
func (a *RemoteUnloaderAdapter) InstallBackend(nodeID, backendType, modelID, galleriesJSON string) (*messaging.BackendInstallReply, error) {
75+
func (a *RemoteUnloaderAdapter) InstallBackend(nodeID, backendType, modelID, galleriesJSON, uri, name, alias string) (*messaging.BackendInstallReply, error) {
7676
subject := messaging.SubjectNodeBackendInstall(nodeID)
7777
xlog.Info("Sending NATS backend.install", "nodeID", nodeID, "backend", backendType, "modelID", modelID)
7878

7979
return messaging.RequestJSON[messaging.BackendInstallRequest, messaging.BackendInstallReply](a.nats, subject, messaging.BackendInstallRequest{
8080
Backend: backendType,
8181
ModelID: modelID,
8282
BackendGalleries: galleriesJSON,
83+
URI: uri,
84+
Name: name,
85+
Alias: alias,
8386
}, 5*time.Minute)
8487
}
8588

0 commit comments

Comments
 (0)