From 2554636ded46f87430cac6c20a35c828801585c8 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Tue, 9 Jun 2026 18:11:38 +0200 Subject: [PATCH] Studio: follow-up fix for GGUF developer prompts (#6115) * Studio: merge developer prompts for GGUF chat * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- studio/backend/routes/inference.py | 13 ++-- .../tests/test_openai_tool_passthrough.py | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index a2e0e5a811..81bc9ff628 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -2050,8 +2050,8 @@ def _extract_content_parts(messages: list) -> tuple[str, list[dict], "Optional[s first_image_b64: Optional[str] = None for msg in messages: - # ── System messages → extract as system_prompt ──────── - if msg.role == "system": + # ── System / developer messages → extract as system_prompt ──────── + if msg.role in ("system", "developer"): if isinstance(msg.content, str): system_parts.append(msg.content) elif isinstance(msg.content, list): @@ -3053,6 +3053,7 @@ async def openai_chat_completions( payload, llama_backend.is_vision, ) + gguf_messages = _set_or_prepend_system_message(gguf_messages, system_prompt) image_b64 = None cancel_event = threading.Event() @@ -5190,9 +5191,9 @@ def _set_or_prepend_system_message( if not system_prompt: return safe_messages - # Drop existing system turns so the backend never sees duplicate or - # conflicting system instructions, then prepend the resolved prompt. - others = [dict(msg) for msg in safe_messages if msg.get("role") != "system"] + # Drop existing system/developer turns so the backend never sees duplicate + # or conflicting system instructions, then prepend the resolved prompt. + others = [dict(msg) for msg in safe_messages if msg.get("role") not in ("system", "developer")] return [{"role": "system", "content": system_prompt}, *others] @@ -6415,6 +6416,8 @@ def _build_openai_passthrough_body(payload, backend_ctx = None) -> dict: leak to the backend. """ messages = _openai_messages_for_passthrough(payload) + system_prompt, _, _ = _extract_content_parts(payload.messages) + messages = _set_or_prepend_system_message(messages, system_prompt) tool_choice = payload.tool_choice if payload.tool_choice is not None else "auto" # When the caller asked for a specific reasoning mode, forward it via # chat_template_kwargs so the Jinja template renders with (or without) the diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index 495ebc434d..db4c07cdc8 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -33,6 +33,7 @@ from core.inference.anthropic_compat import ( anthropic_tool_choice_to_openai, ) from routes.inference import ( + _build_openai_passthrough_body, _build_passthrough_payload, _clamp_finish_reason, _effective_max_tokens, @@ -634,6 +635,24 @@ class TestBuildPassthroughPayloadToolChoice: assert body.get("repeat_penalty") == 1.1 assert "repetition_penalty" not in body + def test_passthrough_body_merges_system_and_developer_messages(self): + payload = ChatCompletionRequest( + model = "default", + messages = [ + {"role": "system", "content": "original system"}, + {"role": "developer", "content": "developer rules"}, + {"role": "user", "content": "hi"}, + ], + tools = self._args()["openai_tools"], + ) + + body = _build_openai_passthrough_body(payload, backend_ctx = 4096) + + assert body["messages"] == [ + {"role": "system", "content": "original system\n\ndeveloper rules"}, + {"role": "user", "content": "hi"}, + ] + # ===================================================================== # OpenAI API compatibility helpers — verified spec edge cases @@ -1092,6 +1111,47 @@ class TestGgufVisionToolRouting: assert captured["kwargs"]["disable_parallel_tool_use"] is True + def test_standard_gguf_merges_system_and_developer_messages(self, monkeypatch): + import routes.inference as inf_mod + + captured = {} + + def _generate(**kwargs): + captured["messages"] = kwargs["messages"] + yield "done" + yield { + "type": "metadata", + "usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}, + "finish_reason": "stop", + } + + backend = SimpleNamespace( + is_loaded = True, + is_vision = False, + supports_tools = False, + model_identifier = "test-gguf", + generate_chat_completion = _generate, + ) + monkeypatch.setattr(inf_mod, "get_llama_cpp_backend", lambda: backend) + + payload = ChatCompletionRequest( + model = "default", + messages = [ + {"role": "system", "content": "original system"}, + {"role": "developer", "content": "developer rules"}, + {"role": "user", "content": "hi"}, + ], + ) + + self._drive( + openai_chat_completions(payload, request = self._Request(), current_subject = "test") + ) + + assert captured["messages"] == [ + {"role": "system", "content": "original system\n\ndeveloper rules"}, + {"role": "user", "content": "hi"}, + ] + @pytest.mark.parametrize( ("seed", "expected"), [