Skip to content

Commit b57f399

Browse files
committed
feat: Add backend gallery
This PR add support to manage backends as similar to models. There is now available a backend gallery which can be used to install and remove extra backends. The backend gallery can be configured similarly as a model gallery, and API calls allows to install and remove new backends in runtime, and as well during the startup phase of LocalAI. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent f86cb8b commit b57f399

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2360
-894
lines changed

core/application/startup.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/mudler/LocalAI/core/backend"
88
"github.com/mudler/LocalAI/core/config"
9+
"github.com/mudler/LocalAI/core/gallery"
910
"github.com/mudler/LocalAI/core/services"
1011
"github.com/mudler/LocalAI/internal"
1112
"github.com/mudler/LocalAI/pkg/assets"
@@ -60,12 +61,20 @@ func New(opts ...config.AppOption) (*Application, error) {
6061
log.Error().Err(err).Msg("error installing models")
6162
}
6263

64+
if err := pkgStartup.InstallExternalBackends(options.BackendGalleries, options.BackendsPath, nil, options.ExternalBackends...); err != nil {
65+
log.Error().Err(err).Msg("error installing external backends")
66+
}
67+
6368
configLoaderOpts := options.ToConfigLoaderOptions()
6469

6570
if err := application.BackendLoader().LoadBackendConfigsFromPath(options.ModelPath, configLoaderOpts...); err != nil {
6671
log.Error().Err(err).Msg("error loading config files")
6772
}
6873

