mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-02 12:26:49 -04:00
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>
This commit is contained in:
@@ -369,7 +369,7 @@ func New(opts ...config.AppOption) (*Application, error) {
|
||||
}
|
||||
|
||||
for _, backend := range options.ExternalBackends {
|
||||
if err := galleryop.InstallExternalBackend(options.Context, options.BackendGalleries, options.SystemState, application.ModelLoader(), nil, backend, "", "", options.RequireBackendIntegrity); err != nil {
|
||||
if err := galleryop.InstallExternalBackend(options.Context, options.BackendGalleries, options.SystemState, application.ModelLoader(), nil, backend, "", "", false, options.RequireBackendIntegrity); err != nil {
|
||||
xlog.Error("error installing external backend", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (bi *BackendsInstall) Run(ctx *cliContext.Context) error {
|
||||
}
|
||||
|
||||
modelLoader := model.NewModelLoader(systemState)
|
||||
err = galleryop.InstallExternalBackend(context.Background(), galleries, systemState, modelLoader, progressCallback, bi.BackendArgs, bi.Name, bi.Alias, bi.RequireBackendIntegrity)
|
||||
err = galleryop.InstallExternalBackend(context.Background(), galleries, systemState, modelLoader, progressCallback, bi.BackendArgs, bi.Name, bi.Alias, false, bi.RequireBackendIntegrity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ var _ = Describe("LocalBackendManager force semantics", func() {
|
||||
backendsDir string
|
||||
srcDir string
|
||||
mgr *galleryop.LocalBackendManager
|
||||
systemState *system.SystemState
|
||||
ml *model.ModelLoader
|
||||
)
|
||||
|
||||
const installedRunSh = "#!/bin/sh\necho installed\n"
|
||||
@@ -56,13 +58,14 @@ var _ = Describe("LocalBackendManager force semantics", func() {
|
||||
Expect(os.MkdirAll(filepath.Join(backendsDir, "test-backend"), 0o755)).To(Succeed())
|
||||
Expect(os.WriteFile(installedRunShPath(), []byte(installedRunSh), 0o755)).To(Succeed())
|
||||
|
||||
systemState, err := system.GetSystemState(system.WithBackendPath(backendsDir))
|
||||
systemState, err = system.GetSystemState(system.WithBackendPath(backendsDir))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
appConfig := &config.ApplicationConfig{
|
||||
SystemState: systemState,
|
||||
BackendGalleries: []config.Gallery{{Name: "test", URL: "file://" + galleryYAML}},
|
||||
}
|
||||
mgr = galleryop.NewLocalBackendManager(appConfig, model.NewModelLoader(systemState))
|
||||
ml = model.NewModelLoader(systemState)
|
||||
mgr = galleryop.NewLocalBackendManager(appConfig, ml)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
@@ -94,4 +97,18 @@ var _ = Describe("LocalBackendManager force semantics", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(content)).To(Equal(galleryRunSh), "install with Force must overwrite the installed backend")
|
||||
})
|
||||
|
||||
// The LOCALAI_EXTERNAL_BACKENDS boot loop goes through
|
||||
// InstallExternalBackend's gallery-name path on EVERY startup; it must not
|
||||
// force, or each boot re-downloads every listed backend.
|
||||
It("skips an already-installed backend on the non-forced external gallery-name path", func() {
|
||||
err := galleryop.InstallExternalBackend(context.Background(),
|
||||
[]config.Gallery{{Name: "test", URL: "file://" + filepath.Join(backendsDir, "gallery.yaml")}},
|
||||
systemState, ml, nil, "test-backend", "", "", false, false)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
content, err := os.ReadFile(installedRunShPath())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(content)).To(Equal(installedRunSh), "non-forced external install must not overwrite an installed backend")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,7 +144,12 @@ func (g *GalleryService) backendHandler(op *ManagementOp[gallery.GalleryBackend,
|
||||
// InstallExternalBackend installs a backend from an external source (OCI image, URL, or path).
|
||||
// This method contains the logic to detect the input type and call the appropriate installation function.
|
||||
// It can be used by both CLI and Web UI for installing backends from external sources.
|
||||
func InstallExternalBackend(ctx context.Context, galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, downloadStatus func(string, string, string, float64), backend, name, alias string, requireIntegrity bool) error {
|
||||
//
|
||||
// force applies only to the gallery-name fallback: a URI install (dir/OCI/file)
|
||||
// always writes, but a bare gallery name is an "ensure installed" — the
|
||||
// LOCALAI_EXTERNAL_BACKENDS boot loop runs it on every start and must not
|
||||
// re-download an installed, runnable backend.
|
||||
func InstallExternalBackend(ctx context.Context, galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, downloadStatus func(string, string, string, float64), backend, name, alias string, force, requireIntegrity bool) error {
|
||||
uri := downloader.URI(backend)
|
||||
switch {
|
||||
case uri.LooksLikeDir():
|
||||
@@ -202,7 +207,7 @@ func InstallExternalBackend(ctx context.Context, galleries []config.Gallery, sys
|
||||
if name != "" || alias != "" {
|
||||
return fmt.Errorf("specifying a name or alias is not supported for gallery backends")
|
||||
}
|
||||
err := gallery.InstallBackendFromGallery(ctx, galleries, systemState, modelLoader, backend, downloadStatus, true, requireIntegrity)
|
||||
err := gallery.InstallBackendFromGallery(ctx, galleries, systemState, modelLoader, backend, downloadStatus, force, requireIntegrity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error installing backend %s: %w", backend, err)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ var _ = Describe("InstallExternalBackend", func() {
|
||||
"test-backend", // gallery name
|
||||
"custom-name", // name should not be allowed
|
||||
"",
|
||||
false, // force
|
||||
false,
|
||||
)
|
||||
Expect(err).To(HaveOccurred())
|
||||
@@ -86,6 +87,7 @@ var _ = Describe("InstallExternalBackend", func() {
|
||||
"non-existent-backend",
|
||||
"",
|
||||
"",
|
||||
false, // force
|
||||
false,
|
||||
)
|
||||
Expect(err).To(HaveOccurred())
|
||||
@@ -103,6 +105,7 @@ var _ = Describe("InstallExternalBackend", func() {
|
||||
"oci://quay.io/mudler/tests:localai-backend-test",
|
||||
"", // name is required for OCI images
|
||||
"",
|
||||
false, // force
|
||||
false,
|
||||
)
|
||||
Expect(err).To(HaveOccurred())
|
||||
@@ -136,6 +139,7 @@ var _ = Describe("InstallExternalBackend", func() {
|
||||
testBackendPath,
|
||||
"", // name should be inferred as "source-backend"
|
||||
"",
|
||||
false, // force
|
||||
false,
|
||||
)
|
||||
// The function should at least attempt to install with the inferred name
|
||||
@@ -155,6 +159,7 @@ var _ = Describe("InstallExternalBackend", func() {
|
||||
testBackendPath,
|
||||
"custom-backend-name",
|
||||
"",
|
||||
false, // force
|
||||
false,
|
||||
)
|
||||
// The function should use the provided name
|
||||
@@ -173,6 +178,7 @@ var _ = Describe("InstallExternalBackend", func() {
|
||||
testBackendPath,
|
||||
"custom-backend-name",
|
||||
"custom-alias",
|
||||
false, // force
|
||||
false,
|
||||
)
|
||||
// The function should accept alias for directory paths
|
||||
|
||||
@@ -110,7 +110,7 @@ func (b *LocalBackendManager) CheckUpgrades(ctx context.Context) (map[string]gal
|
||||
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, b.requireBackendIntegrity)
|
||||
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
|
||||
|
||||
@@ -134,7 +134,7 @@ func (s *backendSupervisor) installBackend(req messaging.BackendInstallRequest,
|
||||
if req.URI != "" {
|
||||
xlog.Info("Installing backend from external URI", "backend", req.Backend, "uri", req.URI, "force", force)
|
||||
if err := galleryop.InstallExternalBackend(
|
||||
context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, s.cfg.RequireBackendIntegrity,
|
||||
context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, force, s.cfg.RequireBackendIntegrity,
|
||||
); err != nil {
|
||||
return "", fmt.Errorf("installing backend from gallery: %w", err)
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func (s *backendSupervisor) upgradeBackend(req messaging.BackendUpgradeRequest)
|
||||
if req.URI != "" {
|
||||
xlog.Info("Upgrading backend from external URI", "backend", req.Backend, "uri", req.URI)
|
||||
if err := galleryop.InstallExternalBackend(
|
||||
context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, s.cfg.RequireBackendIntegrity,
|
||||
context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, true, s.cfg.RequireBackendIntegrity,
|
||||
); err != nil {
|
||||
return fmt.Errorf("upgrading backend from external URI: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user