mirror of
https://github.com/mudler/LocalAI.git
synced 2026-01-04 09:40:32 -06:00
- Add a system backend path - Refactor and consolidate system information in system state - Use system state in all the components to figure out the system paths to used whenever needed - Refactor BackendConfig -> ModelConfig. This was otherway misleading as now we do have a backend configuration which is not the model config. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
386 lines
11 KiB
Go
386 lines
11 KiB
Go
package gallery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/pkg/downloader"
|
|
"github.com/mudler/LocalAI/pkg/model"
|
|
"github.com/mudler/LocalAI/pkg/system"
|
|
cp "github.com/otiai10/copy"
|
|
"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
|
|
}
|
|
|
|
// Installs a model from the gallery
|
|
func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.SystemState, name string, downloadStatus func(string, string, string, float64), force bool) error {
|
|
if !force {
|
|
// check if we already have the backend installed
|
|
backends, err := ListSystemBackends(systemState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if backends.Exists(name) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if name == "" {
|
|
return fmt.Errorf("backend name is empty")
|
|
}
|
|
|
|
log.Debug().Interface("galleries", galleries).Str("name", name).Msg("Installing backend from gallery")
|
|
|
|
backends, err := AvailableBackends(galleries, systemState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
backend := FindGalleryElement(backends, name)
|
|
if backend == nil {
|
|
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 := backend.FindBestBackendFromMeta(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(systemState, 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(systemState.Backend.BackendsPath, 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(systemState, backend, downloadStatus)
|
|
}
|
|
|
|
func InstallBackend(systemState *system.SystemState, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
|
|
// Create base path if it doesn't exist
|
|
err := os.MkdirAll(systemState.Backend.BackendsPath, 0750)
|
|
if err != nil {
|
|
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
|
|
backendPath := filepath.Join(systemState.Backend.BackendsPath, name)
|
|
err = os.MkdirAll(backendPath, 0750)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create base path: %v", err)
|
|
}
|
|
|
|
uri := downloader.URI(config.URI)
|
|
// Check if it is a directory
|
|
if uri.LooksLikeDir() {
|
|
// It is a directory, we just copy it over in the backend folder
|
|
if err := cp.Copy(config.URI, backendPath); err != nil {
|
|
return fmt.Errorf("failed copying: %w", err)
|
|
}
|
|
} else {
|
|
uri := downloader.URI(config.URI)
|
|
if err := uri.DownloadFile(backendPath, "", 1, 1, downloadStatus); err != nil {
|
|
success := false
|
|
// Try to download from mirrors
|
|
for _, mirror := range config.Mirrors {
|
|
if err := downloader.URI(mirror).DownloadFile(backendPath, "", 1, 1, downloadStatus); err == nil {
|
|
success = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !success {
|
|
return fmt.Errorf("failed to download backend %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 != "" {
|
|
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(systemState *system.SystemState, name string) error {
|
|
backends, err := ListSystemBackends(systemState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
backend, ok := backends.Get(name)
|
|
if !ok {
|
|
return fmt.Errorf("backend %q not found", name)
|
|
}
|
|
|
|
if backend.IsSystem {
|
|
return fmt.Errorf("system backend %q cannot be deleted", name)
|
|
}
|
|
|
|
backendDirectory := filepath.Join(systemState.Backend.BackendsPath, name)
|
|
|
|
// 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(systemState.Backend.BackendsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
foundBackend := false
|
|
|
|
for _, backend := range backends {
|
|
if backend.IsDir() {
|
|
metadata, err := readBackendMetadata(filepath.Join(systemState.Backend.BackendsPath, backend.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if metadata != nil && metadata.Alias == name {
|
|
backendDirectory = filepath.Join(systemState.Backend.BackendsPath, 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(systemState.Backend.BackendsPath, 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)
|
|
}
|
|
|
|
type SystemBackend struct {
|
|
Name string
|
|
RunFile string
|
|
IsMeta bool
|
|
IsSystem bool
|
|
Metadata *BackendMetadata
|
|
}
|
|
|
|
type SystemBackends map[string]SystemBackend
|
|
|
|
func (b SystemBackends) Exists(name string) bool {
|
|
_, ok := b[name]
|
|
return ok
|
|
}
|
|
|
|
func (b SystemBackends) Get(name string) (SystemBackend, bool) {
|
|
backend, ok := b[name]
|
|
return backend, ok
|
|
}
|
|
|
|
func (b SystemBackends) GetAll() []SystemBackend {
|
|
backends := make([]SystemBackend, 0)
|
|
for _, backend := range b {
|
|
backends = append(backends, backend)
|
|
}
|
|
return backends
|
|
}
|
|
|
|
func ListSystemBackends(systemState *system.SystemState) (SystemBackends, error) {
|
|
potentialBackends, err := os.ReadDir(systemState.Backend.BackendsPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backends := make(SystemBackends)
|
|
|
|
systemBackends, err := os.ReadDir(systemState.Backend.BackendsSystemPath)
|
|
if err == nil {
|
|
// system backends are special, they are provided by the system and not managed by LocalAI
|
|
for _, systemBackend := range systemBackends {
|
|
if systemBackend.IsDir() {
|
|
systemBackendRunFile := filepath.Join(systemState.Backend.BackendsSystemPath, systemBackend.Name(), runFile)
|
|
if _, err := os.Stat(systemBackendRunFile); err == nil {
|
|
backends[systemBackend.Name()] = SystemBackend{
|
|
Name: systemBackend.Name(),
|
|
RunFile: filepath.Join(systemState.Backend.BackendsSystemPath, systemBackend.Name(), runFile),
|
|
IsMeta: false,
|
|
IsSystem: true,
|
|
Metadata: nil,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log.Warn().Err(err).Msg("Failed to read system backends, but that's ok, we will just use the backends managed by LocalAI")
|
|
}
|
|
|
|
for _, potentialBackend := range potentialBackends {
|
|
if potentialBackend.IsDir() {
|
|
potentialBackendRunFile := filepath.Join(systemState.Backend.BackendsPath, potentialBackend.Name(), runFile)
|
|
|
|
var metadata *BackendMetadata
|
|
|
|
// If metadata file does not exist, we just use the directory name
|
|
// and we do not fill the other metadata (such as potential backend Aliases)
|
|
metadataFilePath := filepath.Join(systemState.Backend.BackendsPath, potentialBackend.Name(), metadataFile)
|
|
if _, err := os.Stat(metadataFilePath); os.IsNotExist(err) {
|
|
metadata = &BackendMetadata{
|
|
Name: potentialBackend.Name(),
|
|
}
|
|
} else {
|
|
// Check for alias in metadata
|
|
metadata, err = readBackendMetadata(filepath.Join(systemState.Backend.BackendsPath, potentialBackend.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if !backends.Exists(potentialBackend.Name()) {
|
|
// We don't want to override aliases if already set, and if we are meta backend
|
|
if _, err := os.Stat(potentialBackendRunFile); err == nil {
|
|
backends[potentialBackend.Name()] = SystemBackend{
|
|
Name: potentialBackend.Name(),
|
|
RunFile: potentialBackendRunFile,
|
|
IsMeta: false,
|
|
Metadata: metadata,
|
|
}
|
|
}
|
|
}
|
|
|
|
if metadata == nil {
|
|
continue
|
|
}
|
|
|
|
if metadata.Alias != "" {
|
|
backends[metadata.Alias] = SystemBackend{
|
|
Name: metadata.Alias,
|
|
RunFile: potentialBackendRunFile,
|
|
IsMeta: false,
|
|
Metadata: metadata,
|
|
}
|
|
}
|
|
|
|
if metadata.MetaBackendFor != "" {
|
|
backends[metadata.Name] = SystemBackend{
|
|
Name: metadata.Name,
|
|
RunFile: filepath.Join(systemState.Backend.BackendsPath, metadata.MetaBackendFor, runFile),
|
|
IsMeta: true,
|
|
Metadata: metadata,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return backends, nil
|
|
}
|
|
|
|
func RegisterBackends(systemState *system.SystemState, modelLoader *model.ModelLoader) error {
|
|
backends, err := ListSystemBackends(systemState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, backend := range backends {
|
|
log.Debug().Str("name", backend.Name).Str("runFile", backend.RunFile).Msg("Registering backend")
|
|
modelLoader.SetExternalBackend(backend.Name, backend.RunFile)
|
|
}
|
|
|
|
return nil
|
|
}
|