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 <dennisadira@gmail.com>
This commit is contained in:
Adira
2026-06-30 11:21:08 +03:00
committed by GitHub
parent 0e381897b5
commit dfaec3bd51
8 changed files with 72 additions and 4 deletions

View File

@@ -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{

View File

@@ -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".

View File

@@ -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"))
})
})
})

View File

@@ -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{

View File

@@ -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://"))
})
})
})

View File

@@ -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{

View File

@@ -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{

View File

@@ -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://"))
})
})
})