Skip to content

fix(model): deterministic, type-filtered backend auto-detection (#9287)#10286

Open
localai-bot wants to merge 3 commits into
masterfrom
fix/9287-backend-autodetect
Open

fix(model): deterministic, type-filtered backend auto-detection (#9287)#10286
localai-bot wants to merge 3 commits into
masterfrom
fix/9287-backend-autodetect

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Closes #9287

Problem

When a model config has no explicit backend:, (*ModelLoader).Load built the auto-detect candidate list by ranging an unordered Go map of installed backends with no filtering, then loaded the first one whose gRPC LoadModel succeeded. Every installed backend is registered there - including non-LLM ones like the opus audio codec - so after installing such a backend it could win a GGUF/LLM load, sending the model to the wrong backend.

Fix

New pure, unit-tested SelectAutoLoadBackends(available, modelFile):

  • deterministic sort (no more map-iteration randomness),
  • for .gguf files, filters to LLM-capable backends (chat/completion/edit/embeddings usecases via core/config.BackendCapabilities) with llama-cpp first,
  • zero-candidate fallback returns the full sorted set, so nothing previously loadable becomes unloadable.

Load() now calls this instead of ranging the map directly. (Verified pkg/model -> core/config introduces no import cycle.)

Test plan

  • New Ginkgo specs in pkg/model/autoload_test.go (red -> green): given {opus, llama-cpp} + a .gguf, opus is excluded and llama-cpp is first; deterministic order; zero-candidate fallback returns the original set.
  • go test ./pkg/model/... ./core/config/... green; scoped golangci-lint --new-from-merge-base clean.

Follow-up (noted, not done)

Did not force cfg.Backend = "llama-cpp" in the empty-backend GGUF hook (more blast radius on non-llama GGUFs); the candidate filter alone fixes the bug. A metadata-based GGUF architecture check is a possible refinement.

Assisted-by: claude:claude-opus-4-8 [Claude Code]

)

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>
@localai-bot

Copy link
Copy Markdown
Collaborator Author

Heads up: this branch is now 427 commits behind master and its last CI run is too old to re-run. I tried refreshing it against current master and hit a genuine, blocking architectural conflict, not a flaky/stale failure:

So a straight merge/rebase produces an import cycle:

package pkg/model
	imports core/config (from autoload.go)
	imports pkg/model (from core/config/runtime_settings_registry.go): import cycle not allowed

master alone has no cycle (its pkg/model/autoload.go does not import core/config); the cycle only appears when this PR's new pkg/model -> core/config edge meets master's core/config -> pkg/model edge. A branch refresh will not fix it.

To move forward this needs a real layering decision: either the auto-detect logic that needs core/config should not live in pkg/model (pass the needed config in from a higher layer, or use a small interface/type in pkg/model that core/config satisfies), or the dependency direction has to be inverted. Given how much the importer/auto-detect area has changed on master in 400+ commits, it may be worth re-evaluating whether the original #9287 approach is still the right shape before reworking. Flagging rather than force-pushing a broken merge.

localai-bot and others added 2 commits July 17, 2026 11:10
…uto-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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

If you add opus as a backend via api, then add a model via the api that should use llama.cpp or vllm, it then tries to use opus

2 participants