mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
* fix(model): deterministic, file-type-filtered backend auto-detect (#9287) When a model config declares no explicit `backend:`, Load() fell into a trial loop built by ranging the external-backends Go map (random order) with no filtering, returning the first backend whose gRPC LoadModel succeeded. An unrelated installed backend - e.g. the "opus" audio codec - could therefore win a GGUF/LLM model load, so a model that should run on llama.cpp wrongly tried to use opus. Extract the candidate selection into a pure, testable function SelectAutoLoadBackends that: - sorts the candidate list deterministically (no more map-order nondeterminism), and - for a `.gguf` model, filters to LLM-capable backends (via core/config.BackendCapabilities) and puts llama-cpp first, so an incompatible audio/codec/image backend can never win the trial loop. If filtering would leave zero candidates, the full sorted set is returned unchanged, so a previously-loadable model is never made unloadable. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * 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] --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com>
98 lines
3.8 KiB
Go
98 lines
3.8 KiB
Go
package model
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// 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"
|
|
|
|
// 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
|
|
// names to try when loading a model that declares no explicit backend.
|
|
//
|
|
// available is the set of installed backend names (unordered, as it comes from a
|
|
// Go map). modelFile is the model file name/path (may be empty).
|
|
//
|
|
// The trial loop in (*ModelLoader).Load picks the first backend whose gRPC
|
|
// LoadModel succeeds, so the order and membership of this list directly decide
|
|
// which backend wins. The previous implementation ranged a Go map (random
|
|
// order) with no filtering, so an unrelated installed backend such as the
|
|
// "opus" audio codec could win a GGUF/LLM model load (#9287).
|
|
//
|
|
// Behaviour:
|
|
// - The result is always deterministically ordered, so auto-detect no longer
|
|
// depends on map iteration order.
|
|
// - For a GGUF model file the list is filtered to LLM-capable backends and
|
|
// llama-cpp is placed first, so an incompatible audio/codec/image backend
|
|
// can never win the trial loop.
|
|
// - If filtering would leave no candidate, the full sorted set is returned
|
|
// instead, so a model that previously loaded never becomes unloadable.
|
|
func SelectAutoLoadBackends(available []string, modelFile string) []string {
|
|
sorted := append([]string(nil), available...)
|
|
sort.Strings(sorted)
|
|
|
|
if !isGGUFModelFile(modelFile) {
|
|
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 {
|
|
if b == preferredGGUFBackend {
|
|
hasLlama = true
|
|
continue // added explicitly first below
|
|
}
|
|
if isLLMCapableBackend(b) {
|
|
filtered = append(filtered, b)
|
|
}
|
|
}
|
|
if hasLlama {
|
|
filtered = append([]string{preferredGGUFBackend}, filtered...)
|
|
}
|
|
|
|
if len(filtered) == 0 {
|
|
// Conservative fallback: no known LLM-capable backend is installed, so
|
|
// rather than refuse to load, fall back to the previous behaviour of
|
|
// trying every installed backend (now at least in a deterministic order).
|
|
return sorted
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func isGGUFModelFile(modelFile string) bool {
|
|
return strings.HasSuffix(strings.ToLower(modelFile), ".gguf")
|
|
}
|
|
|
|
// isLLMCapableBackend reports whether a backend is known to serve text/LLM
|
|
// 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. Callers must ensure llmCapableBackend is non-nil.
|
|
func isLLMCapableBackend(name string) bool {
|
|
return llmCapableBackend(name)
|
|
}
|