mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
Merge pull request #23782 from voidborne-d/fix/map-system-message-list-content
This commit is contained in:
commit
c49ef4d28f
2 changed files with 126 additions and 31 deletions
|
|
@ -86,6 +86,17 @@ DEFAULT_ASSISTANT_CONTINUE_MESSAGE = ChatCompletionAssistantMessage(
|
|||
) # similar to autogen. Only used if `litellm.modify_params=True`.
|
||||
|
||||
|
||||
def _get_content_as_str(content: Union[str, list, None]) -> str:
|
||||
"""Extract text from content that may be a string, a list of content blocks, or None."""
|
||||
if content is None:
|
||||
return ""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
return convert_content_list_to_str({"role": "user", "content": content})
|
||||
return ""
|
||||
|
||||
|
||||
def map_system_message_pt(messages: list) -> list:
|
||||
"""
|
||||
Convert 'system' message to 'user' message if provider doesn't support 'system' role.
|
||||
|
|
@ -100,6 +111,7 @@ def map_system_message_pt(messages: list) -> list:
|
|||
new_messages = []
|
||||
for i, m in enumerate(messages):
|
||||
if m["role"] == "system":
|
||||
system_text = _get_content_as_str(m["content"])
|
||||
if i < len(messages) - 1: # Not the last message
|
||||
next_m = messages[i + 1]
|
||||
next_role = next_m["role"]
|
||||
|
|
@ -107,13 +119,16 @@ def map_system_message_pt(messages: list) -> list:
|
|||
next_role == "user" or next_role == "assistant"
|
||||
): # Next message is a user or assistant message
|
||||
# Merge system prompt into the next message
|
||||
next_m["content"] = m["content"] + " " + next_m["content"]
|
||||
# Copy to avoid mutating the caller's original dict
|
||||
next_m = messages[i + 1] = {**next_m}
|
||||
next_text = _get_content_as_str(next_m["content"])
|
||||
next_m["content"] = " ".join(filter(None, [system_text, next_text]))
|
||||
elif next_role == "system": # Next message is a system message
|
||||
# Append a user message instead of the system message
|
||||
new_message = {"role": "user", "content": m["content"]}
|
||||
new_message = {"role": "user", "content": system_text}
|
||||
new_messages.append(new_message)
|
||||
else: # Last message
|
||||
new_message = {"role": "user", "content": m["content"]}
|
||||
new_message = {"role": "user", "content": system_text}
|
||||
new_messages.append(new_message)
|
||||
else: # Not a system message
|
||||
new_messages.append(m)
|
||||
|
|
@ -1393,10 +1408,10 @@ def convert_to_gemini_tool_call_invoke(
|
|||
if tool_calls is not None:
|
||||
for idx, tool in enumerate(tool_calls):
|
||||
if "function" in tool:
|
||||
gemini_function_call: Optional[
|
||||
VertexFunctionCall
|
||||
] = _gemini_tool_call_invoke_helper(
|
||||
function_call_params=tool["function"]
|
||||
gemini_function_call: Optional[VertexFunctionCall] = (
|
||||
_gemini_tool_call_invoke_helper(
|
||||
function_call_params=tool["function"]
|
||||
)
|
||||
)
|
||||
if gemini_function_call is not None:
|
||||
part_dict: VertexPartType = {
|
||||
|
|
@ -1540,9 +1555,7 @@ def convert_to_gemini_tool_call_result( # noqa: PLR0915
|
|||
file_data = (
|
||||
file_content.get("file_data", "")
|
||||
if isinstance(file_content, dict)
|
||||
else file_content
|
||||
if isinstance(file_content, str)
|
||||
else ""
|
||||
else file_content if isinstance(file_content, str) else ""
|
||||
)
|
||||
|
||||
if file_data:
|
||||
|
|
@ -2046,9 +2059,9 @@ def _sanitize_empty_text_content(
|
|||
if isinstance(content, str):
|
||||
if not content or not content.strip():
|
||||
message = cast(AllMessageValues, dict(message)) # Make a copy
|
||||
message[
|
||||
"content"
|
||||
] = "[System: Empty message content sanitised to satisfy protocol]"
|
||||
message["content"] = (
|
||||
"[System: Empty message content sanitised to satisfy protocol]"
|
||||
)
|
||||
verbose_logger.debug(
|
||||
f"_sanitize_empty_text_content: Replaced empty text content in {message.get('role')} message"
|
||||
)
|
||||
|
|
@ -2388,9 +2401,9 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
# Convert ChatCompletionImageUrlObject to dict if needed
|
||||
image_url_value = m["image_url"]
|
||||
if isinstance(image_url_value, str):
|
||||
image_url_input: Union[
|
||||
str, dict[str, Any]
|
||||
] = image_url_value
|
||||
image_url_input: Union[str, dict[str, Any]] = (
|
||||
image_url_value
|
||||
)
|
||||
else:
|
||||
# ChatCompletionImageUrlObject or dict case - convert to dict
|
||||
image_url_input = {
|
||||
|
|
@ -2417,9 +2430,9 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
)
|
||||
|
||||
if "cache_control" in _content_element:
|
||||
_anthropic_content_element[
|
||||
"cache_control"
|
||||
] = _content_element["cache_control"]
|
||||
_anthropic_content_element["cache_control"] = (
|
||||
_content_element["cache_control"]
|
||||
)
|
||||
user_content.append(_anthropic_content_element)
|
||||
elif m.get("type", "") == "text":
|
||||
m = cast(ChatCompletionTextObject, m)
|
||||
|
|
@ -2479,9 +2492,9 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
)
|
||||
|
||||
if "cache_control" in _content_element:
|
||||
_anthropic_content_text_element[
|
||||
"cache_control"
|
||||
] = _content_element["cache_control"]
|
||||
_anthropic_content_text_element["cache_control"] = (
|
||||
_content_element["cache_control"]
|
||||
)
|
||||
|
||||
user_content.append(_anthropic_content_text_element)
|
||||
|
||||
|
|
@ -2614,9 +2627,9 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
original_content_element=dict(assistant_content_block),
|
||||
)
|
||||
if "cache_control" in _content_element:
|
||||
_anthropic_text_content_element[
|
||||
"cache_control"
|
||||
] = _content_element["cache_control"]
|
||||
_anthropic_text_content_element["cache_control"] = (
|
||||
_content_element["cache_control"]
|
||||
)
|
||||
text_element = _anthropic_text_content_element
|
||||
|
||||
# Interleave: each thinking block precedes its server tool group.
|
||||
|
|
@ -2776,9 +2789,9 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
)
|
||||
|
||||
if "cache_control" in _content_element:
|
||||
_anthropic_text_content_element[
|
||||
"cache_control"
|
||||
] = _content_element["cache_control"]
|
||||
_anthropic_text_content_element["cache_control"] = (
|
||||
_content_element["cache_control"]
|
||||
)
|
||||
|
||||
assistant_content.append(_anthropic_text_content_element)
|
||||
|
||||
|
|
@ -5220,9 +5233,7 @@ def default_response_schema_prompt(response_schema: dict) -> str:
|
|||
prompt_str = """Use this JSON schema:
|
||||
```json
|
||||
{}
|
||||
```""".format(
|
||||
response_schema
|
||||
)
|
||||
```""".format(response_schema)
|
||||
return prompt_str
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,90 @@ def test_supports_system_message():
|
|||
assert isinstance(response, litellm.ModelResponse)
|
||||
|
||||
|
||||
def test_supports_system_message_list_content():
|
||||
"""
|
||||
Test map_system_message_pt when content is a list of content blocks
|
||||
(e.g. from Anthropic pass-through endpoint).
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/23757
|
||||
"""
|
||||
# System message with list content (Anthropic format)
|
||||
messages = [
|
||||
{"role": "system", "content": [{"type": "text", "text": "You are helpful."}]},
|
||||
{"role": "user", "content": [{"type": "text", "text": "Hello!"}]},
|
||||
]
|
||||
|
||||
new_messages = map_system_message_pt(messages=messages)
|
||||
|
||||
assert len(new_messages) == 1
|
||||
assert new_messages[0]["role"] == "user"
|
||||
assert isinstance(new_messages[0]["content"], str)
|
||||
assert "You are helpful." in new_messages[0]["content"]
|
||||
assert "Hello!" in new_messages[0]["content"]
|
||||
|
||||
|
||||
def test_supports_system_message_mixed_content():
|
||||
"""
|
||||
Test map_system_message_pt with mixed str and list content types.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "system", "content": "System prompt"},
|
||||
{"role": "user", "content": [{"type": "text", "text": "User message"}]},
|
||||
]
|
||||
|
||||
new_messages = map_system_message_pt(messages=messages)
|
||||
|
||||
assert len(new_messages) == 1
|
||||
assert new_messages[0]["role"] == "user"
|
||||
assert isinstance(new_messages[0]["content"], str)
|
||||
assert "System prompt" in new_messages[0]["content"]
|
||||
assert "User message" in new_messages[0]["content"]
|
||||
|
||||
|
||||
def test_supports_system_message_list_content_last_message():
|
||||
"""
|
||||
Test map_system_message_pt when system message with list content is the last message.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "system", "content": [{"type": "text", "text": "Only system"}]},
|
||||
]
|
||||
|
||||
new_messages = map_system_message_pt(messages=messages)
|
||||
|
||||
assert len(new_messages) == 1
|
||||
assert new_messages[0]["role"] == "user"
|
||||
assert new_messages[0]["content"] == "Only system"
|
||||
|
||||
|
||||
def test_supports_system_message_none_content():
|
||||
"""
|
||||
Test map_system_message_pt when next message has content=None (e.g. assistant
|
||||
tool-call messages). Should not produce the literal string 'None'.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "system", "content": "Be helpful."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "1",
|
||||
"type": "function",
|
||||
"function": {"name": "f", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
new_messages = map_system_message_pt(messages=messages)
|
||||
|
||||
assert len(new_messages) == 1
|
||||
assert new_messages[0]["role"] == "assistant"
|
||||
# content should start with system text, not contain literal "None"
|
||||
assert "None" not in new_messages[0]["content"]
|
||||
assert "Be helpful." in new_messages[0]["content"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stop_sequence, expected_count", [("\n", 0), (["\n"], 0), (["finish_reason"], 1)]
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue