From 19e6de988a17cb5c1ef5229f39a525e79ea710b6 Mon Sep 17 00:00:00 2001 From: shivam Date: Sat, 25 Jul 2026 02:08:24 +0000 Subject: [PATCH] fix(responses): emit spec-shaped reasoning items from the chat completions bridge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../transformation.py | 20 ++++--- litellm/types/llms/openai.py | 2 + litellm/types/responses/main.py | 24 +++++++++ .../test_litellm_completion_responses.py | 7 ++- .../test_reasoning_content_transformation.py | 52 +++++++++++++++++++ 5 files changed, 92 insertions(+), 13 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 6b1ca3564e3..10dcee8fb0d 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -54,6 +54,9 @@ from litellm.types.responses.main import ( OutputFunctionToolCall, OutputImageGenerationCall, OutputText, + ReasoningOutputItem, + ReasoningSummaryText, + ReasoningText, ) from litellm.types.utils import ( ChatCompletionAnnotation, @@ -1649,6 +1652,7 @@ class LiteLLMCompletionResponsesConfig: responses_api_request: ResponsesAPIOptionalRequestParams | None = None, ) -> list[ GenericResponseOutputItem + | ReasoningOutputItem | OutputCodeInterpreterCall | OutputFunctionToolCall | OutputImageGenerationCall @@ -1657,6 +1661,7 @@ class LiteLLMCompletionResponsesConfig: ]: responses_output: list[ GenericResponseOutputItem + | ReasoningOutputItem | OutputCodeInterpreterCall | OutputFunctionToolCall | OutputImageGenerationCall @@ -1733,27 +1738,20 @@ class LiteLLMCompletionResponsesConfig: def _extract_reasoning_output_items( chat_completion_response: ModelResponse, choices: list[Choices], - ) -> list[GenericResponseOutputItem]: + ) -> list[ReasoningOutputItem]: for choice in choices: if hasattr(choice, "message") and choice.message: message = choice.message if hasattr(message, "reasoning_content") and message.reasoning_content: # Only check the first choice for reasoning content return [ - GenericResponseOutputItem( - type="reasoning", + ReasoningOutputItem( id=f"rs_{hash(str(message.reasoning_content))}", status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( choice.finish_reason ), - role="assistant", - content=[ - OutputText( - type="output_text", - text=message.reasoning_content, - annotations=[], - ) - ], + summary=[ReasoningSummaryText(text=message.reasoning_content)], + content=[ReasoningText(text=message.reasoning_content)], ) ] return [] diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 314bb653196..e18499c7d1e 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -95,6 +95,7 @@ from litellm.types.responses.main import ( OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, + ReasoningOutputItem, ) FileContent = Union[IO[bytes], bytes, PathLike] @@ -1256,6 +1257,7 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): List[ Union[ GenericResponseOutputItem, + ReasoningOutputItem, OutputCodeInterpreterCall, OutputFunctionToolCall, OutputImageGenerationCall, diff --git a/litellm/types/responses/main.py b/litellm/types/responses/main.py index 32e07f9e52f..7a8cfa9c41a 100644 --- a/litellm/types/responses/main.py +++ b/litellm/types/responses/main.py @@ -101,6 +101,30 @@ class CustomToolCallOutputItem(BaseLiteLLMOpenAIResponseObject): status: Optional[Literal["in_progress", "completed", "incomplete"]] = None +class ReasoningSummaryText(BaseLiteLLMOpenAIResponseObject): + """A summary part of a Responses API reasoning item""" + + type: Literal["summary_text"] = "summary_text" + text: str + + +class ReasoningText(BaseLiteLLMOpenAIResponseObject): + """A raw chain-of-thought part of a Responses API reasoning item""" + + type: Literal["reasoning_text"] = "reasoning_text" + text: str + + +class ReasoningOutputItem(BaseLiteLLMOpenAIResponseObject): + """A Responses API reasoning output item""" + + type: Literal["reasoning"] = "reasoning" + id: str + status: Optional[str] = None + summary: List[ReasoningSummaryText] + content: List[ReasoningText] + + class GenericResponseOutputItem(BaseLiteLLMOpenAIResponseObject): """ Generic response API output item 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 d8e3f495ced..4801dfb951f 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 @@ -319,9 +319,12 @@ class TestLiteLLMCompletionResponsesConfig: "rs_" ), f"Expected ID to start with 'rs_', got: {reasoning_item.id}" assert reasoning_item.status == "completed" - assert reasoning_item.role == "assistant" + assert len(reasoning_item.summary) == 1 + assert reasoning_item.summary[0].type == "summary_text" + assert "step by step" in reasoning_item.summary[0].text + assert "42" in reasoning_item.summary[0].text assert len(reasoning_item.content) == 1 - assert reasoning_item.content[0].type == "output_text" + assert reasoning_item.content[0].type == "reasoning_text" assert "step by step" in reasoning_item.content[0].text assert "42" in reasoning_item.content[0].text 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 020b5de0a2a..f45b3fe5d05 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 @@ -184,6 +184,58 @@ class TestReasoningContentFinalResponse: == "Let me think step by step about this problem..." ) + def test_reasoning_item_uses_responses_api_shape(self): + """Reasoning items must carry summary_text/reasoning_text parts, not output_text. + + Clients key off ``summary[].text`` (and ``content[].reasoning_text``) to render + reasoning; emitting an ``output_text`` content part with no ``summary`` made every + reasoning block render as unavailable. + """ + response = ModelResponse( + id="test-id", + created=1234567890, + model="test-model", + object="chat.completion", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message( + content="Here is my answer", + role="assistant", + reasoning_content="Let me think step by step about this problem...", + ), + ) + ], + ) + + responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + request_input="Test input", + responses_api_request={}, + chat_completion_response=response, + ) + + serialized = responses_api_response.model_dump() + reasoning_items = [ + item for item in serialized["output"] if item["type"] == "reasoning" + ] + assert len(reasoning_items) == 1 + + reasoning_item = reasoning_items[0] + assert reasoning_item["summary"] == [ + { + "type": "summary_text", + "text": "Let me think step by step about this problem...", + } + ] + assert reasoning_item["content"] == [ + { + "type": "reasoning_text", + "text": "Let me think step by step about this problem...", + } + ] + assert reasoning_item["status"] == "completed" + def test_no_reasoning_content_in_response(self): """Test handling when no reasoning content in response""" # Setup