mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
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:
parent
fc5ab31fba
commit
19e6de988a
5 changed files with 92 additions and 13 deletions
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue