diff --git a/litellm/responses/litellm_completion_transformation/session_handler.py b/litellm/responses/litellm_completion_transformation/session_handler.py index 08c49db8f6f..127e3385c9d 100644 --- a/litellm/responses/litellm_completion_transformation/session_handler.py +++ b/litellm/responses/litellm_completion_transformation/session_handler.py @@ -152,9 +152,8 @@ class ResponsesSessionHandler: and _response_output and _response_output != {} ): - if ( - _response_output.get("object") == "response" - or "output" in _response_output + if ResponsesSessionHandler._is_responses_api_output_response( + _response_output ): chat_completion_message_history.extend( LiteLLMCompletionResponsesConfig.transform_responses_api_output_to_chat_completion_messages( @@ -170,6 +169,35 @@ class ResponsesSessionHandler: chat_completion_message_history.append(getattr(choice, "message")) return chat_completion_message_history + @staticmethod + def _is_responses_api_output_response(response_output: dict) -> bool: + if response_output.get("object") == "response": + return True + if "choices" in response_output: + return False + output = response_output.get("output") + if not isinstance(output, list): + return False + responses_output_types = { + "reasoning", + "message", + "function_call", + "function_call_output", + "web_search_call", + "file_search_call", + "computer_call", + "image_generation_call", + "code_interpreter_call", + "mcp_call", + "custom_tool_call", + } + for output_item in output: + if not isinstance(output_item, dict): + continue + if output_item.get("type") in responses_output_types: + return True + return False + @staticmethod async def get_proxy_server_request_from_spend_log( spend_log: SpendLogsPayload, diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 617856d60cf..4952dd28115 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -420,6 +420,15 @@ class LiteLLMCompletionResponsesConfig: ) if new_role != "assistant": continue + reasoning_content = ( + LiteLLMCompletionResponsesConfig._get_chat_message_reasoning_content( + new_msg + ) + ) + LiteLLMCompletionResponsesConfig._set_assistant_reasoning_content( + last_msg, + reasoning_content, + ) for tool_call in LiteLLMCompletionResponsesConfig._get_tool_calls_list( new_msg ): diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_reasoning_content_transformation.py b/tests/test_litellm/responses/litellm_completion_transformation/test_reasoning_content_transformation.py index 908ae26a822..37c788ad2e5 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_reasoning_content_transformation.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_reasoning_content_transformation.py @@ -325,6 +325,58 @@ class TestReasoningContentFinalResponse: assert tool_calls[0].get("id") == tool_call_id +def test_merge_consecutive_function_call_messages_preserves_reasoning_content(): + messages = [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_first", + "type": "function", + "function": { + "name": "first_tool", + "arguments": "{}", + }, + } + ], + } + ] + chat_completion_messages = [ + { + "role": "assistant", + "content": None, + "reasoning_content": "Need a second tool before answering.", + "tool_calls": [ + { + "id": "call_second", + "type": "function", + "function": { + "name": "second_tool", + "arguments": "{}", + }, + } + ], + } + ] + + merged = LiteLLMCompletionResponsesConfig._merge_consecutive_function_call_messages( + messages=messages, + chat_completion_messages=chat_completion_messages, + input_item={"type": "function_call", "call_id": "call_second"}, + existing_tool_call_ids={"call_first"}, + ) + + assert merged is True + assert messages[0].get("reasoning_content") == ( + "Need a second tool before answering." + ) + assert [tool_call.get("id") for tool_call in messages[0].get("tool_calls", [])] == [ + "call_first", + "call_second", + ] + + def test_streaming_chunk_id_raw(): """Test that streaming chunk IDs are raw (not encoded) to match OpenAI format""" chunk = ModelResponseStream( diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_session_handler.py b/tests/test_litellm/responses/litellm_completion_transformation/test_session_handler.py index 9c50390d6fe..79f3f45615c 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_session_handler.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_session_handler.py @@ -237,6 +237,52 @@ async def test_session_history_preserves_responses_reasoning_before_tool_call(): assert tool_calls[0].get("function", {}).get("arguments") == '{"location":"Boston"}' +@pytest.mark.asyncio +async def test_chat_completion_with_output_key_uses_choices_not_responses_output(): + mock_spend_logs = [ + { + "request_id": "chatcmpl-custom-output", + "call_type": "aresponses", + "session_id": "session-custom-output", + "proxy_server_request": { + "input": "Return a custom provider response.", + "model": "custom/provider-model", + }, + "response": { + "id": "chatcmpl-custom-output", + "object": "chat.completion", + "model": "custom/provider-model", + "output": [{"custom_provider_payload": "not a Responses API item"}], + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Provider answer from choices.", + }, + "finish_reason": "stop", + } + ], + }, + "status": "success", + } + ] + + with patch.object( + ResponsesSessionHandler, + "get_all_spend_logs_for_previous_response_id", + new_callable=AsyncMock, + ) as mock_get_spend_logs: + mock_get_spend_logs.return_value = mock_spend_logs + result = await ResponsesSessionHandler.get_chat_completion_message_history_for_previous_response_id( + "chatcmpl-custom-output" + ) + + messages = result["messages"] + assert [message.get("role") for message in messages] == ["user", "assistant"] + assert messages[1].get("content") == "Provider answer from choices." + + @pytest.mark.asyncio async def test_previous_response_tool_output_continuation_replays_reasoning_content(): """