| | |
| | |
| | package gallery |
| |
|
| | import ( |
| | "context" |
| | "encoding/json" |
| | "errors" |
| | "fmt" |
| | "os" |
| | "path/filepath" |
| | "strings" |
| | "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" |
| | "github.com/mudler/xlog" |
| | cp "github.com/otiai10/copy" |
| | ) |
| |
|
| | const ( |
| | metadataFile = "metadata.json" |
| | runFile = "run.sh" |
| | ) |
| |
|
| | |
| | type backendCandidate struct { |
| | name string |
| | runFile string |
| | } |
| |
|
| | |
| | func readBackendMetadata(backendPath string) (*BackendMetadata, error) { |
| | metadataPath := filepath.Join(backendPath, metadataFile) |
| |
|
| | |
| | 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 |
| | } |
| |
|
| | |
| | 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 InstallBackendFromGallery(ctx context.Context, galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, name string, downloadStatus func(string, string, string, float64), force bool) error { |
| | if !force { |
| | |
| | backends, err := ListSystemBackends(systemState) |
| | if err != nil { |
| | return err |
| | } |
| | if backends.Exists(name) { |
| | return nil |
| | } |
| | } |
| |
|
| | if name == "" { |
| | return fmt.Errorf("backend name is empty") |
| | } |
| |
|
| | xlog.Debug("Installing backend from gallery", "galleries", galleries, "name", name) |
| |
|
| | 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() { |
| | xlog.Debug("Backend is a meta backend", "systemState", systemState, "name", name) |
| |
|
| | |
| | bestBackend := backend.FindBestBackendFromMeta(systemState, backends) |
| | if bestBackend == nil { |
| | return fmt.Errorf("no backend found with capabilities %q", backend.CapabilitiesMap) |
| | } |
| |
|
| | xlog.Debug("Installing backend from meta backend", "name", name, "bestBackend", bestBackend.Name) |
| |
|
| | |
| | if err := InstallBackend(ctx, systemState, modelLoader, bestBackend, downloadStatus); err != nil { |
| | return err |
| | } |
| |
|
| | |
| | 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) |
| | } |
| |
|
| | |
| | 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(ctx, systemState, modelLoader, backend, downloadStatus) |
| | } |
| |
|
| | func InstallBackend(ctx context.Context, systemState *system.SystemState, modelLoader *model.ModelLoader, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error { |
| | |
| | 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) |
| | |
| | if uri.LooksLikeDir() { |
| | |
| | if err := cp.Copy(config.URI, backendPath); err != nil { |
| | return fmt.Errorf("failed copying: %w", err) |
| | } |
| | } else { |
| | xlog.Debug("Downloading backend", "uri", config.URI, "backendPath", backendPath) |
| | if err := uri.DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err != nil { |
| | success := false |
| | |
| | for _, mirror := range config.Mirrors { |
| | |
| | select { |
| | case <-ctx.Done(): |
| | return ctx.Err() |
| | default: |
| | } |
| | if err := downloader.URI(mirror).DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err == nil { |
| | success = true |
| | xlog.Debug("Downloaded backend", "uri", config.URI, "backendPath", backendPath) |
| | break |
| | } |
| | } |
| |
|
| | if !success { |
| | xlog.Error("Failed to download backend", "uri", config.URI, "backendPath", backendPath, "error", err) |
| | return fmt.Errorf("failed to download backend %q: %v", config.URI, err) |
| | } |
| | } else { |
| | xlog.Debug("Downloaded backend", "uri", config.URI, "backendPath", backendPath) |
| | } |
| | } |
| |
|
| | |
| | runFile := filepath.Join(backendPath, runFile) |
| | if _, err := os.Stat(runFile); os.IsNotExist(err) { |
| | xlog.Error("Run file not found", "runFile", runFile) |
| | return fmt.Errorf("not a valid backend: run file not found %q", runFile) |
| | } |
| |
|
| | |
| | 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 RegisterBackends(systemState, modelLoader) |
| | } |
| |
|
| | 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) |
| |
|
| | |
| | if _, err := os.Stat(backendDirectory); os.IsNotExist(err) { |
| | |
| | |
| | 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 !foundBackend { |
| | return fmt.Errorf("no backend found with name %q", name) |
| | } |
| | } |
| |
|
| | |
| | metadata, err := readBackendMetadata(backendDirectory) |
| | if err != nil { |
| | return err |
| | } |
| |
|
| | if metadata != nil && metadata.MetaBackendFor != "" { |
| | metaBackendDirectory := filepath.Join(systemState.Backend.BackendsPath, metadata.MetaBackendFor) |
| | xlog.Debug("Deleting meta backend", "backendDirectory", metaBackendDirectory) |
| | 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) { |
| | |
| | backends := make(SystemBackends) |
| |
|
| | |
| | if systemBackends, err := os.ReadDir(systemState.Backend.BackendsSystemPath); err == nil { |
| | for _, systemBackend := range systemBackends { |
| | if systemBackend.IsDir() { |
| | run := filepath.Join(systemState.Backend.BackendsSystemPath, systemBackend.Name(), runFile) |
| | if _, err := os.Stat(run); err == nil { |
| | backends[systemBackend.Name()] = SystemBackend{ |
| | Name: systemBackend.Name(), |
| | RunFile: run, |
| | IsMeta: false, |
| | IsSystem: true, |
| | Metadata: nil, |
| | } |
| | } |
| | } |
| | } |
| | } else if !errors.Is(err, os.ErrNotExist) { |
| | xlog.Warn("Failed to read system backends, proceeding with user-managed backends", "error", err) |
| | } else if errors.Is(err, os.ErrNotExist) { |
| | xlog.Debug("No system backends found") |
| | } |
| |
|
| | |
| | entries, err := os.ReadDir(systemState.Backend.BackendsPath) |
| | if err != nil { |
| | return nil, err |
| | } |
| |
|
| | aliasGroups := make(map[string][]backendCandidate) |
| | metaMap := make(map[string]*BackendMetadata) |
| |
|
| | for _, e := range entries { |
| | if !e.IsDir() { |
| | continue |
| | } |
| | dir := e.Name() |
| | run := filepath.Join(systemState.Backend.BackendsPath, dir, runFile) |
| |
|
| | var metadata *BackendMetadata |
| | metadataPath := filepath.Join(systemState.Backend.BackendsPath, dir, metadataFile) |
| | if _, err := os.Stat(metadataPath); os.IsNotExist(err) { |
| | metadata = &BackendMetadata{Name: dir} |
| | } else { |
| | m, rerr := readBackendMetadata(filepath.Join(systemState.Backend.BackendsPath, dir)) |
| | if rerr != nil { |
| | return nil, rerr |
| | } |
| | if m == nil { |
| | metadata = &BackendMetadata{Name: dir} |
| | } else { |
| | metadata = m |
| | } |
| | } |
| |
|
| | metaMap[dir] = metadata |
| |
|
| | |
| | if _, err := os.Stat(run); err == nil { |
| | backends[dir] = SystemBackend{ |
| | Name: dir, |
| | RunFile: run, |
| | IsMeta: false, |
| | Metadata: metadata, |
| | } |
| | } |
| |
|
| | |
| | if metadata.Alias != "" { |
| | aliasGroups[metadata.Alias] = append(aliasGroups[metadata.Alias], backendCandidate{name: dir, runFile: run}) |
| | } |
| |
|
| | |
| | if metadata.MetaBackendFor != "" { |
| | backends[metadata.Name] = SystemBackend{ |
| | Name: metadata.Name, |
| | RunFile: filepath.Join(systemState.Backend.BackendsPath, metadata.MetaBackendFor, runFile), |
| | IsMeta: true, |
| | Metadata: metadata, |
| | } |
| | } |
| | } |
| |
|
| | |
| | tokens := systemState.BackendPreferenceTokens() |
| | for alias, cands := range aliasGroups { |
| | chosen := backendCandidate{} |
| | |
| | for _, t := range tokens { |
| | for _, c := range cands { |
| | if strings.Contains(strings.ToLower(c.name), t) && c.runFile != "" { |
| | chosen = c |
| | break |
| | } |
| | } |
| | if chosen.runFile != "" { |
| | break |
| | } |
| | } |
| | |
| | if chosen.runFile == "" { |
| | for _, c := range cands { |
| | if c.runFile != "" { |
| | chosen = c |
| | break |
| | } |
| | } |
| | } |
| | if chosen.runFile == "" { |
| | continue |
| | } |
| | md := metaMap[chosen.name] |
| | backends[alias] = SystemBackend{ |
| | Name: alias, |
| | RunFile: chosen.runFile, |
| | IsMeta: false, |
| | Metadata: md, |
| | } |
| | } |
| |
|
| | 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 { |
| | xlog.Debug("Registering backend", "name", backend.Name, "runFile", backend.RunFile) |
| | modelLoader.SetExternalBackend(backend.Name, backend.RunFile) |
| | } |
| |
|
| | return nil |
| | } |
| |
|