diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 48b12a5fba9..ce4293e9743 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -282,7 +282,81 @@ class LiteLLMCompletionResponsesConfig: ) ) - return messages + return LiteLLMCompletionResponsesConfig._merge_responses_system_messages( + messages=messages + ) + + @staticmethod + def _get_text_from_message_content(content: Any) -> str: + if content is None: + return "" + if isinstance(content, str): + return content + if isinstance(content, list): + text_parts: List[str] = [] + for content_part in content: + if isinstance(content_part, str): + text_parts.append(content_part) + elif isinstance(content_part, dict): + content_type = content_part.get("type") + text = content_part.get("text") + if content_type in {"text", "input_text"} and isinstance(text, str): + text_parts.append(text) + return "\n\n".join(text_parts) + return str(content) + + @staticmethod + def _merge_responses_system_messages( + messages: List[ + Union[ + AllMessageValues, + GenericChatCompletionMessage, + ChatCompletionMessageToolCall, + ChatCompletionResponseMessage, + Message, + ] + ], + ) -> List[ + Union[ + AllMessageValues, + GenericChatCompletionMessage, + ChatCompletionMessageToolCall, + ChatCompletionResponseMessage, + Message, + ] + ]: + system_parts: List[str] = [] + non_system_messages: List[ + Union[ + AllMessageValues, + GenericChatCompletionMessage, + ChatCompletionMessageToolCall, + ChatCompletionResponseMessage, + Message, + ] + ] = [] + + for message in messages: + role = message.get("role") if isinstance(message, dict) else None + if role in {"system", "developer"}: + content = message.get("content") if isinstance(message, dict) else None + text_content = ( + LiteLLMCompletionResponsesConfig._get_text_from_message_content( + content=content + ) + ) + if text_content: + system_parts.append(text_content) + continue + non_system_messages.append(message) + + if not system_parts: + return messages + + system_message = ChatCompletionSystemMessage( + role="system", content="\n\n".join(system_parts) + ) + return [system_message, *non_system_messages] @staticmethod async def async_responses_api_session_handler( diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 5d8ff8022e5..fdd96d26050 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -808,6 +808,98 @@ class TestFunctionCallTransformation: assert result["extra_headers"] == {"X-Test-Header": "test-value"} + def test_request_transformation_merges_instructions_and_developer_messages(self): + """Responses instructions and developer messages should become one system message.""" + test_input = [ + { + "type": "message", + "role": "developer", + "content": "Use concise responses.", + }, + { + "type": "message", + "role": "developer", + "content": [{"type": "input_text", "text": "Never expose secrets."}], + }, + { + "type": "message", + "role": "user", + "content": "Hello", + }, + ] + responses_api_request = {"instructions": "You are helpful."} + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="custom_openai/qwen", + input=test_input, + responses_api_request=responses_api_request, + ) + + messages = result["messages"] + assert messages == [ + { + "role": "system", + "content": "You are helpful.\n\nUse concise responses.\n\nNever expose secrets.", + }, + {"role": "user", "content": "Hello"}, + ] + + def test_request_transformation_preserves_messages_without_system_content(self): + """Requests without instructions/developer content should not be rewritten.""" + test_input = [ + { + "type": "message", + "role": "user", + "content": "Hello", + }, + { + "type": "message", + "role": "assistant", + "content": "Hi there", + }, + ] + + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="custom_openai/qwen", + input=test_input, + responses_api_request={}, + ) + + assert result["messages"] == [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there"}, + ] + + def test_merge_responses_system_messages_extracts_supported_text_content(self): + """System/developer content blocks should merge only supported text parts.""" + messages = [ + {"role": "system", "content": None}, + { + "role": "developer", + "content": [ + "Plain text", + {"type": "text", "text": "Text block"}, + {"type": "input_text", "text": "Input text block"}, + {"type": "image_url", "text": "Ignored image text"}, + {"type": "text", "text": 123}, + ], + }, + {"role": "developer", "content": 123}, + {"role": "user", "content": "Hello"}, + ] + + result = LiteLLMCompletionResponsesConfig._merge_responses_system_messages( + messages=messages + ) + + assert result == [ + { + "role": "system", + "content": "Plain text\n\nText block\n\nInput text block\n\n123", + }, + {"role": "user", "content": "Hello"}, + ] + def test_function_call_without_call_id_fallback_to_id(self): """Test that function_call items can use 'id' field when 'call_id' is missing""" function_call_item = {