mirror of
https://github.com/mudler/LocalAI.git
synced 2026-01-31 09:42:45 -05:00
* Initial plan * Add backend gallery filtering based on system capabilities Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Refactor L4T backend check to come before NVIDIA check Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Refactor: move capabilities business logic to capabilities.go and use constants Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * feat: display system capability in webui and refactor tests Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * chore: rename System/Capability Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactor: use getSystemCapabilities in IsBackendCompatible for consistency Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * refactor: keep unused constants private in capabilities.go Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * fix: skip AMD/ROCm and Intel/SYCL tests on darwin Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
346 lines
9.5 KiB
Go
346 lines
9.5 KiB
Go
package gallery
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/pkg/downloader"
|
|
"github.com/mudler/LocalAI/pkg/system"
|
|
"github.com/mudler/LocalAI/pkg/xsync"
|
|
"github.com/mudler/xlog"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
func GetGalleryConfigFromURL[T any](url string, basePath string) (T, error) {
|
|
var config T
|
|
uri := downloader.URI(url)
|
|
err := uri.ReadWithCallback(basePath, func(url string, d []byte) error {
|
|
return yaml.Unmarshal(d, &config)
|
|
})
|
|
if err != nil {
|
|
xlog.Error("failed to get gallery config for url", "error", err, "url", url)
|
|
return config, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func GetGalleryConfigFromURLWithContext[T any](ctx context.Context, url string, basePath string) (T, error) {
|
|
var config T
|
|
uri := downloader.URI(url)
|
|
err := uri.ReadWithAuthorizationAndCallback(ctx, basePath, "", func(url string, d []byte) error {
|
|
return yaml.Unmarshal(d, &config)
|
|
})
|
|
if err != nil {
|
|
xlog.Error("failed to get gallery config for url", "error", err, "url", url)
|
|
return config, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func ReadConfigFile[T any](filePath string) (*T, 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 T
|
|
err = yaml.Unmarshal(yamlFile, &config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal YAML: %v", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
type GalleryElement interface {
|
|
SetGallery(gallery config.Gallery)
|
|
SetInstalled(installed bool)
|
|
GetName() string
|
|
GetDescription() string
|
|
GetTags() []string
|
|
GetInstalled() bool
|
|
GetLicense() string
|
|
GetGallery() config.Gallery
|
|
}
|
|
|
|
type GalleryElements[T GalleryElement] []T
|
|
|
|
func (gm GalleryElements[T]) Search(term string) GalleryElements[T] {
|
|
var filteredModels GalleryElements[T]
|
|
term = strings.ToLower(term)
|
|
for _, m := range gm {
|
|
if fuzzy.Match(term, strings.ToLower(m.GetName())) ||
|
|
fuzzy.Match(term, strings.ToLower(m.GetGallery().Name)) ||
|
|
strings.Contains(strings.ToLower(m.GetName()), term) ||
|
|
strings.Contains(strings.ToLower(m.GetDescription()), term) ||
|
|
strings.Contains(strings.ToLower(m.GetGallery().Name), term) ||
|
|
strings.Contains(strings.ToLower(strings.Join(m.GetTags(), ",")), term) {
|
|
filteredModels = append(filteredModels, m)
|
|
}
|
|
}
|
|
|
|
return filteredModels
|
|
}
|
|
|
|
func (gm GalleryElements[T]) SortByName(sortOrder string) GalleryElements[T] {
|
|
sort.Slice(gm, func(i, j int) bool {
|
|
if sortOrder == "asc" {
|
|
return strings.ToLower(gm[i].GetName()) < strings.ToLower(gm[j].GetName())
|
|
} else {
|
|
return strings.ToLower(gm[i].GetName()) > strings.ToLower(gm[j].GetName())
|
|
}
|
|
})
|
|
return gm
|
|
}
|
|
|
|
func (gm GalleryElements[T]) SortByRepository(sortOrder string) GalleryElements[T] {
|
|
sort.Slice(gm, func(i, j int) bool {
|
|
if sortOrder == "asc" {
|
|
return strings.ToLower(gm[i].GetGallery().Name) < strings.ToLower(gm[j].GetGallery().Name)
|
|
} else {
|
|
return strings.ToLower(gm[i].GetGallery().Name) > strings.ToLower(gm[j].GetGallery().Name)
|
|
}
|
|
})
|
|
return gm
|
|
}
|
|
|
|
func (gm GalleryElements[T]) SortByLicense(sortOrder string) GalleryElements[T] {
|
|
sort.Slice(gm, func(i, j int) bool {
|
|
licenseI := gm[i].GetLicense()
|
|
licenseJ := gm[j].GetLicense()
|
|
var result bool
|
|
if licenseI == "" && licenseJ != "" {
|
|
return sortOrder == "desc"
|
|
} else if licenseI != "" && licenseJ == "" {
|
|
return sortOrder == "asc"
|
|
} else if licenseI == "" && licenseJ == "" {
|
|
return false
|
|
} else {
|
|
result = strings.ToLower(licenseI) < strings.ToLower(licenseJ)
|
|
}
|
|
if sortOrder == "desc" {
|
|
return !result
|
|
} else {
|
|
return result
|
|
}
|
|
})
|
|
return gm
|
|
}
|
|
|
|
func (gm GalleryElements[T]) SortByInstalled(sortOrder string) GalleryElements[T] {
|
|
sort.Slice(gm, func(i, j int) bool {
|
|
var result bool
|
|
// Sort by installed status: installed items first (true > false)
|
|
if gm[i].GetInstalled() != gm[j].GetInstalled() {
|
|
result = gm[i].GetInstalled()
|
|
} else {
|
|
result = strings.ToLower(gm[i].GetName()) < strings.ToLower(gm[j].GetName())
|
|
}
|
|
if sortOrder == "desc" {
|
|
return !result
|
|
} else {
|
|
return result
|
|
}
|
|
})
|
|
return gm
|
|
}
|
|
|
|
func (gm GalleryElements[T]) FindByName(name string) T {
|
|
for _, m := range gm {
|
|
if strings.EqualFold(m.GetName(), name) {
|
|
return m
|
|
}
|
|
}
|
|
var zero T
|
|
return zero
|
|
}
|
|
|
|
func (gm GalleryElements[T]) Paginate(pageNum int, itemsNum int) GalleryElements[T] {
|
|
start := (pageNum - 1) * itemsNum
|
|
end := start + itemsNum
|
|
if start > len(gm) {
|
|
start = len(gm)
|
|
}
|
|
if end > len(gm) {
|
|
end = len(gm)
|
|
}
|
|
return gm[start:end]
|
|
}
|
|
|
|
func FindGalleryElement[T GalleryElement](models []T, name string) T {
|
|
var model T
|
|
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
|
|
|
if !strings.Contains(name, "@") {
|
|
for _, m := range models {
|
|
if strings.EqualFold(strings.ToLower(m.GetName()), strings.ToLower(name)) {
|
|
model = m
|
|
break
|
|
}
|
|
}
|
|
|
|
} else {
|
|
for _, m := range models {
|
|
if strings.EqualFold(strings.ToLower(name), strings.ToLower(fmt.Sprintf("%s@%s", m.GetGallery().Name, m.GetName()))) {
|
|
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, systemState *system.SystemState) (GalleryElements[*GalleryModel], error) {
|
|
var models []*GalleryModel
|
|
|
|
// Get models from galleries
|
|
for _, gallery := range galleries {
|
|
galleryModels, err := getGalleryElements(gallery, systemState.Model.ModelsPath, func(model *GalleryModel) bool {
|
|
if _, err := os.Stat(filepath.Join(systemState.Model.ModelsPath, fmt.Sprintf("%s.yaml", model.GetName()))); err == nil {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
models = append(models, galleryModels...)
|
|
}
|
|
|
|
return models, nil
|
|
}
|
|
|
|
// List available backends
|
|
func AvailableBackends(galleries []config.Gallery, systemState *system.SystemState) (GalleryElements[*GalleryBackend], error) {
|
|
return availableBackendsWithFilter(galleries, systemState, true)
|
|
}
|
|
|
|
// AvailableBackendsUnfiltered returns all available backends without filtering by system capability.
|
|
func AvailableBackendsUnfiltered(galleries []config.Gallery, systemState *system.SystemState) (GalleryElements[*GalleryBackend], error) {
|
|
return availableBackendsWithFilter(galleries, systemState, false)
|
|
}
|
|
|
|
// availableBackendsWithFilter is a helper function that lists available backends with optional filtering.
|
|
func availableBackendsWithFilter(galleries []config.Gallery, systemState *system.SystemState, filterByCapability bool) (GalleryElements[*GalleryBackend], error) {
|
|
var backends []*GalleryBackend
|
|
|
|
systemBackends, err := ListSystemBackends(systemState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get backends from galleries
|
|
for _, gallery := range galleries {
|
|
galleryBackends, err := getGalleryElements(gallery, systemState.Backend.BackendsPath, func(backend *GalleryBackend) bool {
|
|
return systemBackends.Exists(backend.GetName())
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Filter backends by system capability if requested
|
|
if filterByCapability {
|
|
for _, backend := range galleryBackends {
|
|
if backend.IsCompatibleWith(systemState) {
|
|
backends = append(backends, backend)
|
|
}
|
|
}
|
|
} else {
|
|
backends = append(backends, galleryBackends...)
|
|
}
|
|
}
|
|
|
|
return backends, nil
|
|
}
|
|
|
|
func findGalleryURLFromReferenceURL(url string, basePath string) (string, error) {
|
|
var refFile string
|
|
uri := downloader.URI(url)
|
|
err := uri.ReadWithCallback(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
|
|
}
|
|
|
|
type galleryCacheEntry struct {
|
|
yamlEntry []byte
|
|
lastUpdated time.Time
|
|
}
|
|
|
|
func (entry galleryCacheEntry) hasExpired() bool {
|
|
return entry.lastUpdated.Before(time.Now().Add(-1 * time.Hour))
|
|
}
|
|
|
|
var galleryCache = xsync.NewSyncedMap[string, galleryCacheEntry]()
|
|
|
|
func getGalleryElements[T GalleryElement](gallery config.Gallery, basePath string, isInstalledCallback func(T) bool) ([]T, error) {
|
|
var models []T = []T{}
|
|
|
|
if strings.HasSuffix(gallery.URL, ".ref") {
|
|
var err error
|
|
gallery.URL, err = findGalleryURLFromReferenceURL(gallery.URL, basePath)
|
|
if err != nil {
|
|
return models, err
|
|
}
|
|
}
|
|
|
|
cacheKey := fmt.Sprintf("%s-%s", gallery.Name, gallery.URL)
|
|
if galleryCache.Exists(cacheKey) {
|
|
entry := galleryCache.Get(cacheKey)
|
|
// refresh if last updated is more than 1 hour ago
|
|
if !entry.hasExpired() {
|
|
err := yaml.Unmarshal(entry.yamlEntry, &models)
|
|
if err != nil {
|
|
return models, err
|
|
}
|
|
} else {
|
|
galleryCache.Delete(cacheKey)
|
|
}
|
|
}
|
|
|
|
uri := downloader.URI(gallery.URL)
|
|
|
|
if len(models) == 0 {
|
|
err := uri.ReadWithCallback(basePath, func(url string, d []byte) error {
|
|
galleryCache.Set(cacheKey, galleryCacheEntry{
|
|
yamlEntry: d,
|
|
lastUpdated: time.Now(),
|
|
})
|
|
return yaml.Unmarshal(d, &models)
|
|
})
|
|
if err != nil {
|
|
if yamlErr, ok := err.(*yaml.TypeError); ok {
|
|
xlog.Debug("YAML errors", "errors", strings.Join(yamlErr.Errors, "\n"), "models", models)
|
|
}
|
|
return models, fmt.Errorf("failed to read gallery elements: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add gallery to models
|
|
for _, model := range models {
|
|
model.SetGallery(gallery)
|
|
model.SetInstalled(isInstalledCallback(model))
|
|
}
|
|
return models, nil
|
|
}
|