diff --git a/studio/backend/core/inference/orchestrator.py b/studio/backend/core/inference/orchestrator.py index 6321ca892d..9810f2b9da 100644 --- a/studio/backend/core/inference/orchestrator.py +++ b/studio/backend/core/inference/orchestrator.py @@ -705,6 +705,7 @@ class InferenceOrchestrator: self.models[self.active_model_name] = { "is_vision": model_info.get("is_vision", False), "is_lora": model_info.get("is_lora", False), + "is_mlx": model_info.get("is_mlx", False), "display_name": model_info.get("display_name", model_name), "is_audio": model_info.get("is_audio", False), "audio_type": model_info.get("audio_type"), diff --git a/studio/backend/core/inference/worker.py b/studio/backend/core/inference/worker.py index d01cce4fc1..31fd55a156 100644 --- a/studio/backend/core/inference/worker.py +++ b/studio/backend/core/inference/worker.py @@ -342,6 +342,8 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None: "is_vision": mc.is_vision, "is_lora": mc.is_lora, "is_gguf": False, + # MLX backend sets device="mlx"; lets the UI tag MLX models. + "is_mlx": getattr(backend, "device", None) == "mlx", "is_audio": getattr(mc, "is_audio", False), "audio_type": getattr(mc, "audio_type", None), "has_audio_input": getattr(mc, "has_audio_input", False), diff --git a/studio/backend/models/models.py b/studio/backend/models/models.py index 46ca4e3784..a53569aaa1 100644 --- a/studio/backend/models/models.py +++ b/studio/backend/models/models.py @@ -76,6 +76,9 @@ class ModelDetails(BaseModel): is_gguf: bool = Field( False, description = "Whether model is a GGUF model (llama.cpp format)" ) + is_mlx: bool = Field( + False, description = "Whether model is served via the MLX backend (Apple Silicon)" + ) is_audio: bool = Field(False, description = "Whether model is a TTS audio model") audio_type: Optional[str] = Field( None, description = "Audio codec type: snac, csm, bicodec, dac" diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index 9ea113e488..2b729eea9c 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -1446,6 +1446,15 @@ async def browse_folders( ) +def _looks_like_mlx_repo(model_id: str) -> bool: + """Name heuristic for unloaded models, mirrors the -GGUF suffix check. + Tokenized so MLX only matches as a whole name segment.""" + if model_id.lower().startswith("mlx-community/"): + return True + tail = model_id.split("/")[-1] + return "MLX" in _re.split(r"[-_.]", tail.upper()) + + @router.get("/list") async def list_models( current_subject: str = Depends(get_current_subject), @@ -1471,6 +1480,7 @@ async def list_models( name = model_name.split("/")[-1] if "/" in model_name else model_name, is_vision = _is_vision, is_lora = model_data.get("is_lora", False), + is_mlx = model_data.get("is_mlx", False), is_audio = model_data.get("is_audio", False), audio_type = _audio_type, has_audio_input = model_data.get("has_audio_input", False), @@ -1498,13 +1508,18 @@ async def list_models( all_models = [] seen_ids = set() + # Prefer loaded entries for duplicate ids so runtime flags + # (is_mlx, is_vision, is_audio, ...) are not lost. + loaded_by_id = {model_info.id: model_info for model_info in loaded_models} + # Add default models for model_id in default_models: if model_id not in seen_ids: - model_info = ModelDetails( + model_info = loaded_by_id.get(model_id) or ModelDetails( id = model_id, name = model_id.split("/")[-1] if "/" in model_id else model_id, is_gguf = model_id.upper().endswith("-GGUF"), + is_mlx = _looks_like_mlx_repo(model_id), ) all_models.append(model_info) seen_ids.add(model_id) diff --git a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts index e6892b34fd..d1dab924cb 100644 --- a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts +++ b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts @@ -94,16 +94,25 @@ function describeModel(model: { is_lora?: boolean; is_vision?: boolean; is_gguf?: boolean; + is_mlx?: boolean; is_audio?: boolean; has_audio_input?: boolean; }): string | undefined { const tags: string[] = []; if (model.is_gguf) tags.push("GGUF"); + if (model.is_mlx) tags.push("MLX"); if (model.is_lora) tags.push("LoRA"); if (model.is_vision) tags.push("Vision"); if (model.is_audio) tags.push("Audio"); if (model.has_audio_input) tags.push("Audio Input"); - if (!model.is_lora && !model.is_vision && !model.is_gguf && !model.is_audio && !model.has_audio_input) + if ( + !model.is_lora && + !model.is_vision && + !model.is_gguf && + !model.is_mlx && + !model.is_audio && + !model.has_audio_input + ) tags.push("Base"); return tags.join(" ยท "); } @@ -114,6 +123,7 @@ function toChatModelSummary(model: { is_lora?: boolean; is_vision?: boolean; is_gguf?: boolean; + is_mlx?: boolean; is_audio?: boolean; audio_type?: string | null; has_audio_input?: boolean; @@ -125,6 +135,7 @@ function toChatModelSummary(model: { isLora: Boolean(model.is_lora), isVision: Boolean(model.is_vision), isGguf: Boolean(model.is_gguf), + isMlx: Boolean(model.is_mlx), isAudio: Boolean(model.is_audio), audioType: model.audio_type ?? null, hasAudioInput: Boolean(model.has_audio_input), diff --git a/studio/frontend/src/features/chat/types/api.ts b/studio/frontend/src/features/chat/types/api.ts index d313b43438..92ea1500bd 100644 --- a/studio/frontend/src/features/chat/types/api.ts +++ b/studio/frontend/src/features/chat/types/api.ts @@ -7,6 +7,7 @@ export interface BackendModelDetails { is_vision?: boolean; is_lora?: boolean; is_gguf?: boolean; + is_mlx?: boolean; is_audio?: boolean; audio_type?: string | null; has_audio_input?: boolean; diff --git a/studio/frontend/src/features/chat/types/runtime.ts b/studio/frontend/src/features/chat/types/runtime.ts index 4c44ee1e9c..24286c94fb 100644 --- a/studio/frontend/src/features/chat/types/runtime.ts +++ b/studio/frontend/src/features/chat/types/runtime.ts @@ -44,6 +44,7 @@ export interface ChatModelSummary { isVision: boolean; isLora: boolean; isGguf?: boolean; + isMlx?: boolean; isAudio?: boolean; audioType?: string | null; hasAudioInput?: boolean;