From c33d36b870f5c3f61dd149f976248c88ec767893 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Fri, 15 May 2026 10:07:50 +0200 Subject: [PATCH] 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 Co-authored-by: Ettore Di Giacinto --- core/services/galleryop/list_models.go | 8 +++ core/services/galleryop/list_models_test.go | 64 +++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 core/services/galleryop/list_models_test.go diff --git a/core/services/galleryop/list_models.go b/core/services/galleryop/list_models.go index 840d91f6b..47c2d91e5 100644 --- a/core/services/galleryop/list_models.go +++ b/core/services/galleryop/list_models.go @@ -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{} diff --git a/core/services/galleryop/list_models_test.go b/core/services/galleryop/list_models_test.go new file mode 100644 index 000000000..99e6e11de --- /dev/null +++ b/core/services/galleryop/list_models_test.go @@ -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()) + }) +})