From 7904412ff2825343520e7e2e9541e89933cfac6e Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 19:35:00 +0100 Subject: [PATCH] [#17740][fix] Preserve streamed content after wrapped Qwen tool calls Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> --- .../serve/tool_parser/base_tool_parser.py | 4 +- ...test_qwen3_tool_parser_trailing_content.py | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/unittest/llmapi/apps/test_qwen3_tool_parser_trailing_content.py diff --git a/tensorrt_llm/serve/tool_parser/base_tool_parser.py b/tensorrt_llm/serve/tool_parser/base_tool_parser.py index ece736749d2f..3a08179f982f 100644 --- a/tensorrt_llm/serve/tool_parser/base_tool_parser.py +++ b/tensorrt_llm/serve/tool_parser/base_tool_parser.py @@ -135,7 +135,9 @@ def parse_streaming_increment(self, new_text: str, # or it is the start of a new tool call after a tool call separator, when there is a previous tool call if not (self.has_tool_call(current_text) or (self.current_tool_id > 0 - and current_text.startswith(self.tool_call_separator))): + and current_text.startswith(self.tool_call_separator) + and not (self.eot_token + and current_text.startswith(self.eot_token)))): # Only clear buffer if we're sure no tool call is starting if not self._ends_with_partial_token(self._buffer, self.bot_token): normal_text = self._buffer diff --git a/tests/unittest/llmapi/apps/test_qwen3_tool_parser_trailing_content.py b/tests/unittest/llmapi/apps/test_qwen3_tool_parser_trailing_content.py new file mode 100644 index 000000000000..4d1e4a2752ae --- /dev/null +++ b/tests/unittest/llmapi/apps/test_qwen3_tool_parser_trailing_content.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from tensorrt_llm.serve.openai_protocol import ( + ChatCompletionToolsParam, + FunctionDefinition, +) +from tensorrt_llm.serve.tool_parser.qwen3_tool_parser import Qwen3ToolParser + +pytestmark = pytest.mark.cpu_only + + +def test_streaming_wrapped_form_preserves_text_after_tool_call(): + tools = [ + ChatCompletionToolsParam( + type="function", + function=FunctionDefinition( + name="get_weather", + description="Get the current weather", + parameters={ + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + ), + ) + ] + parser = Qwen3ToolParser() + + parser.parse_streaming_increment("\n", tools) + parser.parse_streaming_increment( + '{"name":"get_weather","arguments":{"location":"Paris"}}', tools + ) + parser.parse_streaming_increment("\n", tools) + + result = parser.parse_streaming_increment(" It is sunny.", tools) + + assert result.normal_text == " It is sunny." + assert result.calls == [] + assert parser._buffer == ""