diff --git a/core/config/backend_capabilities.go b/core/config/backend_capabilities.go index 79687c302..946527f4e 100644 --- a/core/config/backend_capabilities.go +++ b/core/config/backend_capabilities.go @@ -3,6 +3,8 @@ package config import ( "slices" "strings" + + "github.com/mudler/LocalAI/pkg/model" ) // Usecase name constants — the canonical string values used in gallery entries, @@ -718,6 +720,42 @@ func GetBackendCapability(backend string) *BackendCapability { return nil } +// llmAutoLoadUsecases are the usecases that mark a backend able to serve a +// text/LLM GGUF model. A GGUF model that declares no explicit backend must only +// be auto-tried against backends carrying one of these usecases - never against +// audio/codec/image backends (e.g. opus) that happen to be installed alongside +// it (see issue #9287). +var llmAutoLoadUsecases = []string{ + UsecaseChat, + UsecaseCompletion, + UsecaseEdit, + UsecaseEmbeddings, +} + +// isLLMCapableForAutoLoad reports whether the named backend is known to serve +// text/LLM models, for pkg/model's GGUF backend auto-detection (#9287). Backends +// absent from the capability table are treated as not LLM-capable. +func isLLMCapableForAutoLoad(name string) bool { + capability := GetBackendCapability(name) + if capability == nil { + return false + } + for _, u := range capability.PossibleUsecases { + if slices.Contains(llmAutoLoadUsecases, u) { + return true + } + } + return false +} + +func init() { + // Wire the LLM-capability filter into pkg/model's GGUF backend + // auto-detection. pkg/model is a lower-level package and must not import + // core/config (that would form a core/config -> pkg/model -> core/config + // import cycle), so core/config registers the predicate here instead (#9287). + model.RegisterLLMCapableBackendFunc(isLLMCapableForAutoLoad) +} + // VoiceCloningForModel returns the reference-audio contract only when the // installed model variant can honor it. Several backends serve both Base // (voice cloning) and CustomVoice/VoiceDesign models, so backend name alone is diff --git a/pkg/model/autoload.go b/pkg/model/autoload.go index d6af3e095..d53186283 100644 --- a/pkg/model/autoload.go +++ b/pkg/model/autoload.go @@ -1,27 +1,28 @@ package model import ( - "slices" "sort" "strings" - - "github.com/mudler/LocalAI/core/config" ) // preferredGGUFBackend is tried first when auto-detecting the backend for a // GGUF model, since GGUF is overwhelmingly llama.cpp's native format. const preferredGGUFBackend = "llama-cpp" -// llmCapableUsecases are the BackendCapabilities usecases that signal a backend -// can serve a text/LLM GGUF model. A GGUF model that declares no explicit -// backend must only be auto-tried against backends carrying one of these -// usecases - never against audio/codec/image backends (e.g. opus) that happen -// to be installed alongside it (see issue #9287). -var llmCapableUsecases = []string{ - config.UsecaseChat, - config.UsecaseCompletion, - config.UsecaseEdit, - config.UsecaseEmbeddings, +// llmCapableBackend reports whether the named backend can serve a text/LLM GGUF +// model. The backend capability table lives in core/config, which is a +// higher-level package that already imports pkg/model; importing it back here +// would form a core/config -> pkg/model -> core/config cycle. So core/config +// registers the predicate via RegisterLLMCapableBackendFunc instead (see #9287). +// When unset (e.g. a build that never imports core/config) GGUF capability +// filtering is skipped and auto-detect falls back to the deterministic set. +var llmCapableBackend func(name string) bool + +// RegisterLLMCapableBackendFunc wires the LLM-capability predicate used by +// SelectAutoLoadBackends. It is called from core/config's init so pkg/model +// need not import core/config (see #9287). +func RegisterLLMCapableBackendFunc(fn func(name string) bool) { + llmCapableBackend = fn } // SelectAutoLoadBackends returns the ordered, deterministic list of backend @@ -52,6 +53,12 @@ func SelectAutoLoadBackends(available []string, modelFile string) []string { return sorted } + // No capability predicate wired (core/config not linked in): skip filtering + // rather than risk dropping a valid candidate. + if llmCapableBackend == nil { + return sorted + } + filtered := make([]string, 0, len(sorted)) hasLlama := false for _, b := range sorted { @@ -84,16 +91,7 @@ func isGGUFModelFile(modelFile string) bool { // models. Backends absent from the capability map (unknown) are treated as // not LLM-capable here: for GGUF auto-detection we only want backends we can // positively confirm handle LLMs, and the zero-candidate fallback keeps unknown -// setups working. +// setups working. Callers must ensure llmCapableBackend is non-nil. func isLLMCapableBackend(name string) bool { - capability := config.GetBackendCapability(name) - if capability == nil { - return false - } - for _, u := range capability.PossibleUsecases { - if slices.Contains(llmCapableUsecases, u) { - return true - } - } - return false + return llmCapableBackend(name) } diff --git a/pkg/model/autoload_test.go b/pkg/model/autoload_test.go index f191969b4..9142639af 100644 --- a/pkg/model/autoload_test.go +++ b/pkg/model/autoload_test.go @@ -7,6 +7,15 @@ import ( . "github.com/onsi/gomega" ) +func init() { + // The real LLM-capability table lives in core/config, which pkg/model must + // not import (cycle). Register a fake predicate so the selection algorithm + // is exercised here independent of the capability table (#9287). + model.RegisterLLMCapableBackendFunc(func(name string) bool { + return name == "llama-cpp" || name == "vllm" + }) +} + var _ = Describe("SelectAutoLoadBackends (#9287)", func() { Describe("GGUF model auto-detection", func() { It("excludes incompatible audio/codec backends (e.g. opus) for a .gguf model", func() {