package gallery import ( "errors" "fmt" "os" "path/filepath" "strings" "dario.cat/mergo" "github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/pkg/downloader" "github.com/mudler/LocalAI/pkg/utils" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) // Installs a model from the gallery func InstallModelFromGallery(galleries []config.Gallery, name string, basePath string, req GalleryModel, downloadStatus func(string, string, string, float64), enforceScan bool) 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, enforceScan); 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 []config.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 uri := downloader.URI(url) err := uri.DownloadAndUnmarshal(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 config.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 } } uri := downloader.URI(gallery.URL) err := uri.DownloadAndUnmarshal(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)) for _, f := range []string{configFile, galleryFile} { if err := utils.VerifyPath(f, basePath); err != nil { return fmt.Errorf("failed to verify path %s: %w", f, err) } } 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) } var filesToRemove []string // Remove additional files if galleryconfig != nil { for _, f := range galleryconfig.Files { fullPath := filepath.Join(basePath, f.Filename) filesToRemove = append(filesToRemove, fullPath) } } for _, f := range additionalFiles { fullPath := filepath.Join(filepath.Join(basePath, f)) filesToRemove = append(filesToRemove, fullPath) } filesToRemove = append(filesToRemove, configFile) filesToRemove = append(filesToRemove, galleryFile) // skip duplicates filesToRemove = utils.Unique(filesToRemove) // Removing files for _, f := range filesToRemove { if e := os.Remove(f); e != nil { err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", f, e)) } } return err } // This is ***NEVER*** going to be perfect or finished. // This is a BEST EFFORT function to surface known-vulnerable models to users. func SafetyScanGalleryModels(galleries []config.Gallery, basePath string) error { galleryModels, err := AvailableGalleryModels(galleries, basePath) if err != nil { return err } for _, gM := range galleryModels { if gM.Installed { err = errors.Join(err, SafetyScanGalleryModel(gM)) } } return err } func SafetyScanGalleryModel(galleryModel *GalleryModel) error { for _, file := range galleryModel.AdditionalFiles { scanResults, err := downloader.HuggingFaceScan(downloader.URI(file.URI)) if err != nil && errors.Is(err, downloader.ErrUnsafeFilesFound) { log.Error().Str("model", galleryModel.Name).Strs("clamAV", scanResults.ClamAVInfectedFiles).Strs("pickles", scanResults.DangerousPickles).Msg("Contains unsafe file(s)!") return err } } return nil }