mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-15 04:08:55 -04:00
MLX backends passed request.Model verbatim to mlx_lm/mlx_vlm load(). For a model imported from the filesystem, LocalAI hands the backend a file:// URI (its LocalPrefix), which load() rejects: the scheme is neither a valid HF repo id nor an existing path (Path(model).exists() fails on the scheme), producing "Repo id must be in the form 'repo_name' or 'namespace/repo_name' ... Use repo_type argument if needed". Add a pure, unit-testable resolve_model_path(model, model_file) helper in the shared python_utils: it prefers the resolved ModelFile, strips a file:// scheme and percent-decodes the path, and leaves plain repo ids and local paths untouched. Wire it into the mlx, mlx-vlm and mlx-distributed backends (load, model_key, and the distributed broadcast all use the normalized path). Fixes #7461. Assisted-by: claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
"""Generic utilities shared across Python gRPC backends.
|
|
|
|
These helpers don't depend on any specific inference framework and can be
|
|
imported by any backend that needs to parse LocalAI gRPC options or build a
|
|
chat-template-compatible message list from proto Message objects.
|
|
"""
|
|
import json
|
|
from urllib.parse import unquote
|
|
|
|
|
|
def resolve_model_path(model, model_file=""):
|
|
"""Resolve a LocalAI model reference to something an HF/MLX loader accepts.
|
|
|
|
LocalAI hands backends either a plain HuggingFace repo id
|
|
(``namespace/name``), an already-local filesystem path, or a
|
|
``file://`` URI (its ``LocalPrefix``) for models imported from disk.
|
|
Loaders such as ``mlx_lm.load`` reject the ``file://`` form because the
|
|
scheme is neither a valid repo id nor an existing path, so we normalize
|
|
it here before loading.
|
|
|
|
Resolution order:
|
|
1. Prefer ``model_file`` when set and non-empty - that is the resolved
|
|
local path LocalAI computed for the model.
|
|
2. Strip a ``file://`` scheme and percent-decode it to a plain path.
|
|
3. Leave plain repo ids and already-local paths unchanged.
|
|
"""
|
|
candidate = model_file if model_file else model
|
|
if candidate is None:
|
|
return candidate
|
|
if candidate.startswith("file://"):
|
|
return unquote(candidate[len("file://"):])
|
|
return candidate
|
|
|
|
|
|
def parse_options(options_list):
|
|
"""Parse Options[] list of ``key:value`` strings into a dict.
|
|
|
|
Supports type inference for common cases (bool, int, float). Unknown or
|
|
mixed-case values are returned as strings.
|
|
|
|
Used by LoadModel to extract backend-specific options passed via
|
|
``ModelOptions.Options`` in ``backend.proto``.
|
|
"""
|
|
opts = {}
|
|
for opt in options_list:
|
|
if ":" not in opt:
|
|
continue
|
|
key, value = opt.split(":", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
# Try type conversion
|
|
if value.lower() in ("true", "false"):
|
|
opts[key] = value.lower() == "true"
|
|
else:
|
|
try:
|
|
opts[key] = int(value)
|
|
except ValueError:
|
|
try:
|
|
opts[key] = float(value)
|
|
except ValueError:
|
|
opts[key] = value
|
|
return opts
|
|
|
|
|
|
def messages_to_dicts(proto_messages):
|
|
"""Convert proto ``Message`` objects to dicts suitable for ``apply_chat_template``.
|
|
|
|
Handles: ``role``, ``content``, ``name``, ``tool_call_id``,
|
|
``reasoning_content``, ``tool_calls`` (JSON string → Python list).
|
|
|
|
HuggingFace chat templates (and their MLX/vLLM wrappers) expect a list of
|
|
plain dicts — proto Message objects don't work directly with Jinja, so
|
|
this conversion is needed before every ``apply_chat_template`` call.
|
|
"""
|
|
result = []
|
|
for msg in proto_messages:
|
|
d = {"role": msg.role, "content": msg.content or ""}
|
|
if msg.name:
|
|
d["name"] = msg.name
|
|
if msg.tool_call_id:
|
|
d["tool_call_id"] = msg.tool_call_id
|
|
if msg.reasoning_content:
|
|
d["reasoning_content"] = msg.reasoning_content
|
|
if msg.tool_calls:
|
|
try:
|
|
d["tool_calls"] = json.loads(msg.tool_calls)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
result.append(d)
|
|
return result
|