mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-30 19:37:00 -04:00
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:
@@ -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{
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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://"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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://"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user