From dfaec3bd51314bc347e477984ea6c17f79a33aa9 Mon Sep 17 00:00:00 2001 From: Adira Date: Tue, 30 Jun 2026 11:21:08 +0300 Subject: [PATCH] fix(import): strip file:// scheme from model path for local imports (#10599) Importing a model from a local directory (e.g. a HuggingFace checkout or an LM Studio store) via a file:// URI produced a config whose model field kept the scheme verbatim, e.g. model: file:///Users/u/.../Qwen3-4bit. The mlx and vllm backends treat that field as a HuggingFace repo id or local path and reject the file:// form with "Repo id must be in the form 'repo_name' or 'namespace/repo_name'", so the model imported fine but failed to load (issue #7461). Add a shared LocalModelPath helper that reduces a file:// URI to the bare filesystem path it points at and leaves HuggingFace/HTTP URIs untouched, and route the mlx, vllm, transformers and diffusers importers (all of which pass details.URI straight into the model field for from_pretrained-style loading) through it. Cover the helper directly plus end-to-end file:// import specs for the mlx and vllm importers. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Adira Denis Muhando --- core/gallery/importers/diffuser.go | 2 +- core/gallery/importers/helpers.go | 15 +++++++++++++++ core/gallery/importers/helpers_test.go | 17 +++++++++++++++++ core/gallery/importers/mlx.go | 2 +- core/gallery/importers/mlx_test.go | 19 +++++++++++++++++++ core/gallery/importers/transformers.go | 2 +- core/gallery/importers/vllm.go | 2 +- core/gallery/importers/vllm_test.go | 17 +++++++++++++++++ 8 files changed, 72 insertions(+), 4 deletions(-) diff --git a/core/gallery/importers/diffuser.go b/core/gallery/importers/diffuser.go index c24a47955..a066941c6 100644 --- a/core/gallery/importers/diffuser.go +++ b/core/gallery/importers/diffuser.go @@ -101,7 +101,7 @@ func (i *DiffuserImporter) Import(details Details) (gallery.ModelConfig, error) Backend: backend, PredictionOptions: schema.PredictionOptions{ BasicModelRequest: schema.BasicModelRequest{ - Model: details.URI, + Model: LocalModelPath(details.URI), }, }, Diffusers: config.Diffusers{ diff --git a/core/gallery/importers/helpers.go b/core/gallery/importers/helpers.go index 5f5fe2b9a..03f90d963 100644 --- a/core/gallery/importers/helpers.go +++ b/core/gallery/importers/helpers.go @@ -4,9 +4,24 @@ import ( "path/filepath" "strings" + "github.com/mudler/LocalAI/pkg/downloader" hfapi "github.com/mudler/LocalAI/pkg/huggingface-api" ) +// LocalModelPath normalizes a model URI for backends that treat the model +// field as a HuggingFace repo id or local filesystem path (mlx, mlx-vlm, +// vllm, transformers, diffusers). A "file://" import URI is reduced to the +// bare path it points at: mlx-lm and vLLM otherwise mis-read the "file://" +// scheme as a repo id and fail with "Repo id must be in the form +// 'repo_name' or 'namespace/repo_name'" (issue #7461). HuggingFace and HTTP +// URIs are returned unchanged so the existing remote-load path is untouched. +func LocalModelPath(uri string) string { + if path, ok := strings.CutPrefix(uri, downloader.LocalPrefix); ok { + return path + } + return uri +} + // HasFile returns true when any file in files has exactly the given basename. // Directory components in file.Path are ignored — a nested // "sub/dir/config.json" is considered a match for name = "config.json". diff --git a/core/gallery/importers/helpers_test.go b/core/gallery/importers/helpers_test.go index 89217c689..390f5cb4e 100644 --- a/core/gallery/importers/helpers_test.go +++ b/core/gallery/importers/helpers_test.go @@ -86,4 +86,21 @@ var _ = Describe("importer helpers", func() { Expect(importers.HasGGMLFile(files, "ggml-")).To(BeFalse()) }) }) + + Describe("LocalModelPath", func() { + It("strips the file:// scheme from an absolute local path", func() { + Expect(importers.LocalModelPath("file:///Users/u/.lmstudio/models/mlx-community/Qwen3-4bit")). + To(Equal("/Users/u/.lmstudio/models/mlx-community/Qwen3-4bit")) + }) + It("strips the file:// scheme from a relative local path", func() { + Expect(importers.LocalModelPath("file://my-models/nvidia/Qwen3-30B-A3B-FP4")). + To(Equal("my-models/nvidia/Qwen3-30B-A3B-FP4")) + }) + It("leaves HuggingFace and HTTP URIs unchanged", func() { + Expect(importers.LocalModelPath("https://huggingface.co/mlx-community/test-model")). + To(Equal("https://huggingface.co/mlx-community/test-model")) + Expect(importers.LocalModelPath("mlx-community/test-model")). + To(Equal("mlx-community/test-model")) + }) + }) }) diff --git a/core/gallery/importers/mlx.go b/core/gallery/importers/mlx.go index 079ef4ed8..2698fe72f 100644 --- a/core/gallery/importers/mlx.go +++ b/core/gallery/importers/mlx.go @@ -87,7 +87,7 @@ func (i *MLXImporter) Import(details Details) (gallery.ModelConfig, error) { Backend: backend, PredictionOptions: schema.PredictionOptions{ BasicModelRequest: schema.BasicModelRequest{ - Model: details.URI, + Model: LocalModelPath(details.URI), }, }, TemplateConfig: config.TemplateConfig{ diff --git a/core/gallery/importers/mlx_test.go b/core/gallery/importers/mlx_test.go index dc4c1e6c2..2eeaef3fb 100644 --- a/core/gallery/importers/mlx_test.go +++ b/core/gallery/importers/mlx_test.go @@ -198,5 +198,24 @@ var _ = Describe("MLXImporter", func() { Expect(err).ToNot(HaveOccurred()) Expect(modelConfig.Name).To(Equal("model")) }) + + It("should emit a bare filesystem path for a file:// local import", func() { + // Regression for #7461: a model imported from a local directory + // (e.g. LM Studio's store) must not carry the file:// scheme into + // the model field — mlx-lm rejects it as an invalid repo id. + preferences := json.RawMessage(`{"backend": "mlx"}`) + details := importers.Details{ + URI: "file:///Users/u/.lmstudio/models/mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit", + Preferences: preferences, + } + + modelConfig, err := importer.Import(details) + + Expect(err).ToNot(HaveOccurred()) + Expect(modelConfig.Name).To(Equal("Qwen3-Coder-30B-A3B-Instruct-4bit")) + Expect(modelConfig.ConfigFile).To(ContainSubstring( + "model: /Users/u/.lmstudio/models/mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit")) + Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("model: file://")) + }) }) }) diff --git a/core/gallery/importers/transformers.go b/core/gallery/importers/transformers.go index b29da015e..76220c252 100644 --- a/core/gallery/importers/transformers.go +++ b/core/gallery/importers/transformers.go @@ -91,7 +91,7 @@ func (i *TransformersImporter) Import(details Details) (gallery.ModelConfig, err Backend: backend, PredictionOptions: schema.PredictionOptions{ BasicModelRequest: schema.BasicModelRequest{ - Model: details.URI, + Model: LocalModelPath(details.URI), }, }, TemplateConfig: config.TemplateConfig{ diff --git a/core/gallery/importers/vllm.go b/core/gallery/importers/vllm.go index f4cede7ad..bef700243 100644 --- a/core/gallery/importers/vllm.go +++ b/core/gallery/importers/vllm.go @@ -81,7 +81,7 @@ func (i *VLLMImporter) Import(details Details) (gallery.ModelConfig, error) { Backend: backend, PredictionOptions: schema.PredictionOptions{ BasicModelRequest: schema.BasicModelRequest{ - Model: details.URI, + Model: LocalModelPath(details.URI), }, }, TemplateConfig: config.TemplateConfig{ diff --git a/core/gallery/importers/vllm_test.go b/core/gallery/importers/vllm_test.go index b6eb5c953..e8dfef783 100644 --- a/core/gallery/importers/vllm_test.go +++ b/core/gallery/importers/vllm_test.go @@ -177,5 +177,22 @@ var _ = Describe("VLLMImporter", func() { Expect(modelConfig.ConfigFile).To(ContainSubstring("known_usecases:")) Expect(modelConfig.ConfigFile).To(ContainSubstring("- chat")) }) + + It("should emit a bare filesystem path for a file:// local import", func() { + // Regression for #7461: vLLM rejects a file:// model field as an + // invalid repo id, so a locally-imported model must carry the bare + // path instead. + preferences := json.RawMessage(`{"backend": "vllm"}`) + details := Details{ + URI: "file://my-models/nvidia/Qwen3-30B-A3B-FP4", + Preferences: preferences, + } + + modelConfig, err := importer.Import(details) + + Expect(err).ToNot(HaveOccurred()) + Expect(modelConfig.ConfigFile).To(ContainSubstring("model: my-models/nvidia/Qwen3-30B-A3B-FP4")) + Expect(modelConfig.ConfigFile).ToNot(ContainSubstring("model: file://")) + }) }) })