mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(zai): flatten list-format content in tool/assistant messages before sending to GLM
GLM's Jinja chat template checks ``m.content is string`` and silently
drops list-format content (e.g. [{"type": "text", "text": "..."}]),
causing tool results to be lost and the model to respond as if the tool
returned no data.
Add ZAIChatConfig._transform_messages() that normalises list-format
content in tool and assistant messages to plain strings before delegating
to the parent OpenAIGPTConfig transformer. User-facing content in user
messages is not affected.
Fixes #25868
This commit is contained in:
parent
850fe595ac
commit
3469bb0f1f
2 changed files with 157 additions and 1 deletions
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Optional, Tuple
|
||||
from typing import Any, Coroutine, List, Optional, Tuple, Union
|
||||
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam
|
||||
|
|
@ -8,6 +8,30 @@ from ...openai.chat.gpt_transformation import OpenAIGPTConfig
|
|||
ZAI_API_BASE = "https://api.z.ai/api/paas/v4"
|
||||
|
||||
|
||||
def _flatten_content_parts(content: Any) -> Any:
|
||||
"""Flatten OpenAI multi-part content to a plain string.
|
||||
|
||||
The OpenAI spec allows tool/assistant message content as either a plain
|
||||
string or a list of content parts (e.g. [{"type": "text", "text": "..."}]).
|
||||
GLM's chat template checks ``m.content is string`` and silently drops
|
||||
list-format content (same root cause as vllm-project/vllm#39614).
|
||||
This helper normalises both forms to a plain string.
|
||||
"""
|
||||
if isinstance(content, str) or content is None:
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for part in content:
|
||||
if isinstance(part, dict):
|
||||
text = part.get("text")
|
||||
if text:
|
||||
parts.append(text)
|
||||
elif isinstance(part, str):
|
||||
parts.append(part)
|
||||
return "\n".join(parts) if parts else ""
|
||||
return content
|
||||
|
||||
|
||||
class ZAIChatConfig(OpenAIGPTConfig):
|
||||
@property
|
||||
def custom_llm_provider(self) -> Optional[str]:
|
||||
|
|
@ -56,3 +80,21 @@ class ZAIChatConfig(OpenAIGPTConfig):
|
|||
pass
|
||||
|
||||
return base_params
|
||||
|
||||
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 and assistant messages before sending to ZAI.
|
||||
|
||||
GLM's chat template checks ``m.content is string`` and silently drops list-format
|
||||
content (e.g. [{"type": "text", "text": "..."}]). This ensures tool results and
|
||||
assistant messages always reach the model as plain strings.
|
||||
|
||||
Issue: https://github.com/BerriAI/litellm/issues/25868
|
||||
"""
|
||||
for message in messages:
|
||||
role = message.get("role")
|
||||
content = message.get("content")
|
||||
if role in ("tool", "assistant") and isinstance(content, list):
|
||||
message["content"] = _flatten_content_parts(content) # type: ignore
|
||||
return super()._transform_messages(messages=messages, model=model, is_async=is_async) # type: ignore
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Tests for Z.AI (Zhipu AI) provider - GLM models
|
|||
|
||||
import json
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
|
|
@ -179,3 +180,116 @@ def test_zai_sync_completion(respx_mock, zai_response, monkeypatch):
|
|||
|
||||
assert response.choices[0].message.content == "Hello! How can I help you today?"
|
||||
assert response.usage.total_tokens == 25
|
||||
|
||||
|
||||
class TestZAIMessageTransformation:
|
||||
"""Tests for ZAI message content flattening.
|
||||
|
||||
Issue: https://github.com/BerriAI/litellm/issues/25868
|
||||
GLM's Jinja chat template checks ``m.content is string`` and silently drops
|
||||
list-format content. ZAIChatConfig._transform_messages must flatten these
|
||||
before forwarding to z.ai.
|
||||
"""
|
||||
|
||||
def test_flatten_tool_message_content_list(self):
|
||||
"""Tool message with list-format content is flattened to a plain string."""
|
||||
from litellm.llms.zai.chat.transformation import ZAIChatConfig
|
||||
|
||||
config = ZAIChatConfig()
|
||||
messages = cast(
|
||||
list,
|
||||
[
|
||||
{"role": "user", "content": "What is the temperature in Tokyo?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Let me check.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "get_temp", "arguments": '{"city": "Tokyo"}'},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_1",
|
||||
"content": [{"type": "text", "text": "22.5\u00b0C, partly cloudy."}],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
result = config._transform_messages(messages=messages, model="glm-5.1")
|
||||
|
||||
tool_msg = result[2]
|
||||
assert isinstance(tool_msg["content"], str), (
|
||||
f"Expected str content, got {type(tool_msg['content'])}"
|
||||
)
|
||||
assert tool_msg["content"] == "22.5\u00b0C, partly cloudy."
|
||||
|
||||
def test_flatten_assistant_message_content_list(self):
|
||||
"""Assistant message with list-format content is flattened to a plain string."""
|
||||
from litellm.llms.zai.chat.transformation import ZAIChatConfig
|
||||
|
||||
config = ZAIChatConfig()
|
||||
messages = cast(
|
||||
list,
|
||||
[
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "Let me think about this."}],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
result = config._transform_messages(messages=messages, model="glm-5.1")
|
||||
|
||||
assert result[0]["content"] == "Let me think about this."
|
||||
|
||||
def test_string_content_passes_through_unchanged(self):
|
||||
"""String content is not modified by the flattening step."""
|
||||
from litellm.llms.zai.chat.transformation import ZAIChatConfig
|
||||
|
||||
config = ZAIChatConfig()
|
||||
messages = cast(
|
||||
list,
|
||||
[
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there!"},
|
||||
{"role": "tool", "tool_call_id": "c1", "content": "Result data"},
|
||||
],
|
||||
)
|
||||
|
||||
result = config._transform_messages(messages=messages, model="glm-5.1")
|
||||
|
||||
assert result[0]["content"] == "Hello"
|
||||
assert result[1]["content"] == "Hi there!"
|
||||
assert result[2]["content"] == "Result data"
|
||||
|
||||
def test_flatten_content_parts_helper_multipart(self):
|
||||
"""Multiple text parts are joined with newline."""
|
||||
from litellm.llms.zai.chat.transformation import _flatten_content_parts
|
||||
|
||||
content = [
|
||||
{"type": "text", "text": "Line 1"},
|
||||
{"type": "text", "text": "Line 2"},
|
||||
]
|
||||
assert _flatten_content_parts(content) == "Line 1\nLine 2"
|
||||
|
||||
def test_flatten_content_parts_helper_empty_list(self):
|
||||
"""Empty list returns empty string."""
|
||||
from litellm.llms.zai.chat.transformation import _flatten_content_parts
|
||||
|
||||
assert _flatten_content_parts([]) == ""
|
||||
|
||||
def test_flatten_content_parts_helper_string_passthrough(self):
|
||||
"""Plain string passes through unchanged."""
|
||||
from litellm.llms.zai.chat.transformation import _flatten_content_parts
|
||||
|
||||
assert _flatten_content_parts("already a string") == "already a string"
|
||||
|
||||
def test_flatten_content_parts_helper_none(self):
|
||||
"""None passes through unchanged."""
|
||||
from litellm.llms.zai.chat.transformation import _flatten_content_parts
|
||||
|
||||
assert _flatten_content_parts(None) is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue