Skip to content
73 changes: 73 additions & 0 deletions core/gallery/backend_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package gallery

import (
"fmt"
"runtime"
"strings"

"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/pkg/system"
Expand Down Expand Up @@ -63,6 +65,77 @@ func (m *GalleryBackend) IsMeta() bool {
return len(m.CapabilitiesMap) > 0 && m.URI == ""
}

// IsCompatibleWith checks if the backend is compatible with the current system capability.
// For meta backends, it checks if any of the capabilities in the map match the system capability.
// For concrete backends, it infers compatibility from the backend name and URI.
func (m *GalleryBackend) IsCompatibleWith(systemState *system.SystemState) bool {
if systemState == nil {
return true
}

// Meta backends are compatible if the system capability matches one of the keys
if m.IsMeta() {
capability := systemState.Capability(m.CapabilitiesMap)
_, exists := m.CapabilitiesMap[capability]
return exists
}

// For concrete backends, infer compatibility from name and URI
name := strings.ToLower(m.Name)
uri := strings.ToLower(m.URI)
combined := name + " " + uri

// Check for darwin/macOS-specific backends (mlx, metal, darwin)
isDarwinBackend := strings.Contains(combined, "darwin") ||
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot keep the capabilities business logic in capabilities.go. use common constants ( defined and/or to be defined in pkg/system) instead of hardcoded strings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the backend compatibility business logic to pkg/system/capabilities.go and exported constants for use across packages. The IsCompatibleWith method now delegates to SystemState.IsBackendCompatible(name, uri string). Added backend detection tokens as constants (BackendTokenDarwin, BackendTokenMLX, BackendTokenCUDA, etc.). See commit d9f2c5a.

strings.Contains(combined, "mlx") ||
strings.Contains(combined, "metal")
if isDarwinBackend && runtime.GOOS != "darwin" {
return false
}

// Check for NVIDIA L4T-specific backends (arm64 Linux with NVIDIA GPU)
// This must be checked before the general NVIDIA check as L4T backends
// may also contain "cuda" or "nvidia" in their names
isL4TBackend := strings.Contains(combined, "l4t")
if isL4TBackend {
if runtime.GOOS != "linux" || runtime.GOARCH != "arm64" || systemState.GPUVendor != "nvidia" {
return false
}
return true
}

// Check for NVIDIA/CUDA-specific backends (non-L4T)
isNvidiaBackend := strings.Contains(combined, "cuda") ||
strings.Contains(combined, "nvidia")
if isNvidiaBackend {
if systemState.GPUVendor != "nvidia" {
return false
}
}

// Check for AMD/ROCm-specific backends
isAMDBackend := strings.Contains(combined, "rocm") ||
strings.Contains(combined, "hip") ||
strings.Contains(combined, "amd")
if isAMDBackend {
if systemState.GPUVendor != "amd" {
return false
}
}

// Check for Intel/SYCL-specific backends
isIntelBackend := strings.Contains(combined, "sycl") ||
strings.Contains(combined, "intel")
if isIntelBackend {
if systemState.GPUVendor != "intel" {
return false
}
}

// CPU backends are always compatible
return true
}

func (m *GalleryBackend) SetInstalled(installed bool) {
m.Installed = installed
}
Expand Down
72 changes: 72 additions & 0 deletions core/gallery/backends_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,78 @@ var _ = Describe("Gallery Backends", func() {
Expect(nilMetaBackend.IsMeta()).To(BeFalse())
})

It("should check IsCompatibleWith correctly for meta backends", func() {
metaBackend := &GalleryBackend{
Metadata: Metadata{
Name: "meta-backend",
},
CapabilitiesMap: map[string]string{
"nvidia": "nvidia-backend",
"amd": "amd-backend",
"default": "default-backend",
},
}

// Test with nil state - should be compatible
Expect(metaBackend.IsCompatibleWith(nil)).To(BeTrue())

// Test with NVIDIA system - should be compatible (has nvidia key)
nvidiaState := &system.SystemState{GPUVendor: "nvidia", VRAM: 8 * 1024 * 1024 * 1024}
Expect(metaBackend.IsCompatibleWith(nvidiaState)).To(BeTrue())

// Test with default (no GPU) - should be compatible (has default key)
defaultState := &system.SystemState{}
Expect(metaBackend.IsCompatibleWith(defaultState)).To(BeTrue())
})

It("should check IsCompatibleWith correctly for concrete backends", func() {
// MLX backend should only be compatible on darwin
mlxBackend := &GalleryBackend{
Metadata: Metadata{
Name: "mlx",
},
URI: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-mlx",
}

if runtime.GOOS == "darwin" {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I don't like switches in the middle of the tests. refactor this to have separate It( specs for darwin/non-darwin systems, and skip the entire block as necessary. Also take inspiration from backends/index.xml for adding more test-cases (for instance, intel)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored tests to use separate Describe/Context/When blocks with proper Skip logic for darwin/non-darwin systems. Added Intel/SYCL backend test cases including intel-sycl-f16-llama-cpp, intel-sycl-f32-llama-cpp, and intel-transformers. Also added CUDA13, HIP, Vulkan, and metal backend test cases. See commit f37f6d9.

Expect(mlxBackend.IsCompatibleWith(&system.SystemState{})).To(BeTrue())
} else {
Expect(mlxBackend.IsCompatibleWith(&system.SystemState{})).To(BeFalse())
}

// CPU backend should be compatible on all systems
cpuBackend := &GalleryBackend{
Metadata: Metadata{
Name: "cpu-llama-cpp",
},
URI: "quay.io/go-skynet/local-ai-backends:latest-cpu-llama-cpp",
}
Expect(cpuBackend.IsCompatibleWith(&system.SystemState{})).To(BeTrue())

// CUDA backend should only be compatible on nvidia systems
cudaBackend := &GalleryBackend{
Metadata: Metadata{
Name: "cuda12-llama-cpp",
},
URI: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-llama-cpp",
}
if runtime.GOOS != "darwin" {
// On non-darwin, cuda backend requires nvidia GPU
Expect(cudaBackend.IsCompatibleWith(&system.SystemState{})).To(BeFalse())
Expect(cudaBackend.IsCompatibleWith(&system.SystemState{GPUVendor: "nvidia", VRAM: 8 * 1024 * 1024 * 1024})).To(BeTrue())
}

// ROCm backend should only be compatible on AMD systems
rocmBackend := &GalleryBackend{
Metadata: Metadata{
Name: "rocm-llama-cpp",
},
URI: "quay.io/go-skynet/local-ai-backends:latest-gpu-rocm-hipblas-llama-cpp",
}
Expect(rocmBackend.IsCompatibleWith(&system.SystemState{})).To(BeFalse())
Expect(rocmBackend.IsCompatibleWith(&system.SystemState{GPUVendor: "amd", VRAM: 8 * 1024 * 1024 * 1024})).To(BeTrue())
})

It("should find best backend from meta based on system capabilities", func() {

metaBackend := &GalleryBackend{
Expand Down
22 changes: 21 additions & 1 deletion core/gallery/gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ func AvailableGalleryModels(galleries []config.Gallery, systemState *system.Syst

// 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)
Expand All @@ -241,7 +251,17 @@ func AvailableBackends(galleries []config.Gallery, systemState *system.SystemSta
if err != nil {
return nil, err
}
backends = append(backends, galleryBackends...)

// 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
Expand Down
7 changes: 7 additions & 0 deletions core/http/routes/ui_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,12 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
installedBackendsCount = len(installedBackends)
}

// Get the detected system capability
detectedCapability := ""
if appConfig.SystemState != nil {
detectedCapability = appConfig.SystemState.DetectedCapability()
}

return c.JSON(200, map[string]interface{}{
"backends": backendsJSON,
"repositories": appConfig.BackendGalleries,
Expand All @@ -629,6 +635,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
"totalPages": totalPages,
"prevPage": prevPage,
"nextPage": nextPage,
"systemCapability": detectedCapability,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot now let's make use of this and display in the webui view the capability that was detected of the system

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added display of the detected system capability in the webui. The hero section now shows "System: {capability}" with a microchip icon, fetching the systemCapability field from the API. See commit f37f6d9.

})
})

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ require (
go.opentelemetry.io/otel/metric v1.39.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.6.0
Expand All @@ -68,7 +69,6 @@ require (
github.com/labstack/gommon v0.4.2 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

require (
Expand Down
7 changes: 6 additions & 1 deletion pkg/system/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ func (s *SystemState) getSystemCapabilities() string {
return s.GPUVendor
}


// BackendPreferenceTokens returns a list of substrings that represent the preferred
// backend implementation order for the current system capability. Callers can use
// these tokens to select the most appropriate concrete backend among multiple
Expand All @@ -155,3 +154,9 @@ func (s *SystemState) BackendPreferenceTokens() []string {
return []string{"cpu"}
}
}

// DetectedCapability returns the detected system capability string.
// This can be used by the UI to display what capability was detected.
func (s *SystemState) DetectedCapability() string {
return s.getSystemCapabilities()
}
Loading