fix: normalize tool message content for GLM5.1 chat template (#22595)
This commit is contained in:
@@ -60,6 +60,28 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_tool_content(role: str, content):
|
||||||
|
"""Normalize tool message content from OpenAI array format to plain string.
|
||||||
|
|
||||||
|
OpenAI clients may send tool content as a list of content parts
|
||||||
|
(e.g. [{"type":"text","text":"..."}]) but most chat templates expect
|
||||||
|
a plain string for tool messages. Only flatten when ALL items are
|
||||||
|
pure OpenAI text parts; preserve lists containing non-text-type items
|
||||||
|
that some templates intentionally iterate over.
|
||||||
|
"""
|
||||||
|
if role != "tool" or not isinstance(content, list):
|
||||||
|
return content
|
||||||
|
parts = content
|
||||||
|
is_openai_text_parts = all(
|
||||||
|
(isinstance(p, dict) and p.get("type") == "text") or isinstance(p, str)
|
||||||
|
for p in parts
|
||||||
|
)
|
||||||
|
if is_openai_text_parts:
|
||||||
|
text_parts = [p.get("text", "") if isinstance(p, dict) else p for p in parts]
|
||||||
|
return " ".join(text_parts)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def _extract_max_dynamic_patch(request: ChatCompletionRequest):
|
def _extract_max_dynamic_patch(request: ChatCompletionRequest):
|
||||||
img_vals = []
|
img_vals = []
|
||||||
vid_vals = []
|
vid_vals = []
|
||||||
@@ -457,6 +479,10 @@ class OpenAIServingChat(OpenAIServingBase):
|
|||||||
modalities,
|
modalities,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
processed_msg["content"] = normalize_tool_content(
|
||||||
|
processed_msg["role"], processed_msg.get("content")
|
||||||
|
)
|
||||||
|
|
||||||
# per the Transformers docs & maintainers, tool call arguments in
|
# per the Transformers docs & maintainers, tool call arguments in
|
||||||
# assistant-role messages with tool_calls need to be dicts not JSON str -
|
# assistant-role messages with tool_calls need to be dicts not JSON str -
|
||||||
# this is how tool-use chat templates will expect them moving forwards
|
# this is how tool-use chat templates will expect them moving forwards
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ from sglang.srt.entrypoints.openai.protocol import (
|
|||||||
ChatCompletionRequest,
|
ChatCompletionRequest,
|
||||||
MessageProcessingResult,
|
MessageProcessingResult,
|
||||||
)
|
)
|
||||||
from sglang.srt.entrypoints.openai.serving_chat import OpenAIServingChat
|
from sglang.srt.entrypoints.openai.serving_chat import (
|
||||||
|
OpenAIServingChat,
|
||||||
|
normalize_tool_content,
|
||||||
|
)
|
||||||
from sglang.srt.managers.io_struct import GenerateReqInput
|
from sglang.srt.managers.io_struct import GenerateReqInput
|
||||||
from sglang.srt.utils import get_or_create_event_loop
|
from sglang.srt.utils import get_or_create_event_loop
|
||||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||||
@@ -982,5 +985,42 @@ class TestProcessToolCallsWithRequiredToolChoice(unittest.TestCase):
|
|||||||
self.assertIsNone(tool_calls)
|
self.assertIsNone(tool_calls)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizeToolContent(unittest.TestCase):
|
||||||
|
"""Unit tests for normalize_tool_content()."""
|
||||||
|
|
||||||
|
def test_openai_text_parts_flattened(self):
|
||||||
|
result = normalize_tool_content("tool", [{"type": "text", "text": "10525"}])
|
||||||
|
self.assertEqual(result, "10525")
|
||||||
|
|
||||||
|
def test_multiple_text_parts_joined(self):
|
||||||
|
result = normalize_tool_content(
|
||||||
|
"tool",
|
||||||
|
[{"type": "text", "text": "hello"}, {"type": "text", "text": "world"}],
|
||||||
|
)
|
||||||
|
self.assertEqual(result, "hello world")
|
||||||
|
|
||||||
|
def test_non_text_part_list_preserved(self):
|
||||||
|
content = [{"name": "func", "output": "result"}]
|
||||||
|
result = normalize_tool_content("tool", content)
|
||||||
|
self.assertIs(result, content)
|
||||||
|
|
||||||
|
def test_string_content_unchanged(self):
|
||||||
|
self.assertEqual(normalize_tool_content("tool", "hello"), "hello")
|
||||||
|
|
||||||
|
def test_empty_list_returns_empty_string(self):
|
||||||
|
self.assertEqual(normalize_tool_content("tool", []), "")
|
||||||
|
|
||||||
|
def test_non_tool_role_unchanged(self):
|
||||||
|
content = [{"type": "text", "text": "hi"}]
|
||||||
|
result = normalize_tool_content("user", content)
|
||||||
|
self.assertIs(result, content)
|
||||||
|
|
||||||
|
def test_mixed_str_and_dict_parts(self):
|
||||||
|
result = normalize_tool_content(
|
||||||
|
"tool", ["plain", {"type": "text", "text": "rich"}]
|
||||||
|
)
|
||||||
|
self.assertEqual(result, "plain rich")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user