fix(ollama): guard nil filter in galleryop.ListModels (#9817) (#9836)

The Ollama /api/tags handler passes a nil filter to galleryop.ListModels.
When ModelsPath contains any non-skipped loose file the function then
calls filter(name, nil) and panics, which Echo surfaces to clients as
"Server disconnected without sending a response" - the exact failure
Home Assistant's Ollama integration reports against LocalAI.

Mirror the nil guard already present in
ModelConfigLoader.GetModelConfigsByFilter so every caller is safe, and
add a regression test that exercises the loose-file path with a nil
filter.

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

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
LocalAI [bot]
2026-05-15 10:07:50 +02:00
committed by GitHub
parent 57fa178a64
commit c33d36b870
2 changed files with 72 additions and 0 deletions

View File

@@ -16,6 +16,14 @@ const (
func ListModels(bcl *config.ModelConfigLoader, ml *model.ModelLoader, filter config.ModelConfigFilterFn, looseFilePolicy LooseFilePolicy) ([]string, error) {
// Callers (e.g. the Ollama /api/tags handler) pass nil to mean "no
// filtering". Without this guard the loose-file loop below dereferences
// filter and panics, which Echo surfaces to clients as a dropped
// connection (see issue #9817).
if filter == nil {
filter = config.NoFilterFn
}
skipMap := map[string]struct{}{}
dataModels := []string{}

View File

@@ -0,0 +1,64 @@
package galleryop_test
import (
"os"
"path/filepath"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/services/galleryop"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/system"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// Regression test for issue #9817: the Ollama /api/tags handler calls
// ListModels with a nil filter, which used to panic as soon as a loose file
// existed under ModelsPath. The panic surfaced to Ollama clients (e.g. Home
// Assistant) as "Server disconnected without sending a response".
var _ = Describe("ListModels", func() {
var (
tempDir string
bcl *config.ModelConfigLoader
ml *model.ModelLoader
systemState *system.SystemState
)
BeforeEach(func() {
var err error
tempDir, err = os.MkdirTemp("", "list-models-test-*")
Expect(err).NotTo(HaveOccurred())
systemState, err = system.GetSystemState(system.WithModelPath(tempDir))
Expect(err).NotTo(HaveOccurred())
ml = model.NewModelLoader(systemState)
bcl = config.NewModelConfigLoader(tempDir)
})
AfterEach(func() {
os.RemoveAll(tempDir)
})
It("does not panic with a nil filter when loose files exist", func() {
// ListFilesInModelPath skips well-known weight-file extensions
// (.gguf, .bin, ...) so use an extension-less file to ensure the
// filter path is exercised.
Expect(os.WriteFile(filepath.Join(tempDir, "loose-model"), []byte("x"), 0o644)).To(Succeed())
var names []string
var err error
Expect(func() {
names, err = galleryop.ListModels(bcl, ml, nil, galleryop.SKIP_IF_CONFIGURED)
}).ToNot(Panic())
Expect(err).ToNot(HaveOccurred())
Expect(names).To(ContainElement("loose-model"))
})
It("does not panic with a nil filter when ModelsPath is empty", func() {
Expect(func() {
_, err := galleryop.ListModels(bcl, ml, nil, galleryop.SKIP_IF_CONFIGURED)
Expect(err).ToNot(HaveOccurred())
}).ToNot(Panic())
})
})