74+
if err := gallery.RegisterBackends(options.BackendsPath, application.ModelLoader()); err != nil {
75+
log.Error().Err(err).Msg("error registering external backends")
76+
}
77+
6978
if options.ConfigFile != "" {
7079
if err := application.BackendLoader().LoadMultipleBackendConfigsSingleFile(options.ConfigFile, configLoaderOpts...); err != nil {
7180
log.Error().Err(err).Msg("error loading config file")

core/cli/models.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (mi *ModelsInstall) Run(ctx *cliContext.Context) error {
8686
modelURI := downloader.URI(modelName)
8787

8888
if !modelURI.LooksLikeOCI() {
89-
model := gallery.FindModel(models, modelName, mi.ModelsPath)
89+
model := gallery.FindGalleryElement(models, modelName, mi.ModelsPath)
9090
if model == nil {
9191
log.Error().Str("model", modelName).Msg("model not found")
9292
return err

core/cli/run.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strings"
78
"time"
89

@@ -19,6 +20,8 @@ import (
1920
type RunCMD struct {
2021
ModelArgs []string `arg:"" optional:"" name:"models" help:"Model configuration URLs to load"`
2122

23+
ExternalBackends []string `env:"LOCALAI_EXTERNAL_BACKENDS,EXTERNAL_BACKENDS" help:"A list of external backends to load from gallery on boot" group:"backends"`
24+
BackendsPath string `env:"LOCALAI_BACKENDS_PATH,BACKENDS_PATH" type:"path" default:"${basepath}/backends" help:"Path containing backends used for inferencing" group:"backends"`
2225
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
2326
BackendAssetsPath string `env:"LOCALAI_BACKEND_ASSETS_PATH,BACKEND_ASSETS_PATH" type:"path" default:"/tmp/localai/backend_data" help:"Path used to extract libraries that are required by some of the backends in runtime" group:"storage"`
2427
GeneratedContentPath string `env:"LOCALAI_GENERATED_CONTENT_PATH,GENERATED_CONTENT_PATH" type:"path" default:"/tmp/generated/content" help:"Location for generated content (e.g. images, audio, videos)" group:"storage"`
@@ -27,8 +30,8 @@ type RunCMD struct {
2730
LocalaiConfigDir string `env:"LOCALAI_CONFIG_DIR" type:"path" default:"${basepath}/configuration" help:"Directory for dynamic loading of certain configuration files (currently api_keys.json and external_backends.json)" group:"storage"`
2831
LocalaiConfigDirPollInterval time.Duration `env:"LOCALAI_CONFIG_DIR_POLL_INTERVAL" help:"Typically the config path picks up changes automatically, but if your system has broken fsnotify events, set this to an interval to poll the LocalAI Config Dir (example: 1m)" group:"storage"`
2932
// The alias on this option is there to preserve functionality with the old `--config-file` parameter
30-
ModelsConfigFile string `env:"LOCALAI_MODELS_CONFIG_FILE,CONFIG_FILE" aliases:"config-file" help:"YAML file containing a list of model backend configs" group:"storage"`
31-
33+
ModelsConfigFile string `env:"LOCALAI_MODELS_CONFIG_FILE,CONFIG_FILE" aliases:"config-file" help:"YAML file containing a list of model backend configs" group:"storage"`
34+
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
3235
Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"`
3336
AutoloadGalleries bool `env:"LOCALAI_AUTOLOAD_GALLERIES,AUTOLOAD_GALLERIES" group:"models"`
3437
PreloadModels string `env:"LOCALAI_PRELOAD_MODELS,PRELOAD_MODELS" help:"A List of models to apply in JSON at start" group:"models"`
@@ -73,11 +76,15 @@ type RunCMD struct {
7376
}
7477

7578
func (r *RunCMD) Run(ctx *cliContext.Context) error {
79+
os.MkdirAll(r.BackendsPath, 0750)
80+
os.MkdirAll(r.ModelsPath, 0750)
81+
7682
opts := []config.AppOption{
7783
config.WithConfigFile(r.ModelsConfigFile),
7884
config.WithJSONStringPreload(r.PreloadModels),
7985
config.WithYAMLConfigPreload(r.PreloadModelsConfig),
8086
config.WithModelPath(r.ModelsPath),
87+
config.WithBackendsPath(r.BackendsPath),
8188
config.WithContextSize(r.ContextSize),
8289
config.WithDebug(zerolog.GlobalLevel() <= zerolog.DebugLevel),
8390
config.WithGeneratedContentDir(r.GeneratedContentPath),
@@ -87,6 +94,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
8794
config.WithDynamicConfigDirPollInterval(r.LocalaiConfigDirPollInterval),
8895
config.WithF16(r.F16),
8996
config.WithStringGalleries(r.Galleries),
97+
config.WithBackendGalleries(r.BackendGalleries),
9098
config.WithCors(r.CORS),
9199
config.WithCorsAllowOrigins(r.CORSAllowOrigins),
92100
config.WithCsrf(r.CSRF),
@@ -97,6 +105,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
97105
config.WithUploadLimitMB(r.UploadLimit),
98106
config.WithApiKeys(r.APIKeys),
99107
config.WithModelsURL(append(r.Models, r.ModelArgs...)...),
108+
config.WithExternalBackends(r.ExternalBackends...),
100109
config.WithOpaqueErrors(r.OpaqueErrors),
101110
config.WithEnforcedPredownloadScans(!r.DisablePredownloadScan),
102111
config.WithSubtleKeyComparison(r.UseSubtleKeyComparison),

core/config/application_config.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ type ApplicationConfig struct {
1515
Context context.Context
1616
ConfigFile string
1717
ModelPath string
18+
BackendsPath string
19+
ExternalBackends []string
1820
LibPath string
1921
UploadLimitMB, Threads, ContextSize int
2022
F16 bool
@@ -45,7 +47,8 @@ type ApplicationConfig struct {
4547
DisableGalleryEndpoint bool
4648
LoadToMemory []string
4749

48-
Galleries []Gallery
50+
Galleries []Gallery
51+
BackendGalleries []Gallery
4952

5053
BackendAssets *rice.Box
5154
AssetsDestination string
@@ -95,6 +98,18 @@ func WithModelPath(path string) AppOption {
9598
}
9699
}
97100

101+
func WithBackendsPath(path string) AppOption {
102+
return func(o *ApplicationConfig) {
103+
o.BackendsPath = path
104+
}
105+
}
106+
107+
func WithExternalBackends(backends ...string) AppOption {
108+
return func(o *ApplicationConfig) {
109+
o.ExternalBackends = backends
110+
}
111+
}
112+
98113
func WithMachineTag(tag string) AppOption {
99114
return func(o *ApplicationConfig) {
100115
o.MachineTag = tag
@@ -218,6 +233,20 @@ func WithStringGalleries(galls string) AppOption {
218233
}
219234
}
220235

236+
func WithBackendGalleries(galls string) AppOption {
237+
return func(o *ApplicationConfig) {
238+
if galls == "" {
239+
o.BackendGalleries = []Gallery{}
240+
return
241+
}
242+
var galleries []Gallery
243+
if err := json.Unmarshal([]byte(galls), &galleries); err != nil {
244+
log.Error().Err(err).Msg("failed loading galleries")
245+
}
246+
o.BackendGalleries = append(o.BackendGalleries, galleries...)
247+
}
248+
}
249+
221250
func WithGalleries(galleries []Gallery) AppOption {
222251
return func(o *ApplicationConfig) {
223252
o.Galleries = append(o.Galleries, galleries...)

core/gallery/backend_types.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package gallery
2+
3+
import "github.com/mudler/LocalAI/core/config"
4+
5+
type GalleryBackend struct {
6+
Metadata `json:",inline" yaml:",inline"`
7+
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
8+
URI string `json:"uri,omitempty" yaml:"uri,omitempty"`
9+
}
10+
11+
type GalleryBackends []*GalleryBackend
12+
13+
func (m *GalleryBackend) SetGallery(gallery config.Gallery) {
14+
m.Gallery = gallery
15+
}
16+
17+
func (m *GalleryBackend) SetInstalled(installed bool) {
18+
m.Installed = installed
19+
}
20+
21+
func (m *GalleryBackend) GetName() string {
22+
return m.Name
23+
}
24+
25+
func (m *GalleryBackend) GetGallery() config.Gallery {
26+
return m.Gallery
27+
}
28+
29+
func (m *GalleryBackend) GetDescription() string {
30+
return m.Description
31+
}
32+
33+
func (m *GalleryBackend) GetTags() []string {
34+
return m.Tags
35+
}

core/gallery/backends.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package gallery
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/mudler/LocalAI/core/config"
9+
"github.com/mudler/LocalAI/pkg/model"
10+
"github.com/mudler/LocalAI/pkg/oci"
11+
)
12+
13+
// Installs a model from the gallery
14+
func InstallBackendFromGallery(galleries []config.Gallery, name string, basePath string, downloadStatus func(string, string, string, float64)) error {
15+
backends, err := AvailableBackends(galleries, basePath)
16+
if err != nil {
17+
return err
18+
}
19+
20+
backend := FindGalleryElement(backends, name, basePath)
21+
if backend == nil {
22+
return fmt.Errorf("no model found with name %q", name)
23+
}
24+
25+
return InstallBackend(basePath, backend, downloadStatus)
26+
}
27+
28+
func InstallBackend(basePath string, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
29+
// Create base path if it doesn't exist
30+
err := os.MkdirAll(basePath, 0750)
31+
if err != nil {
32+
return fmt.Errorf("failed to create base path: %v", err)
33+
}
34+
35+
name := config.Name
36+
37+
img, err := oci.GetImage(config.URI, "", nil, nil)
38+
if err != nil {
39+
return fmt.Errorf("failed to get image %q: %v", config.URI, err)
40+
}
41+
42+
backendPath := filepath.Join(basePath, name)
43+
if err := os.MkdirAll(backendPath, 0750); err != nil {
44+
return fmt.Errorf("failed to create backend path %q: %v", backendPath, err)
45+
}
46+
47+
if err := oci.ExtractOCIImage(img, backendPath); err != nil {
48+
return fmt.Errorf("failed to extract image %q: %v", config.URI, err)
49+
}
50+
51+
if config.Alias != "" {
52+
// Write an alias file inside
53+
aliasFile := filepath.Join(backendPath, "alias")
54+
if err := os.WriteFile(aliasFile, []byte(config.Alias), 0644); err != nil {
55+
return fmt.Errorf("failed to write alias file %q: %v", aliasFile, err)
56+
}
57+
}
58+
59+
return nil
60+
}
61+
62+
func DeleteBackendFromSystem(basePath string, name string) error {
63+
backendFile := filepath.Join(basePath, name)
64+
65+
return os.RemoveAll(backendFile)
66+
}
67+
68+
func ListSystemBackends(basePath string) (map[string]string, error) {
69+
backends, err := os.ReadDir(basePath)
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
backendsNames := make(map[string]string)
75+
76+
for _, backend := range backends {
77+
if backend.IsDir() {
78+
runFile := filepath.Join(basePath, backend.Name(), "run.sh")
79+
backendsNames[backend.Name()] = runFile
80+
81+
aliasFile := filepath.Join(basePath, backend.Name(), "alias")
82+
if _, err := os.Stat(aliasFile); err == nil {
83+
// read the alias file, and use it as key
84+
alias, err := os.ReadFile(aliasFile)
85+
if err != nil {
86+
return nil, err
87+
}
88+
backendsNames[string(alias)] = runFile
89+
}
90+
}
91+
}
92+
93+
return backendsNames, nil
94+
}
95+
96+
func RegisterBackends(basePath string, modelLoader *model.ModelLoader) error {
97+
backends, err := ListSystemBackends(basePath)
98+
if err != nil {
99+
return err
100+
}
101+
102+
for name, runFile := range backends {
103+
modelLoader.SetExternalBackend(name, runFile)
104+
}
105+
106+
return nil
107+
}

0 commit comments

Comments
 (0)