From f02ccd461bb41d392600fd8ab1b3475ae172fbf8 Mon Sep 17 00:00:00 2001 From: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:06:53 -0700 Subject: [PATCH] fix(glm45): emit the newline before every tool call after assistant text The GLM-4.5 chat template opens every tool call with "\n", but when an assistant turn had text and two or more calls the renderer only emitted that newline before the first call. I emit it before each later call too and add a parity scenario with text plus two tool calls. Assisted-by: Claude Assisted-by: GPT-6 Astra Signed-off-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> --- renderers/glm45.py | 6 ++++-- tests/parity.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/renderers/glm45.py b/renderers/glm45.py index cb967552d..e6b111d1e 100644 --- a/renderers/glm45.py +++ b/renderers/glm45.py @@ -517,12 +517,14 @@ def _render_assistant( elif content.strip(): emit_text("\n" + content.strip(), msg_idx, is_sampled=True, is_content=True) - for tc in tool_calls: + for tc_idx, tc in enumerate(tool_calls): func = tc.get("function") or tc name = func.get("name", "") arguments = func.get("arguments", {}) - if not content.strip(): + # The template opens every call with "\n"; only the + # first call's "\n" is already carried by the content text above. + if tc_idx > 0 or not content.strip(): emit_text("\n", msg_idx, is_sampled=True, is_content=True) emit_special(self._tool_call_tok, msg_idx, is_sampled=True, is_content=True) emit_text(name + "\n", msg_idx, is_sampled=True, is_content=True) diff --git a/tests/parity.py b/tests/parity.py index ac34e11cc..ee42375bc 100644 --- a/tests/parity.py +++ b/tests/parity.py @@ -202,6 +202,7 @@ def _model( "tool-call-content", "tool-call-none", "multiple-tool-calls", + "multiple-tool-calls-content", "tool-response", "named-tool-response", "consecutive-tool-responses", @@ -217,6 +218,7 @@ def _model( "tool-call-content", "tool-call-none", "multiple-tool-calls", + "multiple-tool-calls-content", "tool-response", "named-tool-response", "consecutive-tool-responses", @@ -246,7 +248,11 @@ def _model( _model( "meta-llama/Llama-3.2-1B-Instruct", bridge=True, - excluded={"multiple-tool-calls", "consecutive-tool-responses"}, + excluded={ + "multiple-tool-calls", + "multiple-tool-calls-content", + "consecutive-tool-responses", + }, oracle_defaults={"date_string": "26 Jul 2024"}, extra_suites={"llama-checkpoints"}, ), @@ -254,7 +260,11 @@ def _model( "meta-llama/Llama-3.2-3B-Instruct", shared=False, roundtrip=False, - excluded={"multiple-tool-calls", "consecutive-tool-responses"}, + excluded={ + "multiple-tool-calls", + "multiple-tool-calls-content", + "consecutive-tool-responses", + }, oracle_defaults={"date_string": "26 Jul 2024"}, extra_suites={"llama-checkpoints"}, ), @@ -486,6 +496,21 @@ def _call(city: str, *, id: str | None = None) -> dict: ], tools=TOOLS, ), + _scenario( + "multiple-tool-calls-content", + [ + {"role": "user", "content": "Weather in Paris and London?"}, + { + "role": "assistant", + "content": "Let me check both.", + "tool_calls": [ + _call("Paris", id="call_1"), + _call("London", id="call_2"), + ], + }, + ], + tools=TOOLS, + ), _scenario( "tool-response", [