mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(zai): flatten list-format content in tool/assistant messages for GLM
GLM's Jinja chat template checks `m.content is string` — when it receives list-format content parts (as sent by Go clients like openai-go), the content is silently replaced with an empty artifact. Flatten list-format content to strings for tool/assistant messages using the existing convert_content_list_to_str utility. User messages are left intact so the parent's image_url processing still works. Fixes #25868 Signed-off-by: Jay <moonandstar99@yahoo.com>
This commit is contained in:
parent
98cd057f38
commit
a177e05c82
4 changed files with 205 additions and 1 deletions
|
|
@ -1,5 +1,8 @@
|
|||
from typing import List, Optional, Tuple
|
||||
from typing import Any, Coroutine, List, Literal, Optional, Tuple, Union, overload
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
convert_content_list_to_str,
|
||||
)
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam
|
||||
|
||||
|
|
@ -20,6 +23,50 @@ class ZAIChatConfig(OpenAIGPTConfig):
|
|||
dynamic_api_key = api_key or get_secret_str("ZAI_API_KEY")
|
||||
return api_base, dynamic_api_key
|
||||
|
||||
@overload
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: str, is_async: Literal[True]
|
||||
) -> Coroutine[Any, Any, List[AllMessageValues]]: ...
|
||||
|
||||
@overload
|
||||
def _transform_messages(
|
||||
self,
|
||||
messages: List[AllMessageValues],
|
||||
model: str,
|
||||
is_async: Literal[False] = False,
|
||||
) -> List[AllMessageValues]: ...
|
||||
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: str, is_async: bool = False
|
||||
) -> Union[List[AllMessageValues], Coroutine[Any, Any, List[AllMessageValues]]]:
|
||||
"""Flatten list-format content in tool/assistant messages for GLM.
|
||||
|
||||
GLM's Jinja template checks ``m.content is string`` — list-format
|
||||
content parts (used by Go clients like openai-go) are silently
|
||||
dropped. Flatten them to strings before forwarding.
|
||||
|
||||
Only tool/assistant roles are flattened — user messages are left
|
||||
intact so the parent's image_url processing can handle them.
|
||||
|
||||
See: https://github.com/BerriAI/litellm/issues/25868
|
||||
"""
|
||||
for message in messages:
|
||||
role = message.get("role")
|
||||
if role in ("tool", "assistant"):
|
||||
content = message.get("content")
|
||||
if content is not None and not isinstance(content, str):
|
||||
text = convert_content_list_to_str(message)
|
||||
message["content"] = text if text else ""
|
||||
|
||||
if is_async:
|
||||
return super()._transform_messages(
|
||||
messages=messages, model=model, is_async=True
|
||||
)
|
||||
else:
|
||||
return super()._transform_messages(
|
||||
messages=messages, model=model, is_async=False
|
||||
)
|
||||
|
||||
def remove_cache_control_flag_from_messages_and_tools(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
0
tests/litellm/llms/zai/__init__.py
Normal file
0
tests/litellm/llms/zai/__init__.py
Normal file
0
tests/litellm/llms/zai/chat/__init__.py
Normal file
0
tests/litellm/llms/zai/chat/__init__.py
Normal file
157
tests/litellm/llms/zai/chat/test_zai_chat_transformation.py
Normal file
157
tests/litellm/llms/zai/chat/test_zai_chat_transformation.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"""
|
||||
Unit tests for ZAI/GLM chat transformation.
|
||||
|
||||
Tests that list-format content in tool/assistant messages is flattened
|
||||
to strings before sending to GLM, which requires string-type content.
|
||||
|
||||
See: https://github.com/BerriAI/litellm/issues/25868
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.llms.zai.chat.transformation import ZAIChatConfig
|
||||
|
||||
|
||||
class TestZAITransformMessages:
|
||||
"""Test that ZAIChatConfig._transform_messages flattens tool/assistant content."""
|
||||
|
||||
def setup_method(self):
|
||||
self.config = ZAIChatConfig()
|
||||
|
||||
def test_tool_message_list_content_flattened(self):
|
||||
"""Tool message with list content is flattened to string."""
|
||||
messages = [
|
||||
{"role": "user", "content": "What is 1+1?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "calc", "arguments": '{"x": 1}'},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_1",
|
||||
"content": [{"type": "text", "text": "2"}],
|
||||
},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
tool_msg = [m for m in result if m.get("role") == "tool"][0]
|
||||
assert isinstance(tool_msg["content"], str)
|
||||
assert tool_msg["content"] == "2"
|
||||
|
||||
def test_tool_message_string_content_unchanged(self):
|
||||
"""Tool message with string content passes through."""
|
||||
messages = [
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "result text"},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert result[0]["content"] == "result text"
|
||||
|
||||
def test_assistant_message_list_content_flattened(self):
|
||||
"""Assistant message with list content is flattened."""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "Hello there"}],
|
||||
},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert isinstance(result[0]["content"], str)
|
||||
assert result[0]["content"] == "Hello there"
|
||||
|
||||
def test_user_message_not_modified(self):
|
||||
"""User messages are not modified by the ZAI transform.
|
||||
|
||||
User messages may contain image_url content parts that the parent
|
||||
class processes — we must not flatten those.
|
||||
"""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "hello"}],
|
||||
},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert isinstance(result[0]["content"], list)
|
||||
|
||||
def test_system_message_not_modified(self):
|
||||
"""System messages are not modified."""
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert result[0]["content"] == "You are helpful."
|
||||
|
||||
def test_tool_message_none_content_unchanged(self):
|
||||
"""Tool message with None content stays None."""
|
||||
messages = [
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": None},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert result[0]["content"] is None
|
||||
|
||||
def test_multiple_tool_messages_all_flattened(self):
|
||||
"""Multiple tool messages with list content are all flattened."""
|
||||
messages = [
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_1",
|
||||
"content": [{"type": "text", "text": "result 1"}],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_2",
|
||||
"content": [{"type": "text", "text": "result 2"}],
|
||||
},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert all(isinstance(m["content"], str) for m in result)
|
||||
assert result[0]["content"] == "result 1"
|
||||
assert result[1]["content"] == "result 2"
|
||||
|
||||
def test_tool_message_empty_list_becomes_empty_string(self):
|
||||
"""Tool message with empty list content becomes empty string."""
|
||||
messages = [
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": []},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert result[0]["content"] == ""
|
||||
|
||||
def test_non_text_content_parts_dropped(self):
|
||||
"""Non-text content parts (e.g., image_url) in tool messages are dropped."""
|
||||
messages = [
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_1",
|
||||
"content": [
|
||||
{"type": "image_url", "image_url": {"url": "https://example.com/img.png"}},
|
||||
{"type": "text", "text": "caption"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = self.config._transform_messages(messages, model="glm-4.6")
|
||||
|
||||
assert isinstance(result[0]["content"], str)
|
||||
assert "caption" in result[0]["content"]
|
||||
Loading…
Add table
Reference in a new issue