From 8b1ed2fa8a5fb35807f613b312e14885c715b7eb Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:50:15 +0800 Subject: [PATCH] [data] fix MiniMax tool_extractor to return content when no tool call parses MiniMaxM1/M2 tool_extractor returned an empty list when the wrapper tag was present but nothing parsed (M1 skips bad-JSON lines, M2 finds no block), silently dropping the original content. The sibling extractors (Seed / Qwen3.5 / LFM2) already return content in that case; this aligns MiniMax with them and adds unit tests for the no-call and empty-wrapper paths. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> --- src/llamafactory/data/tool_utils.py | 4 +-- tests/data/test_tool_utils.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tests/data/test_tool_utils.py diff --git a/src/llamafactory/data/tool_utils.py b/src/llamafactory/data/tool_utils.py index 69a13c5745..bcec9bf45c 100644 --- a/src/llamafactory/data/tool_utils.py +++ b/src/llamafactory/data/tool_utils.py @@ -490,7 +490,7 @@ def tool_extractor(content: str) -> Union[str, list["FunctionCall"]]: except json.JSONDecodeError: continue - return results + return results if results else content class MiniMaxM2ToolUtils(ToolUtils): @@ -548,7 +548,7 @@ def tool_extractor(content: str) -> Union[str, list["FunctionCall"]]: results.append(FunctionCall(func_name.strip(), json.dumps(args_dict, ensure_ascii=False))) - return results + return results if results else content class MistralToolUtils(ToolUtils): diff --git a/tests/data/test_tool_utils.py b/tests/data/test_tool_utils.py new file mode 100644 index 0000000000..26ea0b58b7 --- /dev/null +++ b/tests/data/test_tool_utils.py @@ -0,0 +1,45 @@ +# Copyright 2025 the LlamaFactory team. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from llamafactory.data.tool_utils import MiniMaxM1ToolUtils, MiniMaxM2ToolUtils + + +def test_minimax_m1_parses_valid_tool_call(): + content = '\n{"name": "get_weather", "arguments": {"city": "NYC"}}\n' + result = MiniMaxM1ToolUtils.tool_extractor(content) + assert isinstance(result, list) and len(result) == 1 + assert result[0].name == "get_weather" + + +def test_minimax_m1_returns_content_when_no_valid_tool_call(): + # Wrapper tag is present but its body is not valid JSON, so nothing parses. + # The extractor should return the original content, not an empty list. + content = "\nthis is not json\n" + assert MiniMaxM1ToolUtils.tool_extractor(content) == content + + +def test_minimax_m1_empty_wrapper_returns_content(): + content = "\n\n" + assert MiniMaxM1ToolUtils.tool_extractor(content) == content + + +def test_minimax_m2_returns_content_when_no_valid_tool_call(): + # Wrapper tag is present but there is no well-formed block. + content = "\nno invoke block here\n" + assert MiniMaxM2ToolUtils.tool_extractor(content) == content + + +def test_minimax_m2_empty_wrapper_returns_content(): + content = "\n\n" + assert MiniMaxM2ToolUtils.tool_extractor(content) == content