Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions core/gallery/backend_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@ 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"`
URI string `json:"uri,omitempty" yaml:"uri,omitempty"`
Metadata `json:",inline" yaml:",inline"`
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
URI string `json:"uri,omitempty" yaml:"uri,omitempty"`
CapabilitiesMap map[string]string `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
}

type GalleryBackends []*GalleryBackend
Expand All @@ -14,6 +29,10 @@ func (m *GalleryBackend) SetGallery(gallery config.Gallery) {
m.Gallery = gallery
}

func (m *GalleryBackend) IsMeta() bool {
return len(m.CapabilitiesMap) > 0
}

func (m *GalleryBackend) SetInstalled(installed bool) {
m.Installed = installed
}
Expand Down
190 changes: 172 additions & 18 deletions core/gallery/backends.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,124 @@
package gallery

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"

"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/system"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/oci"
"github.com/rs/zerolog/log"
)

const (
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 {
if systemState == nil {
return nil
}

realBackend := backend.CapabilitiesMap[systemState.GPUVendor]
if realBackend == "" {
return nil
}

return backends.FindByName(realBackend)
}

// Installs a model from the gallery
func InstallBackendFromGallery(galleries []config.Gallery, name string, basePath string, downloadStatus func(string, string, string, float64)) error {
func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.SystemState, name string, basePath string, downloadStatus func(string, string, string, float64)) error {
log.Debug().Interface("galleries", galleries).Str("name", name).Msg("Installing backend from gallery")

backends, err := AvailableBackends(galleries, basePath)
if err != nil {
return err
}

backend := FindGalleryElement(backends, name, basePath)
if backend == nil {
return fmt.Errorf("no model found with name %q", name)
return fmt.Errorf("no backend found with name %q", name)
}

if backend.IsMeta() {
log.Debug().Interface("systemState", systemState).Str("name", name).Msg("Backend is a meta backend")

// Then, let's try to find the best backend based on the capabilities map
bestBackend := findBestBackendFromMeta(backend, systemState, backends)
if bestBackend == nil {
return fmt.Errorf("no backend found with capabilities %q", backend.CapabilitiesMap)
}

log.Debug().Str("name", name).Str("bestBackend", bestBackend.Name).Msg("Installing backend from meta backend")

// Then, let's install the best backend
if err := InstallBackend(basePath, bestBackend, downloadStatus); err != nil {
return err
}

// we need now to create a path for the meta backend, with the alias to the installed ones so it can be used to remove it
metaBackendPath := filepath.Join(basePath, name)
if err := os.MkdirAll(metaBackendPath, 0750); err != nil {
return fmt.Errorf("failed to create meta backend path %q: %v", metaBackendPath, 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
}

return InstallBackend(basePath, backend, downloadStatus)
Expand All @@ -32,6 +131,10 @@ func InstallBackend(basePath string, config *GalleryBackend, downloadStatus func
return fmt.Errorf("failed to create base path: %v", err)
}

if config.IsMeta() {
return fmt.Errorf("meta backends cannot be installed directly")
}

name := config.Name

img, err := oci.GetImage(config.URI, "", nil, nil)
Expand All @@ -48,21 +151,73 @@ 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, "alias")
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
}

func DeleteBackendFromSystem(basePath string, name string) error {
backendFile := filepath.Join(basePath, name)
backendDirectory := filepath.Join(basePath, name)

return os.RemoveAll(backendFile)
// check if the backend dir exists
if _, err := os.Stat(backendDirectory); os.IsNotExist(err) {
// if doesn't exist, it might be an alias, so we need to check if we have a matching alias in
// all the backends in the basePath
backends, err := os.ReadDir(basePath)
if err != nil {
return err
}
foundBackend := false

for _, backend := range backends {
if backend.IsDir() {
metadata, err := readBackendMetadata(filepath.Join(basePath, backend.Name()))
if err != nil {
return err
}
if metadata != nil && metadata.Alias == name {
backendDirectory = filepath.Join(basePath, backend.Name())
foundBackend = true
break
}
}
}

// 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 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", metadata.MetaBackendFor)
}
os.RemoveAll(metaBackendDirectory)
}

return os.RemoveAll(backendDirectory)
}

func ListSystemBackends(basePath string) (map[string]string, error) {
Expand All @@ -75,17 +230,16 @@ func ListSystemBackends(basePath string) (map[string]string, error) {

for _, backend := range backends {
if backend.IsDir() {
runFile := filepath.Join(basePath, backend.Name(), "run.sh")
runFile := filepath.Join(basePath, backend.Name(), runFile)
backendsNames[backend.Name()] = runFile

aliasFile := filepath.Join(basePath, backend.Name(), "alias")
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
}
}
}
Expand Down
Loading
Loading