Files
LocalAI/core/services/galleryop/managers_local.go
LocalAI [bot] 6eea3ef2ac fix(backends): make backend install ops idempotent unless forced (#10643)
* fix(backends): make backend install ops idempotent unless forced

POST /backends/apply hardcoded force=true through
LocalBackendManager.InstallBackend, so applying an already-installed
backend re-downloaded and re-extracted the whole artifact every time.
API clients that ensure a backend exists at startup paid a full OCI
image pull on every boot.

Backend install ops now default to non-forced — an installed, runnable
backend short-circuits (the orphaned-meta reinstall path in
InstallBackendFromGallery is preserved) — and reinstall stays available:

- ManagementOp gains a Force field; the local manager passes it through
  instead of hardcoding true.
- /backends/apply accepts an optional "force" boolean in the body.
- The React UI install route keeps forcing, since its button doubles as
  the explicit "Reinstall backend" action.

Distributed installs already behaved this way (workers skip when the
binary exists unless force is set); this aligns single-node behavior.

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

* fix(backends): don't force-reinstall LOCALAI_EXTERNAL_BACKENDS on boot

The startup loop for LOCALAI_EXTERNAL_BACKENDS runs
InstallExternalBackend for each listed backend on every boot, and its
gallery-name path hardcoded force=true — so every start re-downloaded
and re-extracted each listed backend's OCI image even when it was
installed and runnable. Supervising apps that list several backends
paid several full OCI pulls per launch.

Give InstallExternalBackend an explicit force parameter (it only
affects the gallery-name fallback; URI installs always write) and pass:

- false from the boot loop and `local-ai backends install` (idempotent
  ensure — `backends upgrade` is the refresh path),
- op.Force from the local manager's external-URI op,
- the request's force on the worker install path and true on its
  upgrade path (behavior unchanged).

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-02 19:16:29 +02:00

123 lines
5.2 KiB
Go

package galleryop
import (
"context"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/gallery"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/system"
"github.com/mudler/xlog"
)
// LocalModelManager handles model install/delete on the local instance.
type LocalModelManager struct {
systemState *system.SystemState
modelLoader *model.ModelLoader
enforcePredownloadScans bool
automaticallyInstallBackend bool
requireBackendIntegrity bool
}
// NewLocalModelManager creates a LocalModelManager from the application config.
func NewLocalModelManager(appConfig *config.ApplicationConfig, ml *model.ModelLoader) *LocalModelManager {
return &LocalModelManager{
systemState: appConfig.SystemState,
modelLoader: ml,
enforcePredownloadScans: appConfig.EnforcePredownloadScans,
automaticallyInstallBackend: appConfig.AutoloadBackendGalleries,
requireBackendIntegrity: appConfig.RequireBackendIntegrity,
}
}
// SetAutoInstallBackend controls whether backend binaries are automatically
// installed when a model is installed. In distributed mode the frontend node
// disables this because backends only run on workers.
func (m *LocalModelManager) SetAutoInstallBackend(v bool) {
m.automaticallyInstallBackend = v
}
func (m *LocalModelManager) DeleteModel(name string) error {
if err := m.modelLoader.ShutdownModel(name); err != nil {
xlog.Warn("Failed to unload model during deletion", "model", name, "error", err)
}
return gallery.DeleteModelFromSystem(m.systemState, name)
}
func (m *LocalModelManager) InstallModel(ctx context.Context, op *ManagementOp[gallery.GalleryModel, gallery.ModelConfig], progressCb ProgressCallback) error {
switch {
case op.GalleryElement != nil:
installedModel, err := gallery.InstallModel(ctx, m.systemState, op.GalleryElement.Name,
op.GalleryElement, op.Req.Overrides, progressCb, m.enforcePredownloadScans)
if err != nil {
return err
}
if m.automaticallyInstallBackend && installedModel.Backend != "" {
xlog.Debug("Installing backend", "backend", installedModel.Backend)
return gallery.InstallBackendFromGallery(ctx, op.BackendGalleries, m.systemState,
m.modelLoader, installedModel.Backend, progressCb, false, m.requireBackendIntegrity)
}
return nil
case op.GalleryElementName != "":
return gallery.InstallModelFromGallery(ctx, op.Galleries, op.BackendGalleries,
m.systemState, m.modelLoader, op.GalleryElementName, op.Req, progressCb,
m.enforcePredownloadScans, m.automaticallyInstallBackend, m.requireBackendIntegrity)
default:
return installModelFromRemoteConfig(ctx, m.systemState, m.modelLoader, op.Req,
progressCb, m.enforcePredownloadScans, m.automaticallyInstallBackend, op.BackendGalleries, m.requireBackendIntegrity)
}
}
// LocalBackendManager handles backend install/delete on the local instance.
type LocalBackendManager struct {
systemState *system.SystemState
modelLoader *model.ModelLoader
backendGalleries []config.Gallery
requireBackendIntegrity bool
}
// NewLocalBackendManager creates a LocalBackendManager from the application config.
func NewLocalBackendManager(appConfig *config.ApplicationConfig, ml *model.ModelLoader) *LocalBackendManager {
return &LocalBackendManager{
systemState: appConfig.SystemState,
modelLoader: ml,
backendGalleries: appConfig.BackendGalleries,
requireBackendIntegrity: appConfig.RequireBackendIntegrity,
}
}
func (b *LocalBackendManager) DeleteBackend(name string) error {
err := gallery.DeleteBackendFromSystem(b.systemState, name)
b.modelLoader.DeleteExternalBackend(name)
return err
}
func (b *LocalBackendManager) ListBackends() (gallery.SystemBackends, error) {
return gallery.ListSystemBackends(b.systemState)
}
// UpgradeBackend ignores opID: a single-node install reports progress through
// the local progressCb already; opID only matters for distributed per-node
// streaming (see DistributedBackendManager.UpgradeBackend).
func (b *LocalBackendManager) UpgradeBackend(ctx context.Context, _ string, name string, progressCb ProgressCallback) error {
return gallery.UpgradeBackend(ctx, b.systemState, b.modelLoader, b.backendGalleries, name, progressCb, b.requireBackendIntegrity)
}
func (b *LocalBackendManager) CheckUpgrades(ctx context.Context) (map[string]gallery.UpgradeInfo, error) {
return gallery.CheckBackendUpgrades(ctx, b.backendGalleries, b.systemState)
}
func (b *LocalBackendManager) InstallBackend(ctx context.Context, op *ManagementOp[gallery.GalleryBackend, any], progressCb ProgressCallback) error {
if op.ExternalURI != "" {
return InstallExternalBackend(ctx, b.backendGalleries, b.systemState, b.modelLoader,
progressCb, op.ExternalURI, op.ExternalName, op.ExternalAlias, op.Force, b.requireBackendIntegrity)
}
// op.Force distinguishes an explicit reinstall from an idempotent
// "make sure it's installed" op; the latter must not re-download an
// already-runnable backend (supervisors apply on every boot).
return gallery.InstallBackendFromGallery(ctx, b.backendGalleries, b.systemState,
b.modelLoader, op.GalleryElementName, progressCb, op.Force, b.requireBackendIntegrity)
}
func (b *LocalBackendManager) IsDistributed() bool { return false }