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>
This commit is contained in:
shivam 2026-07-25 02:08:24 +00:00
parent fc5ab31fba
commit 19e6de988a
5 changed files with 92 additions and 13 deletions

View file

@ -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 []

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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