mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
Merge 18de9e6eff into 883b722fd2
This commit is contained in:
commit
d69670dc73
2 changed files with 134 additions and 8 deletions
|
|
@ -434,22 +434,59 @@ class LiteLLMCompletionResponsesConfig:
|
|||
| ChatCompletionResponseMessage
|
||||
| Message
|
||||
] = []
|
||||
if responses_api_request.get("instructions"):
|
||||
messages.append(
|
||||
LiteLLMCompletionResponsesConfig.transform_instructions_to_system_message(
|
||||
responses_api_request.get("instructions")
|
||||
)
|
||||
)
|
||||
|
||||
messages.extend(
|
||||
input_messages: Final = tuple(
|
||||
LiteLLMCompletionResponsesConfig._transform_response_input_param_to_chat_completion_message(
|
||||
input=input,
|
||||
replay_reasoning=replay_reasoning,
|
||||
)
|
||||
)
|
||||
|
||||
# `instructions` and the input can each carry a system message, which left a
|
||||
# conversation reading system, user, system. Chat templates that require the
|
||||
# system message first reject that, so gather them into one leading message.
|
||||
instructions: Final = responses_api_request.get("instructions")
|
||||
system_contents: Final = tuple(
|
||||
content
|
||||
for content in (
|
||||
instructions,
|
||||
*(message.get("content") for message in input_messages if message.get("role") == "system"),
|
||||
)
|
||||
if content
|
||||
)
|
||||
|
||||
if system_contents:
|
||||
messages.append(LiteLLMCompletionResponsesConfig._merge_system_contents(system_contents))
|
||||
|
||||
messages.extend(message for message in input_messages if message.get("role") != "system")
|
||||
|
||||
return messages
|
||||
|
||||
@staticmethod
|
||||
def _merge_system_contents(contents: tuple[Any, ...]) -> ChatCompletionSystemMessage:
|
||||
"""Join system prompts into one leading message.
|
||||
|
||||
Part lists collapse to their text: system content is conventionally a string, and
|
||||
the backends that reject a trailing system message are the same ones that expect one.
|
||||
"""
|
||||
texts: Final = tuple(
|
||||
text for content in contents for text in LiteLLMCompletionResponsesConfig._system_content_texts(content)
|
||||
)
|
||||
return ChatCompletionSystemMessage(role="system", content="\n\n".join(text for text in texts if text))
|
||||
|
||||
@staticmethod
|
||||
def _system_content_texts(content: object) -> tuple[str, ...]:
|
||||
"""Every text a system content field carries, as a plain string or as a part list
|
||||
that may mix bare strings with text parts."""
|
||||
if isinstance(content, str):
|
||||
return (content,)
|
||||
if not isinstance(content, (list, tuple)):
|
||||
return ()
|
||||
return tuple(
|
||||
part if isinstance(part, str) else str(part.get("text", ""))
|
||||
for part in content
|
||||
if isinstance(part, (str, dict))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def async_responses_api_session_handler(
|
||||
previous_response_id: str,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
Unit tests for keeping system messages at the front of the bridged chat request.
|
||||
|
||||
``instructions`` becomes a leading system message and the Responses input can carry
|
||||
a system message of its own, so an Anthropic ``/v1/messages`` conversation bridged
|
||||
to chat completions could come out as system, user, system. Chat templates that
|
||||
require the system message to come first reject that with
|
||||
"System message must be at the beginning".
|
||||
|
||||
See: https://github.com/BerriAI/litellm/issues/40693
|
||||
"""
|
||||
|
||||
from litellm.responses.litellm_completion_transformation.transformation import (
|
||||
LiteLLMCompletionResponsesConfig,
|
||||
)
|
||||
|
||||
|
||||
def _roles(input, responses_api_request):
|
||||
messages = LiteLLMCompletionResponsesConfig.transform_responses_api_input_to_messages(
|
||||
input=input, responses_api_request=responses_api_request
|
||||
)
|
||||
return [message.get("role") for message in messages], messages
|
||||
|
||||
|
||||
def test_system_message_after_user_content_is_hoisted():
|
||||
roles, messages = _roles(
|
||||
[
|
||||
{"role": "user", "content": "first question"},
|
||||
{"role": "system", "content": "mid"},
|
||||
{"role": "user", "content": "second question"},
|
||||
],
|
||||
{"instructions": "lead"},
|
||||
)
|
||||
|
||||
assert roles == ["system", "user", "user"]
|
||||
assert "system" not in roles[1:]
|
||||
|
||||
|
||||
def test_both_system_prompts_survive_the_merge():
|
||||
_, messages = _roles(
|
||||
[
|
||||
{"role": "user", "content": "q"},
|
||||
{"role": "system", "content": "mid"},
|
||||
],
|
||||
{"instructions": "lead"},
|
||||
)
|
||||
|
||||
assert messages[0]["content"] == "lead\n\nmid"
|
||||
|
||||
|
||||
def test_part_list_content_collapses_to_its_text():
|
||||
"""A system message given as parts still contributes its text to the merged prompt."""
|
||||
_, messages = _roles(
|
||||
[
|
||||
{"role": "user", "content": "q"},
|
||||
{"role": "system", "content": [{"type": "text", "text": "B"}]},
|
||||
],
|
||||
{"instructions": "A"},
|
||||
)
|
||||
|
||||
assert messages[0]["content"] == "A\n\nB"
|
||||
|
||||
|
||||
def test_an_already_leading_system_message_is_left_alone():
|
||||
"""The common case must not be rewritten."""
|
||||
roles, messages = _roles([{"role": "user", "content": "q"}], {"instructions": "lead"})
|
||||
|
||||
assert roles == ["system", "user"]
|
||||
assert messages[0]["content"] == "lead"
|
||||
|
||||
|
||||
def test_a_conversation_without_a_system_message_is_unchanged():
|
||||
roles, _ = _roles([{"role": "user", "content": "q"}], {})
|
||||
|
||||
assert roles == ["user"]
|
||||
|
||||
|
||||
def test_bare_strings_in_a_part_list_survive_the_merge():
|
||||
"""Upstream normalization leaves plain strings in a content list, so filtering the
|
||||
list down to dicts silently dropped part of the prompt."""
|
||||
_, messages = _roles(
|
||||
[
|
||||
{"role": "user", "content": "q"},
|
||||
{"role": "system", "content": ["B", {"type": "text", "text": "C"}]},
|
||||
],
|
||||
{"instructions": "A"},
|
||||
)
|
||||
|
||||
assert messages[0]["content"] == "A\n\nB\n\nC"
|
||||
Loading…
Add table
Reference in a new issue