From 18de9e6effc49351f878ad1692f3f1937ad07462 Mon Sep 17 00:00:00 2001 From: Animesh Kumar Date: Fri, 11 Sep 2026 22:39:50 +0530 Subject: [PATCH] fix(responses): keep bare strings in a system content part list Filtering a content list down to dicts dropped plain strings, which upstream normalization leaves in place, so part of the system prompt went missing from the merged message. --- .../transformation.py | 19 +++++++++++++++---- .../test_system_message_hoisting.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 5501e87bfe4..b503ea363b1 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -468,13 +468,24 @@ class LiteLLMCompletionResponsesConfig: the backends that reject a trailing system message are the same ones that expect one. """ texts: Final = tuple( - content - if isinstance(content, str) - else "\n\n".join(part.get("text", "") for part in content if isinstance(part, dict)) - for content in contents + 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, diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_system_message_hoisting.py b/tests/test_litellm/responses/litellm_completion_transformation/test_system_message_hoisting.py index e58a68838fb..4b91694d73b 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_system_message_hoisting.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_system_message_hoisting.py @@ -73,3 +73,17 @@ 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"