feat: use a metadata file
Some checks failed
Security Scan / tests (push) Has been cancelled

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto 2025-06-20 22:18:48 +02:00
parent 9a34fc8c66
commit ec137888ba
7 changed files with 164 additions and 55 deletions

View file

@ -2,6 +2,20 @@ package gallery
import "github.com/mudler/LocalAI/core/config" 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 { type GalleryBackend struct {
Metadata `json:",inline" yaml:",inline"` Metadata `json:",inline" yaml:",inline"`
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`

View file

@ -1,9 +1,11 @@
package gallery package gallery
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/system" "github.com/mudler/LocalAI/core/system"
@ -13,11 +15,48 @@ import (
) )
const ( const (
aliasFile = "alias" metadataFile = "metadata.json"
metaFile = "meta" runFile = "run.sh"
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 { func findBestBackendFromMeta(backend *GalleryBackend, systemState *system.SystemState, backends GalleryElements[*GalleryBackend]) *GalleryBackend {
realBackend := backend.CapabilitiesMap[systemState.GPUVendor] realBackend := backend.CapabilitiesMap[systemState.GPUVendor]
if realBackend == "" { 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) 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 // Create metadata for the meta backend
metaFile := filepath.Join(metaBackendPath, metaFile) metaMetadata := &BackendMetadata{
if err := os.WriteFile(metaFile, []byte(bestBackend.Name), 0644); err != nil { MetaBackendFor: bestBackend.Name,
return fmt.Errorf("failed to write meta file %q: %v", metaFile, err) 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 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) 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 != "" { if config.Alias != "" {
// Write an alias file inside metadata.Alias = config.Alias
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) if err := writeBackendMetadata(backendPath, metadata); err != nil {
} return fmt.Errorf("failed to write metadata for backend %q: %v", name, err)
} }
return nil return nil
@ -124,37 +176,39 @@ func DeleteBackendFromSystem(basePath string, name string) error {
if err != nil { if err != nil {
return err return err
} }
foundBackend := false
for _, backend := range backends { for _, backend := range backends {
if backend.IsDir() { if backend.IsDir() {
aliasFile := filepath.Join(basePath, backend.Name(), aliasFile) metadata, err := readBackendMetadata(filepath.Join(basePath, backend.Name()))
alias, err := os.ReadFile(aliasFile)
if err != nil { if err != nil {
return err return err
} }
if string(alias) == name { if metadata != nil && metadata.Alias == name {
backendDirectory = filepath.Join(basePath, backend.Name()) backendDirectory = filepath.Join(basePath, backend.Name())
foundBackend = true
break break
} }
} }
} }
if backendDirectory == "" { // If no backend found, return successfully (idempotent behavior)
if !foundBackend {
return fmt.Errorf("no backend found with name %q", name) return fmt.Errorf("no backend found with name %q", name)
} }
} }
// If it's a meta, delete also associated backend // If it's a meta backend, delete also associated backend
metaFile := filepath.Join(backendDirectory, metaFile) metadata, err := readBackendMetadata(backendDirectory)
if _, err := os.Stat(metaFile); err == nil { if err != nil {
meta, err := os.ReadFile(metaFile) return err
if err != nil { }
return err
} if metadata != nil && metadata.MetaBackendFor != "" {
metaBackendDirectory := filepath.Join(basePath, string(meta)) metaBackendDirectory := filepath.Join(basePath, metadata.MetaBackendFor)
log.Debug().Str("backendDirectory", metaBackendDirectory).Msg("Deleting meta backend") log.Debug().Str("backendDirectory", metaBackendDirectory).Msg("Deleting meta backend")
if _, err := os.Stat(metaBackendDirectory); os.IsNotExist(err) { 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) os.RemoveAll(metaBackendDirectory)
} }
@ -175,14 +229,13 @@ func ListSystemBackends(basePath string) (map[string]string, error) {
runFile := filepath.Join(basePath, backend.Name(), runFile) runFile := filepath.Join(basePath, backend.Name(), runFile)
backendsNames[backend.Name()] = runFile backendsNames[backend.Name()] = runFile
aliasFile := filepath.Join(basePath, backend.Name(), aliasFile) // Check for alias in metadata
if _, err := os.Stat(aliasFile); err == nil { metadata, err := readBackendMetadata(filepath.Join(basePath, backend.Name()))
// read the alias file, and use it as key if err != nil {
alias, err := os.ReadFile(aliasFile) return nil, err
if err != nil { }
return nil, err if metadata != nil && metadata.Alias != "" {
} backendsNames[metadata.Alias] = runFile
backendsNames[string(alias)] = runFile
} }
} }
} }

View file

@ -1,6 +1,7 @@
package gallery package gallery
import ( import (
"encoding/json"
"os" "os"
"path/filepath" "path/filepath"
@ -206,14 +207,20 @@ var _ = Describe("Gallery Backends", func() {
}) })
It("should list meta backends correctly in system 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") metaBackendPath := filepath.Join(tempDir, "meta-backend")
err := os.MkdirAll(metaBackendPath, 0750) err := os.MkdirAll(metaBackendPath, 0750)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Create alias file pointing to concrete backend // Create metadata file pointing to concrete backend
aliasFilePath := filepath.Join(metaBackendPath, "meta") metadata := &BackendMetadata{
err = os.WriteFile(aliasFilePath, []byte("concrete-backend"), 0644) 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()) Expect(err).NotTo(HaveOccurred())
// Create the concrete backend directory with run.sh // Create the concrete backend directory with run.sh
@ -264,10 +271,17 @@ var _ = Describe("Gallery Backends", func() {
err := InstallBackend(tempDir, &backend, nil) err := InstallBackend(tempDir, &backend, nil)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(filepath.Join(tempDir, "test-backend", "alias")).To(BeARegularFile()) Expect(filepath.Join(tempDir, "test-backend", "metadata.json")).To(BeARegularFile())
content, err := os.ReadFile(filepath.Join(tempDir, "test-backend", "alias"))
// Read and verify metadata
metadataData, err := os.ReadFile(filepath.Join(tempDir, "test-backend", "metadata.json"))
Expect(err).ToNot(HaveOccurred()) 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()) Expect(filepath.Join(tempDir, "test-backend", "run.sh")).To(BeARegularFile())
// Check that the alias was recognized // 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() { It("should not error when backend doesn't exist", func() {
err := DeleteBackendFromSystem(tempDir, "non-existent") 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) err := os.MkdirAll(backendPath, 0750)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Create alias file // Create metadata file with alias
err = os.WriteFile(filepath.Join(backendPath, "alias"), []byte(alias), 0644) 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()) Expect(err).NotTo(HaveOccurred())
backends, err := ListSystemBackends(tempDir) backends, err := ListSystemBackends(tempDir)

View file

@ -2,12 +2,13 @@ package services
import ( import (
"github.com/mudler/LocalAI/core/gallery" "github.com/mudler/LocalAI/core/gallery"
"github.com/mudler/LocalAI/core/system"
"github.com/mudler/LocalAI/pkg/utils" "github.com/mudler/LocalAI/pkg/utils"
"github.com/rs/zerolog/log" "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() utils.ResetDownloadTimers()
g.UpdateStatus(op.ID, &GalleryOpStatus{Message: "processing", Progress: 0}) 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) g.modelLoader.DeleteExternalBackend(op.GalleryElementName)
} else { } else {
log.Warn().Msgf("installing backend %s", op.GalleryElementName) 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 { if err == nil {
err = gallery.RegisterBackends(g.appConfig.BackendsPath, g.modelLoader) err = gallery.RegisterBackends(g.appConfig.BackendsPath, g.modelLoader)
} }

View file

@ -7,7 +7,9 @@ import (
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/gallery" "github.com/mudler/LocalAI/core/gallery"
"github.com/mudler/LocalAI/core/system"
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
"github.com/rs/zerolog/log"
) )
type GalleryService struct { 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() { go func() {
for { for {
select { select {
case <-c.Done(): case <-c.Done():
return return
case op := <-g.BackendGalleryChannel: case op := <-g.BackendGalleryChannel:
err := g.backendHandler(&op) err := g.backendHandler(&op, systemState)
if err != nil { if err != nil {
updateError(op.ID, err) updateError(op.ID, err)
} }

View file

@ -25,15 +25,22 @@ func detectGPUVendor() (string, error) {
} }
for _, gpu := range gpus { for _, gpu := range gpus {
if strings.ToUpper(gpu.DeviceInfo.Vendor.Name) == "NVIDIA" { if gpu.DeviceInfo != nil {
return "nvidia", nil if gpu.DeviceInfo.Vendor != nil {
} gpuVendorName := strings.ToUpper(gpu.DeviceInfo.Vendor.Name)
if strings.ToUpper(gpu.DeviceInfo.Vendor.Name) == "AMD" { if gpuVendorName == "NVIDIA" {
return "amd", nil return "nvidia", nil
} }
if strings.ToUpper(gpu.DeviceInfo.Vendor.Name) == "INTEL" { if gpuVendorName == "AMD" {
return "intel", nil return "amd", nil
}
if gpuVendorName == "INTEL" {
return "intel", nil
}
return "nvidia", nil
}
} }
} }
return "", nil return "", nil

View file

@ -7,10 +7,15 @@ import (
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/gallery" "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 { func InstallExternalBackends(galleries []config.Gallery, backendPath string, downloadStatus func(string, string, string, float64), backends ...string) error {
var errs 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 { for _, backend := range backends {
switch { switch {
case strings.HasPrefix(backend, "oci://"): 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)) errs = errors.Join(err, fmt.Errorf("error installing backend %s", backend))
} }
default: default:
err := gallery.InstallBackendFromGallery(galleries, backend, backendPath, downloadStatus) err := gallery.InstallBackendFromGallery(galleries, systemState, backend, backendPath, downloadStatus)
if err != nil { if err != nil {
errs = errors.Join(err, fmt.Errorf("error installing backend %s", backend)) errs = errors.Join(err, fmt.Errorf("error installing backend %s", backend))
} }