mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-13 17:54:02 -04:00
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>
107 lines
3.5 KiB
Go
107 lines
3.5 KiB
Go
package importers
|
|
|
|
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".
|
|
func HasFile(files []hfapi.ModelFile, name string) bool {
|
|
for _, f := range files {
|
|
if filepath.Base(f.Path) == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasExtension returns true when any file has the given extension
|
|
// (case-insensitive). ext must include the leading dot, e.g. ".onnx".
|
|
func HasExtension(files []hfapi.ModelFile, ext string) bool {
|
|
lower := strings.ToLower(ext)
|
|
for _, f := range files {
|
|
if strings.HasSuffix(strings.ToLower(f.Path), lower) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasONNX returns true when any file ends in .onnx (case-insensitive).
|
|
func HasONNX(files []hfapi.ModelFile) bool {
|
|
return HasExtension(files, ".onnx")
|
|
}
|
|
|
|
// HasONNXConfigPair returns true when an .onnx file has an accompanying
|
|
// "<same basename>.onnx.json" file. This is the piper voice packaging
|
|
// convention, e.g. en_US-amy-medium.onnx + en_US-amy-medium.onnx.json.
|
|
func HasONNXConfigPair(files []hfapi.ModelFile) bool {
|
|
paths := make(map[string]struct{}, len(files))
|
|
for _, f := range files {
|
|
paths[strings.ToLower(f.Path)] = struct{}{}
|
|
}
|
|
for p := range paths {
|
|
if !strings.HasSuffix(p, ".onnx") {
|
|
continue
|
|
}
|
|
if _, ok := paths[p+".json"]; ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HFOwnerRepoFromURI extracts the "owner", "repo" pair from an HF URI.
|
|
// Accepted prefixes: "https://huggingface.co/", "huggingface://", "hf://".
|
|
// Returns ok=false when the URI is not an HF URI or is missing either
|
|
// component. This exists so importers can fall back to URI-based matching
|
|
// when pkg/huggingface-api's recursive tree listing errors out on repos
|
|
// with nested subdirectories (a known pre-existing bug).
|
|
func HFOwnerRepoFromURI(uri string) (owner, repo string, ok bool) {
|
|
stripped := uri
|
|
for _, pfx := range []string{"https://huggingface.co/", "huggingface://", "hf://"} {
|
|
stripped = strings.TrimPrefix(stripped, pfx)
|
|
}
|
|
parts := strings.SplitN(stripped, "/", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return "", "", false
|
|
}
|
|
return parts[0], parts[1], true
|
|
}
|
|
|
|
// HasGGMLFile returns true when any file matches "<prefix>*.bin", which is
|
|
// the whisper.cpp packaging convention (e.g. "ggml-base.en.bin"). Both prefix
|
|
// and suffix match is case-sensitive on prefix and case-insensitive on the
|
|
// .bin extension.
|
|
func HasGGMLFile(files []hfapi.ModelFile, prefix string) bool {
|
|
for _, f := range files {
|
|
name := filepath.Base(f.Path)
|
|
if !strings.HasPrefix(name, prefix) {
|
|
continue
|
|
}
|
|
if strings.HasSuffix(strings.ToLower(name), ".bin") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|