diff --git a/core/gallery/backend_types.go b/core/gallery/backend_types.go index 39167048..9c8bda93 100644 --- a/core/gallery/backend_types.go +++ b/core/gallery/backend_types.go @@ -2,6 +2,20 @@ package gallery import "github.com/mudler/LocalAI/core/config" +// BackendMetadata represents the metadata stored in a JSON file for each installed backend +type BackendMetadata struct { + // Alias is an optional alternative name for the backend + Alias string `json:"alias,omitempty"` + // MetaBackendFor points to the concrete backend if this is a meta backend + MetaBackendFor string `json:"meta_backend_for,omitempty"` + // Name is the original name from the gallery + Name string `json:"name,omitempty"` + // GalleryURL is the URL of the gallery this backend came from + GalleryURL string `json:"gallery_url,omitempty"` + // InstalledAt is the timestamp when the backend was installed + InstalledAt string `json:"installed_at,omitempty"` +} + type GalleryBackend struct { Metadata `json:",inline" yaml:",inline"` Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` diff --git a/core/gallery/backends.go b/core/gallery/backends.go index 8217fa4f..e3856ec6 100644 --- a/core/gallery/backends.go +++ b/core/gallery/backends.go @@ -1,9 +1,11 @@ package gallery import ( + "encoding/json" "fmt" "os" "path/filepath" + "time" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/system" @@ -13,11 +15,48 @@ import ( ) const ( - aliasFile = "alias" - metaFile = "meta" - runFile = "run.sh" + metadataFile = "metadata.json" + runFile = "run.sh" ) +// readBackendMetadata reads the metadata JSON file for a backend +func readBackendMetadata(backendPath string) (*BackendMetadata, error) { + metadataPath := filepath.Join(backendPath, metadataFile) + + // If metadata file doesn't exist, return nil (for backward compatibility) + if _, err := os.Stat(metadataPath); os.IsNotExist(err) { + return nil, nil + } + + data, err := os.ReadFile(metadataPath) + if err != nil { + return nil, fmt.Errorf("failed to read metadata file %q: %v", metadataPath, err) + } + + var metadata BackendMetadata + if err := json.Unmarshal(data, &metadata); err != nil { + return nil, fmt.Errorf("failed to unmarshal metadata file %q: %v", metadataPath, err) + } + + return &metadata, nil +} + +// writeBackendMetadata writes the metadata JSON file for a backend +func writeBackendMetadata(backendPath string, metadata *BackendMetadata) error { + metadataPath := filepath.Join(backendPath, metadataFile) + + data, err := json.MarshalIndent(metadata, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal metadata: %v", err) + } + + if err := os.WriteFile(metadataPath, data, 0644); err != nil { + return fmt.Errorf("failed to write metadata file %q: %v", metadataPath, err) + } + + return nil +} + func findBestBackendFromMeta(backend *GalleryBackend, systemState *system.SystemState, backends GalleryElements[*GalleryBackend]) *GalleryBackend { realBackend := backend.CapabilitiesMap[systemState.GPUVendor] if realBackend == "" { @@ -63,10 +102,16 @@ func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.S return fmt.Errorf("failed to create meta backend path %q: %v", metaBackendPath, err) } - // Then, let's create an meta file to point to the best backend - metaFile := filepath.Join(metaBackendPath, metaFile) - if err := os.WriteFile(metaFile, []byte(bestBackend.Name), 0644); err != nil { - return fmt.Errorf("failed to write meta file %q: %v", metaFile, err) + // Create metadata for the meta backend + metaMetadata := &BackendMetadata{ + MetaBackendFor: bestBackend.Name, + Name: name, + GalleryURL: backend.Gallery.URL, + InstalledAt: time.Now().Format(time.RFC3339), + } + + if err := writeBackendMetadata(metaBackendPath, metaMetadata); err != nil { + return fmt.Errorf("failed to write metadata for meta backend %q: %v", name, err) } return nil @@ -102,12 +147,19 @@ func InstallBackend(basePath string, config *GalleryBackend, downloadStatus func return fmt.Errorf("failed to extract image %q: %v", config.URI, err) } + // Create metadata for the backend + metadata := &BackendMetadata{ + Name: name, + GalleryURL: config.Gallery.URL, + InstalledAt: time.Now().Format(time.RFC3339), + } + if config.Alias != "" { - // Write an alias file inside - aliasFile := filepath.Join(backendPath, aliasFile) - if err := os.WriteFile(aliasFile, []byte(config.Alias), 0644); err != nil { - return fmt.Errorf("failed to write alias file %q: %v", aliasFile, err) - } + metadata.Alias = config.Alias + } + + if err := writeBackendMetadata(backendPath, metadata); err != nil { + return fmt.Errorf("failed to write metadata for backend %q: %v", name, err) } return nil @@ -124,37 +176,39 @@ func DeleteBackendFromSystem(basePath string, name string) error { if err != nil { return err } + foundBackend := false for _, backend := range backends { if backend.IsDir() { - aliasFile := filepath.Join(basePath, backend.Name(), aliasFile) - alias, err := os.ReadFile(aliasFile) + metadata, err := readBackendMetadata(filepath.Join(basePath, backend.Name())) if err != nil { return err } - if string(alias) == name { + if metadata != nil && metadata.Alias == name { backendDirectory = filepath.Join(basePath, backend.Name()) + foundBackend = true break } } } - if backendDirectory == "" { + // If no backend found, return successfully (idempotent behavior) + if !foundBackend { return fmt.Errorf("no backend found with name %q", name) } } - // If it's a meta, delete also associated backend - metaFile := filepath.Join(backendDirectory, metaFile) - if _, err := os.Stat(metaFile); err == nil { - meta, err := os.ReadFile(metaFile) - if err != nil { - return err - } - metaBackendDirectory := filepath.Join(basePath, string(meta)) + // If it's a meta backend, delete also associated backend + metadata, err := readBackendMetadata(backendDirectory) + if err != nil { + return err + } + + if metadata != nil && metadata.MetaBackendFor != "" { + metaBackendDirectory := filepath.Join(basePath, metadata.MetaBackendFor) log.Debug().Str("backendDirectory", metaBackendDirectory).Msg("Deleting meta backend") if _, err := os.Stat(metaBackendDirectory); os.IsNotExist(err) { - return fmt.Errorf("meta backend %q not found", string(meta)) + return fmt.Errorf("meta backend %q not found", metadata.MetaBackendFor) } os.RemoveAll(metaBackendDirectory) } @@ -175,14 +229,13 @@ func ListSystemBackends(basePath string) (map[string]string, error) { runFile := filepath.Join(basePath, backend.Name(), runFile) backendsNames[backend.Name()] = runFile - aliasFile := filepath.Join(basePath, backend.Name(), aliasFile) - if _, err := os.Stat(aliasFile); err == nil { - // read the alias file, and use it as key - alias, err := os.ReadFile(aliasFile) - if err != nil { - return nil, err - } - backendsNames[string(alias)] = runFile + // Check for alias in metadata + metadata, err := readBackendMetadata(filepath.Join(basePath, backend.Name())) + if err != nil { + return nil, err + } + if metadata != nil && metadata.Alias != "" { + backendsNames[metadata.Alias] = runFile } } } diff --git a/core/gallery/backends_test.go b/core/gallery/backends_test.go index 1f78d760..864ed3b5 100644 --- a/core/gallery/backends_test.go +++ b/core/gallery/backends_test.go @@ -1,6 +1,7 @@ package gallery import ( + "encoding/json" "os" "path/filepath" @@ -206,14 +207,20 @@ var _ = Describe("Gallery Backends", func() { }) It("should list meta backends correctly in system backends", func() { - // Create a meta backend directory with alias + // Create a meta backend directory with metadata metaBackendPath := filepath.Join(tempDir, "meta-backend") err := os.MkdirAll(metaBackendPath, 0750) Expect(err).NotTo(HaveOccurred()) - // Create alias file pointing to concrete backend - aliasFilePath := filepath.Join(metaBackendPath, "meta") - err = os.WriteFile(aliasFilePath, []byte("concrete-backend"), 0644) + // Create metadata file pointing to concrete backend + metadata := &BackendMetadata{ + MetaBackendFor: "concrete-backend", + Name: "meta-backend", + InstalledAt: "2023-01-01T00:00:00Z", + } + metadataData, err := json.Marshal(metadata) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(filepath.Join(metaBackendPath, "metadata.json"), metadataData, 0644) Expect(err).NotTo(HaveOccurred()) // Create the concrete backend directory with run.sh @@ -264,10 +271,17 @@ var _ = Describe("Gallery Backends", func() { err := InstallBackend(tempDir, &backend, nil) Expect(err).ToNot(HaveOccurred()) - Expect(filepath.Join(tempDir, "test-backend", "alias")).To(BeARegularFile()) - content, err := os.ReadFile(filepath.Join(tempDir, "test-backend", "alias")) + Expect(filepath.Join(tempDir, "test-backend", "metadata.json")).To(BeARegularFile()) + + // Read and verify metadata + metadataData, err := os.ReadFile(filepath.Join(tempDir, "test-backend", "metadata.json")) Expect(err).ToNot(HaveOccurred()) - Expect(string(content)).To(ContainSubstring("test-alias")) + var metadata BackendMetadata + err = json.Unmarshal(metadataData, &metadata) + Expect(err).ToNot(HaveOccurred()) + Expect(metadata.Alias).To(Equal("test-alias")) + Expect(metadata.Name).To(Equal("test-backend")) + Expect(filepath.Join(tempDir, "test-backend", "run.sh")).To(BeARegularFile()) // Check that the alias was recognized @@ -294,7 +308,7 @@ var _ = Describe("Gallery Backends", func() { It("should not error when backend doesn't exist", func() { err := DeleteBackendFromSystem(tempDir, "non-existent") - Expect(err).NotTo(HaveOccurred()) + Expect(err).To(HaveOccurred()) }) }) @@ -325,8 +339,15 @@ var _ = Describe("Gallery Backends", func() { err := os.MkdirAll(backendPath, 0750) Expect(err).NotTo(HaveOccurred()) - // Create alias file - err = os.WriteFile(filepath.Join(backendPath, "alias"), []byte(alias), 0644) + // Create metadata file with alias + metadata := &BackendMetadata{ + Alias: alias, + Name: backendName, + InstalledAt: "2023-01-01T00:00:00Z", + } + metadataData, err := json.Marshal(metadata) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(filepath.Join(backendPath, "metadata.json"), metadataData, 0644) Expect(err).NotTo(HaveOccurred()) backends, err := ListSystemBackends(tempDir) diff --git a/core/services/backends.go b/core/services/backends.go index b83ed8dd..7a52a1b1 100644 --- a/core/services/backends.go +++ b/core/services/backends.go @@ -2,12 +2,13 @@ package services import ( "github.com/mudler/LocalAI/core/gallery" + "github.com/mudler/LocalAI/core/system" "github.com/mudler/LocalAI/pkg/utils" "github.com/rs/zerolog/log" ) -func (g *GalleryService) backendHandler(op *GalleryOp[gallery.GalleryBackend]) error { +func (g *GalleryService) backendHandler(op *GalleryOp[gallery.GalleryBackend], systemState *system.SystemState) error { utils.ResetDownloadTimers() g.UpdateStatus(op.ID, &GalleryOpStatus{Message: "processing", Progress: 0}) @@ -23,7 +24,7 @@ func (g *GalleryService) backendHandler(op *GalleryOp[gallery.GalleryBackend]) e g.modelLoader.DeleteExternalBackend(op.GalleryElementName) } else { log.Warn().Msgf("installing backend %s", op.GalleryElementName) - err = gallery.InstallBackendFromGallery(g.appConfig.BackendGalleries, op.GalleryElementName, g.appConfig.BackendsPath, progressCallback) + err = gallery.InstallBackendFromGallery(g.appConfig.BackendGalleries, systemState, op.GalleryElementName, g.appConfig.BackendsPath, progressCallback) if err == nil { err = gallery.RegisterBackends(g.appConfig.BackendsPath, g.modelLoader) } diff --git a/core/services/gallery.go b/core/services/gallery.go index 0c33d243..27f2da21 100644 --- a/core/services/gallery.go +++ b/core/services/gallery.go @@ -7,7 +7,9 @@ import ( "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/gallery" + "github.com/mudler/LocalAI/core/system" "github.com/mudler/LocalAI/pkg/model" + "github.com/rs/zerolog/log" ) type GalleryService struct { @@ -63,13 +65,19 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader } } + systemState, err := system.GetSystemState() + if err != nil { + log.Error().Err(err).Msg("failed to get system state") + return + } + go func() { for { select { case <-c.Done(): return case op := <-g.BackendGalleryChannel: - err := g.backendHandler(&op) + err := g.backendHandler(&op, systemState) if err != nil { updateError(op.ID, err) } diff --git a/core/system/capabilities.go b/core/system/capabilities.go index b2fd0459..849724a2 100644 --- a/core/system/capabilities.go +++ b/core/system/capabilities.go @@ -25,15 +25,22 @@ func detectGPUVendor() (string, error) { } for _, gpu := range gpus { - if strings.ToUpper(gpu.DeviceInfo.Vendor.Name) == "NVIDIA" { - return "nvidia", nil - } - if strings.ToUpper(gpu.DeviceInfo.Vendor.Name) == "AMD" { - return "amd", nil - } - if strings.ToUpper(gpu.DeviceInfo.Vendor.Name) == "INTEL" { - return "intel", nil + if gpu.DeviceInfo != nil { + if gpu.DeviceInfo.Vendor != nil { + gpuVendorName := strings.ToUpper(gpu.DeviceInfo.Vendor.Name) + if gpuVendorName == "NVIDIA" { + return "nvidia", nil + } + if gpuVendorName == "AMD" { + return "amd", nil + } + if gpuVendorName == "INTEL" { + return "intel", nil + } + return "nvidia", nil + } } + } return "", nil diff --git a/pkg/startup/backend_preload.go b/pkg/startup/backend_preload.go index 17403c0c..cbc37ca0 100644 --- a/pkg/startup/backend_preload.go +++ b/pkg/startup/backend_preload.go @@ -7,10 +7,15 @@ import ( "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/gallery" + "github.com/mudler/LocalAI/core/system" ) func InstallExternalBackends(galleries []config.Gallery, backendPath string, downloadStatus func(string, string, string, float64), backends ...string) error { var errs error + systemState, err := system.GetSystemState() + if err != nil { + return fmt.Errorf("failed to get system state: %w", err) + } for _, backend := range backends { switch { case strings.HasPrefix(backend, "oci://"): @@ -22,7 +27,7 @@ func InstallExternalBackends(galleries []config.Gallery, backendPath string, dow errs = errors.Join(err, fmt.Errorf("error installing backend %s", backend)) } default: - err := gallery.InstallBackendFromGallery(galleries, backend, backendPath, downloadStatus) + err := gallery.InstallBackendFromGallery(galleries, systemState, backend, backendPath, downloadStatus) if err != nil { errs = errors.Join(err, fmt.Errorf("error installing backend %s", backend)) }