From f22e890ab81ebe3fd22505c954fe1df7c61e152d Mon Sep 17 00:00:00 2001 From: James Dawdy Date: Fri, 12 Jun 2026 02:27:04 -0500 Subject: [PATCH] fix(studio): inherit llama_extra_args and honor --no-mmproj (#5902) * fix(studio): inherit llama_extra_args and honor --no-mmproj Reloading the same GGUF from the UI without gguf_variant no longer drops CLI pass-through args like --no-mmproj. Skip mmproj download and launch when --no-mmproj is present in llama_extra_args. Co-authored-by: Cursor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): tighten GGUF llama_extra_args variant inheritance guard Reject inherited CLI args when the request changes gguf_variant or when omitted variant resolves differently from the stored extra_args source. Co-authored-by: Cursor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Treat --no-mmproj-auto and --mmproj-auto with last-wins parsing for PR #5902 --------- Co-authored-by: Cursor Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- studio/backend/core/inference/llama_cpp.py | 15 ++++++----- .../core/inference/llama_server_args.py | 22 +++++++++++++++ studio/backend/routes/inference.py | 27 +++++++++++-------- .../backend/tests/test_llama_server_args.py | 17 ++++++++++++ 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index b83e4e6961..80b4862aa8 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -28,6 +28,7 @@ from typing import Callable, Generator, Iterable, List, Optional import httpx from core.inference.llama_server_args import ( + extra_args_disable_mmproj, parse_cache_override, parse_ctx_override, resolve_cache_type_kv, @@ -2929,8 +2930,8 @@ class LlamaCppBackend: hf_variant = hf_variant, hf_token = hf_token, ) - # Auto-download mmproj for vision models - if is_vision and not mmproj_path: + # Auto-download mmproj for vision models unless opted out. + if is_vision and not mmproj_path and not extra_args_disable_mmproj(extra_args): mmproj_path = self._download_mmproj( hf_repo = hf_repo, hf_token = hf_token, @@ -3191,10 +3192,12 @@ class LlamaCppBackend: gpu_indices, use_fit = None, True effective_ctx = requested_ctx # fall back to original - launch_mmproj_path = self._resolve_launch_mmproj_path( - model_path = model_path, - mmproj_path = mmproj_path, - ) + launch_mmproj_path = None + if not extra_args_disable_mmproj(extra_args): + launch_mmproj_path = self._resolve_launch_mmproj_path( + model_path = model_path, + mmproj_path = mmproj_path, + ) # Need both a resolved mmproj AND the config vision flag; a stray # mmproj passing the family-name heuristic must not flip a non-VLM # GGUF into vision mode. diff --git a/studio/backend/core/inference/llama_server_args.py b/studio/backend/core/inference/llama_server_args.py index 1862c9c5de..00f8c66d5c 100644 --- a/studio/backend/core/inference/llama_server_args.py +++ b/studio/backend/core/inference/llama_server_args.py @@ -260,6 +260,28 @@ def resolve_cache_type_kv( return override if override is not None else fallback_cache_type_kv +_MMPROJ_DISABLE_FLAGS: frozenset[str] = frozenset({"--no-mmproj", "--no-mmproj-auto"}) +_MMPROJ_ENABLE_FLAGS: frozenset[str] = frozenset({"--mmproj-auto"}) + + +def extra_args_disable_mmproj(args: Optional[Iterable[str]]) -> bool: + """True when pass-through args opt out of vision mmproj loading. + + llama-server parses --mmproj-auto / --no-mmproj / --no-mmproj-auto as one + boolean with last-wins semantics; mirror that here. + """ + if not args: + return False + disabled = False + for raw in args: + flag = _flag_name(str(raw)) + if flag in _MMPROJ_DISABLE_FLAGS: + disabled = True + elif flag in _MMPROJ_ENABLE_FLAGS: + disabled = False + return disabled + + def strip_shadowing_flags( args: Iterable[str], *, diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 3850d0cbb2..856d9bacf8 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1449,18 +1449,23 @@ async def load_model( # parse against a freshly-supplied first-class field. if request.llama_extra_args is None and llama_backend.extra_args: source = llama_backend.extra_args_source - # Compare against the resolved variant, not the request field: - # callers commonly omit gguf_variant for local ``.gguf`` paths - # and HF auto-pick flows. ``config.gguf_variant`` is the variant - # load_model was actually invoked with (see HF / local branches - # below), so both sides key off the same string. - resolved_variant = config.gguf_variant - same_source = bool( - source - and source[0] - and source[0].lower() == model_identifier.lower() - and (source[1] or "").lower() == (resolved_variant or "").lower() + # Compare against the resolved variant, not the request + # field: callers commonly omit gguf_variant for local + # ``.gguf`` paths and HF auto-pick flows. ``config.gguf_ + # variant`` is the variant load_model was actually + # invoked with (see the HF / local branches below), so + # both sides of the comparison key off the same string. + resolved_variant = (config.gguf_variant or "").lower() + request_variant = (request.gguf_variant or "").lower() + stored_variant = (source[1] or "").lower() if source else "" + same_model = bool( + source and source[0] and source[0].lower() == model_identifier.lower() ) + if request.gguf_variant: + variant_mismatch = request_variant != stored_variant + else: + variant_mismatch = bool(stored_variant and resolved_variant != stored_variant) + same_source = same_model and not variant_mismatch if not same_source: logger.info( "Not inheriting llama_extra_args: stored args came from %s, loading %s", diff --git a/studio/backend/tests/test_llama_server_args.py b/studio/backend/tests/test_llama_server_args.py index 09775707ee..6ae9d21e47 100644 --- a/studio/backend/tests/test_llama_server_args.py +++ b/studio/backend/tests/test_llama_server_args.py @@ -27,6 +27,7 @@ parse_cache_override = _lsa.parse_cache_override parse_ctx_override = _lsa.parse_ctx_override resolve_cache_type_kv = _lsa.resolve_cache_type_kv strip_shadowing_flags = _lsa.strip_shadowing_flags +extra_args_disable_mmproj = _lsa.extra_args_disable_mmproj validate_extra_args = _lsa.validate_extra_args @@ -509,6 +510,22 @@ def test_strip_shadowing_flags_defaults_strip_everything(): assert out == [] +def test_extra_args_disable_mmproj_detects_flag(): + assert extra_args_disable_mmproj(["--no-mmproj"]) is True + assert extra_args_disable_mmproj(["--threads", "12", "--no-mmproj"]) is True + assert extra_args_disable_mmproj(["--no-mmproj-auto"]) is True + + +def test_extra_args_disable_mmproj_false_when_absent(): + assert extra_args_disable_mmproj(None) is False + assert extra_args_disable_mmproj(["--threads", "12"]) is False + + +def test_extra_args_disable_mmproj_last_wins(): + assert extra_args_disable_mmproj(["--no-mmproj", "--mmproj-auto"]) is False + assert extra_args_disable_mmproj(["--mmproj-auto", "--no-mmproj-auto"]) is True + + def test_strip_shadowing_flags_drops_model_draft_with_spec(): # --model-draft (and aliases) are Studio-managed since the separate # MTP drafter support: an inherited copy must not last-wins-override