mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-20 10:35:01 +00:00
refactor: gallery inconsistencies (#2647)
* refactor(gallery): move under core/ Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(unarchive): do not allow symlinks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
parent
69206fcd4b
commit
a181dd0ebc
25 changed files with 93 additions and 54 deletions
|
@ -1,234 +0,0 @@
|
|||
package gallery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Gallery struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
}
|
||||
|
||||
// Installs a model from the gallery
|
||||
func InstallModelFromGallery(galleries []Gallery, name string, basePath string, req GalleryModel, downloadStatus func(string, string, string, float64)) error {
|
||||
|
||||
applyModel := func(model *GalleryModel) error {
|
||||
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
||||
|
||||
var config Config
|
||||
|
||||
if len(model.URL) > 0 {
|
||||
var err error
|
||||
config, err = GetGalleryConfigFromURL(model.URL, basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if len(model.ConfigFile) > 0 {
|
||||
// TODO: is this worse than using the override method with a blank cfg yaml?
|
||||
reYamlConfig, err := yaml.Marshal(model.ConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config = Config{
|
||||
ConfigFile: string(reYamlConfig),
|
||||
Description: model.Description,
|
||||
License: model.License,
|
||||
URLs: model.URLs,
|
||||
Name: model.Name,
|
||||
Files: make([]File, 0), // Real values get added below, must be blank
|
||||
// Prompt Template Skipped for now - I expect in this mode that they will be delivered as files.
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid gallery model %+v", model)
|
||||
}
|
||||
|
||||
installName := model.Name
|
||||
if req.Name != "" {
|
||||
installName = req.Name
|
||||
}
|
||||
|
||||
// Copy the model configuration from the request schema
|
||||
config.URLs = append(config.URLs, model.URLs...)
|
||||
config.Icon = model.Icon
|
||||
config.Files = append(config.Files, req.AdditionalFiles...)
|
||||
config.Files = append(config.Files, model.AdditionalFiles...)
|
||||
|
||||
// TODO model.Overrides could be merged with user overrides (not defined yet)
|
||||
if err := mergo.Merge(&model.Overrides, req.Overrides, mergo.WithOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := InstallModel(basePath, installName, &config, model.Overrides, downloadStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
models, err := AvailableGalleryModels(galleries, basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
model := FindModel(models, name, basePath)
|
||||
if model == nil {
|
||||
return fmt.Errorf("no model found with name %q", name)
|
||||
}
|
||||
|
||||
return applyModel(model)
|
||||
}
|
||||
|
||||
func FindModel(models []*GalleryModel, name string, basePath string) *GalleryModel {
|
||||
var model *GalleryModel
|
||||
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
||||
|
||||
if !strings.Contains(name, "@") {
|
||||
for _, m := range models {
|
||||
if strings.EqualFold(m.Name, name) {
|
||||
model = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
for _, m := range models {
|
||||
if strings.EqualFold(name, fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)) {
|
||||
model = m
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
// List available models
|
||||
// Models galleries are a list of yaml files that are hosted on a remote server (for example github).
|
||||
// Each yaml file contains a list of models that can be downloaded and optionally overrides to define a new model setting.
|
||||
func AvailableGalleryModels(galleries []Gallery, basePath string) ([]*GalleryModel, error) {
|
||||
var models []*GalleryModel
|
||||
|
||||
// Get models from galleries
|
||||
for _, gallery := range galleries {
|
||||
galleryModels, err := getGalleryModels(gallery, basePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models = append(models, galleryModels...)
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func findGalleryURLFromReferenceURL(url string, basePath string) (string, error) {
|
||||
var refFile string
|
||||
err := downloader.DownloadAndUnmarshal(url, basePath, func(url string, d []byte) error {
|
||||
refFile = string(d)
|
||||
if len(refFile) == 0 {
|
||||
return fmt.Errorf("invalid reference file at url %s: %s", url, d)
|
||||
}
|
||||
cutPoint := strings.LastIndex(url, "/")
|
||||
refFile = url[:cutPoint+1] + refFile
|
||||
return nil
|
||||
})
|
||||
return refFile, err
|
||||
}
|
||||
|
||||
func getGalleryModels(gallery Gallery, basePath string) ([]*GalleryModel, error) {
|
||||
var models []*GalleryModel = []*GalleryModel{}
|
||||
|
||||
if strings.HasSuffix(gallery.URL, ".ref") {
|
||||
var err error
|
||||
gallery.URL, err = findGalleryURLFromReferenceURL(gallery.URL, basePath)
|
||||
if err != nil {
|
||||
return models, err
|
||||
}
|
||||
}
|
||||
|
||||
err := downloader.DownloadAndUnmarshal(gallery.URL, basePath, func(url string, d []byte) error {
|
||||
return yaml.Unmarshal(d, &models)
|
||||
})
|
||||
if err != nil {
|
||||
if yamlErr, ok := err.(*yaml.TypeError); ok {
|
||||
log.Debug().Msgf("YAML errors: %s\n\nwreckage of models: %+v", strings.Join(yamlErr.Errors, "\n"), models)
|
||||
}
|
||||
return models, err
|
||||
}
|
||||
|
||||
// Add gallery to models
|
||||
for _, model := range models {
|
||||
model.Gallery = gallery
|
||||
// we check if the model was already installed by checking if the config file exists
|
||||
// TODO: (what to do if the model doesn't install a config file?)
|
||||
if _, err := os.Stat(filepath.Join(basePath, fmt.Sprintf("%s.yaml", model.Name))); err == nil {
|
||||
model.Installed = true
|
||||
}
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func GetLocalModelConfiguration(basePath string, name string) (*Config, error) {
|
||||
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
||||
galleryFile := filepath.Join(basePath, galleryFileName(name))
|
||||
return ReadConfigFile(galleryFile)
|
||||
}
|
||||
|
||||
func DeleteModelFromSystem(basePath string, name string, additionalFiles []string) error {
|
||||
// os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths.
|
||||
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
||||
|
||||
configFile := filepath.Join(basePath, fmt.Sprintf("%s.yaml", name))
|
||||
|
||||
galleryFile := filepath.Join(basePath, galleryFileName(name))
|
||||
|
||||
var err error
|
||||
// Delete all the files associated to the model
|
||||
// read the model config
|
||||
galleryconfig, err := ReadConfigFile(galleryFile)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to read gallery file %s", configFile)
|
||||
}
|
||||
|
||||
// Remove additional files
|
||||
if galleryconfig != nil {
|
||||
for _, f := range galleryconfig.Files {
|
||||
fullPath := filepath.Join(basePath, f.Filename)
|
||||
log.Debug().Msgf("Removing file %s", fullPath)
|
||||
if e := os.Remove(fullPath); e != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", f.Filename, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range additionalFiles {
|
||||
fullPath := filepath.Join(filepath.Join(basePath, f))
|
||||
log.Debug().Msgf("Removing additional file %s", fullPath)
|
||||
if e := os.Remove(fullPath); e != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", f, e))
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Removing model config file %s", configFile)
|
||||
|
||||
// Delete the model config file
|
||||
if e := os.Remove(configFile); e != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", configFile, e))
|
||||
}
|
||||
|
||||
// Delete gallery config file
|
||||
os.Remove(galleryFile)
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package gallery_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestGallery(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Gallery test suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
if os.Getenv("FIXTURES") == "" {
|
||||
Fail("FIXTURES env var not set")
|
||||
}
|
||||
})
|
|
@ -1,199 +0,0 @@
|
|||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
description: |
|
||||
foo
|
||||
license: ""
|
||||
|
||||
urls:
|
||||
-
|
||||
-
|
||||
|
||||
name: "bar"
|
||||
|
||||
config_file: |
|
||||
# Note, name will be injected. or generated by the alias wanted by the user
|
||||
threads: 14
|
||||
|
||||
files:
|
||||
- filename: ""
|
||||
sha: ""
|
||||
uri: ""
|
||||
|
||||
prompt_templates:
|
||||
- name: ""
|
||||
content: ""
|
||||
|
||||
*/
|
||||
// Config is the model configuration which contains all the model details
|
||||
// This configuration is read from the gallery endpoint and is used to download and install the model
|
||||
// It is the internal structure, separated from the request
|
||||
type Config struct {
|
||||
Description string `yaml:"description"`
|
||||
Icon string `yaml:"icon"`
|
||||
License string `yaml:"license"`
|
||||
URLs []string `yaml:"urls"`
|
||||
Name string `yaml:"name"`
|
||||
ConfigFile string `yaml:"config_file"`
|
||||
Files []File `yaml:"files"`
|
||||
PromptTemplates []PromptTemplate `yaml:"prompt_templates"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Filename string `yaml:"filename" json:"filename"`
|
||||
SHA256 string `yaml:"sha256" json:"sha256"`
|
||||
URI string `yaml:"uri" json:"uri"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}
|
||||
|
||||
func GetGalleryConfigFromURL(url string, basePath string) (Config, error) {
|
||||
var config Config
|
||||
err := downloader.DownloadAndUnmarshal(url, basePath, func(url string, d []byte) error {
|
||||
return yaml.Unmarshal(d, &config)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("url", url).Msg("failed to get gallery config for url")
|
||||
return config, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func ReadConfigFile(filePath string) (*Config, error) {
|
||||
// Read the YAML file
|
||||
yamlFile, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal YAML data into a Config struct
|
||||
var config Config
|
||||
err = yaml.Unmarshal(yamlFile, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal YAML: %v", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func InstallModel(basePath, nameOverride string, config *Config, configOverrides map[string]interface{}, downloadStatus func(string, string, string, float64)) error {
|
||||
// Create base path if it doesn't exist
|
||||
err := os.MkdirAll(basePath, 0750)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create base path: %v", err)
|
||||
}
|
||||
|
||||
if len(configOverrides) > 0 {
|
||||
log.Debug().Msgf("Config overrides %+v", configOverrides)
|
||||
}
|
||||
|
||||
// Download files and verify their SHA
|
||||
for i, file := range config.Files {
|
||||
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
|
||||
|
||||
if err := utils.VerifyPath(file.Filename, basePath); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create file path
|
||||
filePath := filepath.Join(basePath, file.Filename)
|
||||
|
||||
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, i, len(config.Files), downloadStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Write prompt template contents to separate files
|
||||
for _, template := range config.PromptTemplates {
|
||||
if err := utils.VerifyPath(template.Name+".tmpl", basePath); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create file path
|
||||
filePath := filepath.Join(basePath, template.Name+".tmpl")
|
||||
|
||||
// Create parent directory
|
||||
err := os.MkdirAll(filepath.Dir(filePath), 0750)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err)
|
||||
}
|
||||
// Create and write file content
|
||||
err = os.WriteFile(filePath, []byte(template.Content), 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write prompt template %q: %v", template.Name, err)
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Prompt template %q written", template.Name)
|
||||
}
|
||||
|
||||
name := config.Name
|
||||
if nameOverride != "" {
|
||||
name = nameOverride
|
||||
}
|
||||
|
||||
if err := utils.VerifyPath(name+".yaml", basePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write config file
|
||||
if len(configOverrides) != 0 || len(config.ConfigFile) != 0 {
|
||||
configFilePath := filepath.Join(basePath, name+".yaml")
|
||||
|
||||
// Read and update config file as map[string]interface{}
|
||||
configMap := make(map[string]interface{})
|
||||
err = yaml.Unmarshal([]byte(config.ConfigFile), &configMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal config YAML: %v", err)
|
||||
}
|
||||
|
||||
configMap["name"] = name
|
||||
|
||||
if err := mergo.Merge(&configMap, configOverrides, mergo.WithOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write updated config file
|
||||
updatedConfigYAML, err := yaml.Marshal(configMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal updated config YAML: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(configFilePath, updatedConfigYAML, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write updated config file: %v", err)
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Written config file %s", configFilePath)
|
||||
}
|
||||
|
||||
// Save the model gallery file for further reference
|
||||
modelFile := filepath.Join(basePath, galleryFileName(name))
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Written gallery file %s", modelFile)
|
||||
|
||||
return os.WriteFile(modelFile, data, 0600)
|
||||
|
||||
//return nil
|
||||
}
|
||||
|
||||
func galleryFileName(name string) string {
|
||||
return "._gallery_" + name + ".yaml"
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
package gallery_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/mudler/LocalAI/pkg/gallery"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var _ = Describe("Model test", func() {
|
||||
|
||||
Context("Downloading", func() {
|
||||
It("applies model correctly", func() {
|
||||
tempdir, err := os.MkdirTemp("", "test")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tempdir)
|
||||
c, err := ReadConfigFile(filepath.Join(os.Getenv("FIXTURES"), "gallery_simple.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = InstallModel(tempdir, "", c, map[string]interface{}{}, func(string, string, string, float64) {})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, f := range []string{"cerebras", "cerebras-completion.tmpl", "cerebras-chat.tmpl", "cerebras.yaml"} {
|
||||
_, err = os.Stat(filepath.Join(tempdir, f))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
content := map[string]interface{}{}
|
||||
|
||||
dat, err := os.ReadFile(filepath.Join(tempdir, "cerebras.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = yaml.Unmarshal(dat, content)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(content["context_size"]).To(Equal(1024))
|
||||
})
|
||||
|
||||
It("applies model from gallery correctly", func() {
|
||||
tempdir, err := os.MkdirTemp("", "test")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
gallery := []GalleryModel{{
|
||||
Name: "bert",
|
||||
URL: "https://raw.githubusercontent.com/go-skynet/model-gallery/main/bert-embeddings.yaml",
|
||||
}}
|
||||
out, err := yaml.Marshal(gallery)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
galleryFilePath := filepath.Join(tempdir, "gallery_simple.yaml")
|
||||
err = os.WriteFile(galleryFilePath, out, 0600)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.IsAbs(galleryFilePath)).To(BeTrue(), galleryFilePath)
|
||||
galleries := []Gallery{
|
||||
{
|
||||
Name: "test",
|
||||
URL: "file://" + galleryFilePath,
|
||||
},
|
||||
}
|
||||
|
||||
models, err := AvailableGalleryModels(galleries, tempdir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(models)).To(Equal(1))
|
||||
Expect(models[0].Name).To(Equal("bert"))
|
||||
Expect(models[0].URL).To(Equal("https://raw.githubusercontent.com/go-skynet/model-gallery/main/bert-embeddings.yaml"))
|
||||
Expect(models[0].Installed).To(BeFalse())
|
||||
|
||||
err = InstallModelFromGallery(galleries, "test@bert", tempdir, GalleryModel{}, func(s1, s2, s3 string, f float64) {})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
dat, err := os.ReadFile(filepath.Join(tempdir, "bert.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
content := map[string]interface{}{}
|
||||
err = yaml.Unmarshal(dat, &content)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(content["backend"]).To(Equal("bert-embeddings"))
|
||||
|
||||
models, err = AvailableGalleryModels(galleries, tempdir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(models)).To(Equal(1))
|
||||
Expect(models[0].Installed).To(BeTrue())
|
||||
|
||||
// delete
|
||||
err = DeleteModelFromSystem(tempdir, "bert", []string{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
models, err = AvailableGalleryModels(galleries, tempdir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(models)).To(Equal(1))
|
||||
Expect(models[0].Installed).To(BeFalse())
|
||||
|
||||
_, err = os.Stat(filepath.Join(tempdir, "bert.yaml"))
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("renames model correctly", func() {
|
||||
tempdir, err := os.MkdirTemp("", "test")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tempdir)
|
||||
c, err := ReadConfigFile(filepath.Join(os.Getenv("FIXTURES"), "gallery_simple.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = InstallModel(tempdir, "foo", c, map[string]interface{}{}, func(string, string, string, float64) {})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, f := range []string{"cerebras", "cerebras-completion.tmpl", "cerebras-chat.tmpl", "foo.yaml"} {
|
||||
_, err = os.Stat(filepath.Join(tempdir, f))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
})
|
||||
|
||||
It("overrides parameters", func() {
|
||||
tempdir, err := os.MkdirTemp("", "test")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tempdir)
|
||||
c, err := ReadConfigFile(filepath.Join(os.Getenv("FIXTURES"), "gallery_simple.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = InstallModel(tempdir, "foo", c, map[string]interface{}{"backend": "foo"}, func(string, string, string, float64) {})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, f := range []string{"cerebras", "cerebras-completion.tmpl", "cerebras-chat.tmpl", "foo.yaml"} {
|
||||
_, err = os.Stat(filepath.Join(tempdir, f))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
content := map[string]interface{}{}
|
||||
|
||||
dat, err := os.ReadFile(filepath.Join(tempdir, "foo.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = yaml.Unmarshal(dat, content)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
Expect(content["backend"]).To(Equal("foo"))
|
||||
})
|
||||
|
||||
It("catches path traversals", func() {
|
||||
tempdir, err := os.MkdirTemp("", "test")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tempdir)
|
||||
c, err := ReadConfigFile(filepath.Join(os.Getenv("FIXTURES"), "gallery_simple.yaml"))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = InstallModel(tempdir, "../../../foo", c, map[string]interface{}{}, func(string, string, string, float64) {})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,23 +0,0 @@
|
|||
package gallery
|
||||
|
||||
type GalleryOp struct {
|
||||
Id string
|
||||
GalleryModelName string
|
||||
ConfigURL string
|
||||
Delete bool
|
||||
|
||||
Req GalleryModel
|
||||
Galleries []Gallery
|
||||
}
|
||||
|
||||
type GalleryOpStatus struct {
|
||||
Deletion bool `json:"deletion"` // Deletion is true if the operation is a deletion
|
||||
FileName string `json:"file_name"`
|
||||
Error error `json:"error"`
|
||||
Processed bool `json:"processed"`
|
||||
Message string `json:"message"`
|
||||
Progress float64 `json:"progress"`
|
||||
TotalFileSize string `json:"file_size"`
|
||||
DownloadedFileSize string `json:"downloaded_size"`
|
||||
GalleryModelName string `json:"gallery_model_name"`
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GalleryModel is the struct used to represent a model in the gallery returned by the endpoint.
|
||||
// It is used to install the model by resolving the URL and downloading the files.
|
||||
// The other fields are used to override the configuration of the model.
|
||||
type GalleryModel struct {
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
License string `json:"license,omitempty" yaml:"license,omitempty"`
|
||||
URLs []string `json:"urls,omitempty" yaml:"urls,omitempty"`
|
||||
Icon string `json:"icon,omitempty" yaml:"icon,omitempty"`
|
||||
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
|
||||
// config_file is read in the situation where URL is blank - and therefore this is a base config.
|
||||
ConfigFile map[string]interface{} `json:"config_file,omitempty" yaml:"config_file,omitempty"`
|
||||
// Overrides are used to override the configuration of the model located at URL
|
||||
Overrides map[string]interface{} `json:"overrides,omitempty" yaml:"overrides,omitempty"`
|
||||
// AdditionalFiles are used to add additional files to the model
|
||||
AdditionalFiles []File `json:"files,omitempty" yaml:"files,omitempty"`
|
||||
// Gallery is a reference to the gallery which contains the model
|
||||
Gallery Gallery `json:"gallery,omitempty" yaml:"gallery,omitempty"`
|
||||
// Installed is used to indicate if the model is installed or not
|
||||
Installed bool `json:"installed,omitempty" yaml:"installed,omitempty"`
|
||||
}
|
||||
|
||||
func (m GalleryModel) ID() string {
|
||||
return fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
||||
}
|
||||
|
||||
type GalleryModels []*GalleryModel
|
||||
|
||||
func (gm GalleryModels) Search(term string) GalleryModels {
|
||||
var filteredModels GalleryModels
|
||||
|
||||
for _, m := range gm {
|
||||
if strings.Contains(m.Name, term) ||
|
||||
strings.Contains(m.Description, term) ||
|
||||
strings.Contains(m.Gallery.Name, term) ||
|
||||
strings.Contains(strings.Join(m.Tags, ","), term) {
|
||||
filteredModels = append(filteredModels, m)
|
||||
}
|
||||
}
|
||||
return filteredModels
|
||||
}
|
||||
|
||||
func (gm GalleryModels) FindByName(name string) *GalleryModel {
|
||||
for _, m := range gm {
|
||||
if strings.EqualFold(m.Name, name) {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package gallery_test
|
||||
|
||||
import (
|
||||
. "github.com/mudler/LocalAI/pkg/gallery"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Gallery API tests", func() {
|
||||
Context("requests", func() {
|
||||
It("parses github with a branch", func() {
|
||||
req := GalleryModel{URL: "github:go-skynet/model-gallery/gpt4all-j.yaml@main"}
|
||||
e, err := GetGalleryConfigFromURL(req.URL, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(e.Name).To(Equal("gpt4all-j"))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/klauspost/cpuid/v2"
|
||||
grpc "github.com/mudler/LocalAI/pkg/grpc"
|
||||
"github.com/mudler/LocalAI/pkg/library"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/mudler/LocalAI/pkg/xsysinfo"
|
||||
"github.com/phayes/freeport"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -309,6 +310,9 @@ func (ml *ModelLoader) grpcModel(backend string, o *Options) func(string, string
|
|||
}
|
||||
} else {
|
||||
grpcProcess := backendPath(o.assetDir, backend)
|
||||
if err := utils.VerifyPath(grpcProcess, o.assetDir); err != nil {
|
||||
return "", fmt.Errorf("grpc process not found in assetdir: %s", err.Error())
|
||||
}
|
||||
|
||||
if autoDetect {
|
||||
// autoDetect GRPC process to start based on system capabilities
|
||||
|
|
|
@ -7,9 +7,10 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/gallery"
|
||||
"github.com/mudler/LocalAI/embedded"
|
||||
"github.com/mudler/LocalAI/pkg/downloader"
|
||||
"github.com/mudler/LocalAI/pkg/gallery"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -17,7 +18,7 @@ import (
|
|||
// InstallModels will preload models from the given list of URLs and galleries
|
||||
// It will download the model if it is not already present in the model path
|
||||
// It will also try to resolve if the model is an embedded model YAML configuration
|
||||
func InstallModels(galleries []gallery.Gallery, modelLibraryURL string, modelPath string, downloadStatus func(string, string, string, float64), models ...string) error {
|
||||
func InstallModels(galleries []config.Gallery, modelLibraryURL string, modelPath string, downloadStatus func(string, string, string, float64), models ...string) error {
|
||||
// create an error that groups all errors
|
||||
var err error
|
||||
|
||||
|
@ -126,7 +127,7 @@ func InstallModels(galleries []gallery.Gallery, modelLibraryURL string, modelPat
|
|||
return err
|
||||
}
|
||||
|
||||
func installModel(galleries []gallery.Gallery, modelName, modelPath string, downloadStatus func(string, string, string, float64)) (error, bool) {
|
||||
func installModel(galleries []config.Gallery, modelName, modelPath string, downloadStatus func(string, string, string, float64)) (error, bool) {
|
||||
models, err := gallery.AvailableGalleryModels(galleries, modelPath)
|
||||
if err != nil {
|
||||
return err, false
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/gallery"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
. "github.com/mudler/LocalAI/pkg/startup"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
|
||||
|
@ -22,7 +22,7 @@ var _ = Describe("Preload test", func() {
|
|||
libraryURL := "https://raw.githubusercontent.com/mudler/LocalAI/master/embedded/model_library.yaml"
|
||||
fileName := fmt.Sprintf("%s.yaml", "1701d57f28d47552516c2b6ecc3cc719")
|
||||
|
||||
InstallModels([]gallery.Gallery{}, libraryURL, tmpdir, nil, "phi-2")
|
||||
InstallModels([]config.Gallery{}, libraryURL, tmpdir, nil, "phi-2")
|
||||
|
||||
resultFile := filepath.Join(tmpdir, fileName)
|
||||
|
||||
|
@ -38,7 +38,7 @@ var _ = Describe("Preload test", func() {
|
|||
url := "https://raw.githubusercontent.com/mudler/LocalAI/master/examples/configurations/phi-2.yaml"
|
||||
fileName := fmt.Sprintf("%s.yaml", utils.MD5(url))
|
||||
|
||||
InstallModels([]gallery.Gallery{}, "", tmpdir, nil, url)
|
||||
InstallModels([]config.Gallery{}, "", tmpdir, nil, url)
|
||||
|
||||
resultFile := filepath.Join(tmpdir, fileName)
|
||||
|
||||
|
@ -52,7 +52,7 @@ var _ = Describe("Preload test", func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
url := "phi-2"
|
||||
|
||||
InstallModels([]gallery.Gallery{}, "", tmpdir, nil, url)
|
||||
InstallModels([]config.Gallery{}, "", tmpdir, nil, url)
|
||||
|
||||
entry, err := os.ReadDir(tmpdir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
@ -70,7 +70,7 @@ var _ = Describe("Preload test", func() {
|
|||
url := "mistral-openorca"
|
||||
fileName := fmt.Sprintf("%s.yaml", utils.MD5(url))
|
||||
|
||||
InstallModels([]gallery.Gallery{}, "", tmpdir, nil, url)
|
||||
InstallModels([]config.Gallery{}, "", tmpdir, nil, url)
|
||||
|
||||
resultFile := filepath.Join(tmpdir, fileName)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package utils
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
)
|
||||
|
@ -52,5 +53,17 @@ func ExtractArchive(archive, dst string) error {
|
|||
case *archiver.TarZstd:
|
||||
v.Tar = mytar
|
||||
}
|
||||
|
||||
err = archiver.Walk(archive, func(f archiver.File) error {
|
||||
if f.FileInfo.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("archive contains a symlink")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return un.Unarchive(archive, dst)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue