mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-08 15:28:23 -04:00
* 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>
123 lines
5.2 KiB
Go
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 }
|