mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
fix(model): break core/config <-> pkg/model import cycle in backend auto-detect
The #9287 auto-detect change made pkg/model/autoload.go import core/config for the backend capability table. core/config already imports pkg/model (runtime_settings_registry.go uses model.DefaultWatchdogInterval), so this closed a core/config -> pkg/model -> core/config import cycle and broke the build and golangci-lint. Invert the dependency so the lower-level pkg/model no longer imports the higher-level core/config. pkg/model exposes RegisterLLMCapableBackendFunc and uses the registered predicate; core/config (which owns the capability table) registers it from an init(). The deterministic, GGUF-type-filtered selection behaviour is unchanged. When the predicate is unwired the GGUF filter is skipped, preserving the existing zero-candidate fallback. The unit test now injects a fake capability predicate so SelectAutoLoadBackends is exercised independently of the core/config table. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:opus-4.8 [Claude Code]
This commit is contained in:
committed by
localai-org-maint-bot
parent
fddd5558a0
commit
3c129686a9
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user