From fd6a15ea712215fa419dc87c10ca5eda08de83f8 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 20 Oct 2026 05:28:56 +0000 Subject: [PATCH 1/3] Match Glimmer tool parsing to stock vLLM (cherry picked from commit d477c7c83abf11c4570366fae03ac8c8e21f1bf9) --- renderers/muse_glimmer.py | 51 ++++++++++++++++++++++++-------------- tests/test_muse_glimmer.py | 16 ++++++++++-- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/renderers/muse_glimmer.py b/renderers/muse_glimmer.py index ec9bb0066..6fdea195f 100644 --- a/renderers/muse_glimmer.py +++ b/renderers/muse_glimmer.py @@ -51,7 +51,6 @@ should_rerender_for_thinking_retention, ) from renderers.configs import MuseGlimmerRendererConfig -from renderers.parsing import _build_param_type_index, _coerce_arg_value _IMAGE_TYPES = ("image", "image_url") _UNSUPPORTED_MEDIA_TYPES = ("video", "video_url", "audio", "audio_url", "input_audio") @@ -104,6 +103,15 @@ r']*?\bname="(?P[^"]+)"[^>]*?>(?P.*?)', re.DOTALL, ) +_STRIP_REASONING_RE = re.compile( + r"(?:<\|start\|>assistant\s*)?to=self<\|message\|>.*?<\|eom\|>", re.DOTALL +) +_STRIP_OPEN_REASONING_RE = re.compile( + r"(?:<\|start\|>assistant\s*)?to=self<\|message\|>" + r"(?:(?!<\|eom\|>)(?!to=[^\s<]+<\|message\|>).)*" + r"(?=to=[^\s<]+<\|message\|>|$)", + re.DOTALL, +) def _json(value: Any) -> str: @@ -723,23 +731,31 @@ def cut(h: re.Match) -> int: reasoning = ( "\n".join(p for p in reasoning_parts if p) if reasoning_parts else None ) - param_types = _build_param_type_index(tools) + registered = {_function(tool)["name"] for tool in tools or []} calls = [ c for recipient, body, closed in tool_bodies - for c in self._parse_calls(recipient, body, param_types) + for c in self._parse_calls(body, registered) ] + content = "".join(content_parts) + if tool_bodies and not any(call.name for call in calls): + # Stock vLLM preserves malformed tool markup as content when no + # complete invocation can be dispatched; it does not invent a call. + remainder = _STRIP_REASONING_RE.sub("", text) + remainder = _STRIP_OPEN_REASONING_RE.sub("", remainder) + if "" in remainder: + content = remainder + elif not content: + content = "" if "to=self<|message|>" in text else text return ParsedResponse( - content="".join(content_parts), + content=content, reasoning_content=reasoning, tool_calls=calls, reasoning_complete=reasoning_complete, ) @staticmethod - def _parse_calls( - recipient: str, body: str, param_types: dict[str, dict[str, dict[str, Any]]] - ) -> list[ParsedToolCall]: + def _parse_calls(body: str, registered: set[str]) -> list[ParsedToolCall]: """Every ```` in one tool-channel message.""" calls: list[ParsedToolCall] = [] last_end = 0 @@ -754,23 +770,24 @@ def _parse_calls( ) ) continue - schema = param_types.get(name, {}) + if name not in registered: + head, sep, tail = name.partition(".") + if sep and head == tail and head in registered: + name = head arguments: dict[str, Any] = {} - fallback = False for p in _PARAM_RE.finditer(m.group("body")): - value, used = _coerce_arg_value( - p.group("value"), schema.get(p.group("key")) - ) + value = p.group("value") + try: + value = json.loads(value) + except (json.JSONDecodeError, ValueError): + pass arguments[p.group("key")] = value - fallback = fallback or used calls.append( ParsedToolCall( raw=m.group(0), name=name, arguments=arguments, - status=ToolCallParseStatus.INVALID_JSON - if fallback - else ToolCallParseStatus.OK, + status=ToolCallParseStatus.OK, ) ) rest = body[last_end:] @@ -779,7 +796,6 @@ def _parse_calls( calls.append( ParsedToolCall( raw=rest.strip(), - name=recipient, status=ToolCallParseStatus.UNCLOSED_BLOCK, ) ) @@ -787,7 +803,6 @@ def _parse_calls( calls.append( ParsedToolCall( raw=body, - name=recipient, status=ToolCallParseStatus.MALFORMED_STRUCTURE, ) ) diff --git a/tests/test_muse_glimmer.py b/tests/test_muse_glimmer.py index 961cfed3b..675a1291e 100644 --- a/tests/test_muse_glimmer.py +++ b/tests/test_muse_glimmer.py @@ -17,7 +17,6 @@ from functools import lru_cache import pytest - from renderers.base import ToolCallParseStatus MODEL = "meta-models/Muse-Glimmer-30B" @@ -413,6 +412,16 @@ def test_consecutive_assistant_messages_continue_the_turn(): "<|start|>assistant to=user<|message|>Done.", ("First.\nSecond.", "Done.", []), ), + "doubled-bare-name": ( + " to=get_weather<|message|>" + + _invoke("get_weather.get_weather", ("city", "Paris")), + (None, "", [("get_weather", {"city": "Paris"})]), + ), + "string-schema-does-not-override-scalar-decoding": ( + " to=get_weather<|message|>" + + _invoke("get_weather", ("city", "123"), ("code", "false")), + (None, "", [("get_weather", {"city": 123, "code": False})]), + ), } @@ -520,14 +529,17 @@ def test_unfinished_tool_call_is_unclosed(): ) ) assert [c.status for c in parsed.tool_calls] == [ToolCallParseStatus.UNCLOSED_BLOCK] + assert parsed.tool_calls[0].name is None + assert '' in parsed.content def test_tool_channel_without_invoke_is_malformed(): _, renderer = _load() parsed = renderer.parse_response(_ids(" to=get_weather<|message|>Paris please")) assert [(c.name, c.status) for c in parsed.tool_calls] == [ - ("get_weather", ToolCallParseStatus.MALFORMED_STRUCTURE) + (None, ToolCallParseStatus.MALFORMED_STRUCTURE) ] + assert parsed.content == " to=get_weather<|message|>Paris please" def test_prefilled_reasoning_prompt_continues_reasoning(): From f0ccabbdb93924c641236db8946f635c12c867a0 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 20 Oct 2026 06:11:09 +0000 Subject: [PATCH 2/3] Preserve Glimmer non-streaming answer whitespace parity (cherry picked from commit d6d6fa6853a84bfe3bfd44c4696089ae908748fc) --- renderers/muse_glimmer.py | 11 +++++++++++ tests/test_muse_glimmer.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/renderers/muse_glimmer.py b/renderers/muse_glimmer.py index 6fdea195f..779c1a786 100644 --- a/renderers/muse_glimmer.py +++ b/renderers/muse_glimmer.py @@ -738,6 +738,17 @@ def cut(h: re.Match) -> int: for c in self._parse_calls(body, registered) ] content = "".join(content_parts) + if ( + messages + and messages[-1][0] == "user" + and not messages[-1][2] + and not any(call.name for call in calls) + and content.endswith("\n") + and "" not in content + ): + # vLLM's non-streaming content regex ends at `$`, before one final LF. + content = content[:-1] if tool_bodies and not any(call.name for call in calls): # Stock vLLM preserves malformed tool markup as content when no # complete invocation can be dispatched; it does not invent a call. diff --git a/tests/test_muse_glimmer.py b/tests/test_muse_glimmer.py index 675a1291e..500898eed 100644 --- a/tests/test_muse_glimmer.py +++ b/tests/test_muse_glimmer.py @@ -364,6 +364,22 @@ def test_consecutive_assistant_messages_continue_the_turn(): ("Let me think.", "The answer is 4.", []), ), "answer-only": (" to=user<|message|>Hello there!", (None, "Hello there!", [])), + "answer-final-newline": (" to=user<|message|>Hello\n", (None, "Hello", [])), + "answer-final-two-newlines": ( + " to=user<|message|>Hello\n\n", + (None, "Hello\n", []), + ), + "answer-final-space-newline": (" to=user<|message|>Hello \n", (None, "Hello ", [])), + "closed-answer-keeps-newline": ( + " to=user<|message|>Hello\n<|eom|>", + (None, "Hello\n", []), + ), + "tool-and-answer-keeps-newline": ( + " to=get_weather<|message|>" + + _invoke("get_weather", ("city", "Paris")) + + "<|eom|><|start|>assistant to=user<|message|>Hello\n", + (None, "Hello\n", [("get_weather", {"city": "Paris"})]), + ), "tool-call": ( " to=self<|message|>Need weather.<|eom|><|start|>assistant to=get_weather<|message|>" + _invoke( From 49ada0ec4aec79b1a059e6d96b08de4a1714e2ac Mon Sep 17 00:00:00 2001 From: Eli Gottlieb <78387377+eligotts@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:51:47 -0700 Subject: [PATCH 3/3] Align shared Glimmer argument tests with vLLM semantics --- tests/test_tool_arg_type_preservation.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test_tool_arg_type_preservation.py b/tests/test_tool_arg_type_preservation.py index aa3d15bf7..ec12a3e2b 100644 --- a/tests/test_tool_arg_type_preservation.py +++ b/tests/test_tool_arg_type_preservation.py @@ -13,6 +13,9 @@ (Qwen3) and section-JSON (Kimi K2) parsers sidestep the bug because their wire format quotes strings; both serve as controls. +Muse Glimmer follows vLLM's schema-independent JSON decoding instead, so +its cases assert decoded values even when the schema declares a string. + Originally raised by Robin (Poolside) on PR #21. """ @@ -25,6 +28,8 @@ import pytest from parity import models_for +from renderers.muse_glimmer import MuseGlimmerRenderer + # (HuggingFace model name, renderer name). Two JSON-shaped controls # (string types already preserved by the wire format) + five XML-style @@ -110,9 +115,8 @@ def _extract_assistant_tokens(renderer, prompt, assistant_msg, *, tools=None): @pytest.mark.parametrize("args", JSON_LOOKING_STRING_ARGS) -def test_string_arg_preserves_type(model, renderer_name, renderer, args): - """Tool-call args of declared type ``str`` must round-trip as ``str``, - not get re-parsed as bool/int/null/list/dict by the parser.""" +def test_string_arg_follows_parser_contract(model, renderer_name, renderer, args): + """Preserve declared strings unless the reference parser decodes JSON.""" msg = { "role": "assistant", "content": "", @@ -128,8 +132,13 @@ def test_string_arg_preserves_type(model, renderer_name, renderer, args): assert parsed.tool_calls, f"{model}: parser returned no tool_calls" got = _normalize_args(parsed.tool_calls[0].arguments) - assert got == args, ( - f"{model}: tool-arg type drift — sent {args!r}, parser returned {got!r}" + expected = ( + {key: json.loads(value) for key, value in args.items()} + if isinstance(renderer, MuseGlimmerRenderer) + else args + ) + assert got == expected, ( + f"{model}: tool-arg type drift — expected {expected!r}, parser returned {got!r}" )