diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 545b317055a..84e12bdfa9d 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -28238,6 +28238,9 @@ { "$ref": "#/components/schemas/CustomToolCallOutputItem" }, + { + "$ref": "#/components/schemas/ResponseReasoningItem" + }, { "$ref": "#/components/schemas/OutputToolSearchCall" } diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index d9ebf50cc1a..f2b1abbe236 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -28,8 +28,13 @@ from openai.types.chat.chat_completion_named_tool_choice_param import ( from openai.types.chat.chat_completion_named_tool_choice_param import ( Function as NamedToolChoiceFunction, ) -from openai.types.responses import ResponseFunctionToolCall, ResponseFunctionWebSearch +from openai.types.responses import ( + ResponseFunctionToolCall, + ResponseFunctionWebSearch, + ResponseReasoningItem, +) from openai.types.responses.response_create_params import ResponseInputParam +from openai.types.responses.response_reasoning_item import Content as ReasoningContent from openai.types.responses.tool_choice_custom_param import ToolChoiceCustomParam from openai.types.responses.tool_choice_function_param import ToolChoiceFunctionParam from openai.types.responses.tool_param import FunctionToolParam @@ -3037,6 +3042,7 @@ class LiteLLMCompletionResponsesConfig: | ResponseFunctionToolCall | ResponseFunctionWebSearch | CustomToolCallOutputItem + | ResponseReasoningItem | OutputToolSearchCall ]: responses_output: list[ @@ -3047,6 +3053,7 @@ class LiteLLMCompletionResponsesConfig: | ResponseFunctionToolCall | ResponseFunctionWebSearch | CustomToolCallOutputItem + | ResponseReasoningItem | OutputToolSearchCall ] = [] @@ -3124,7 +3131,7 @@ class LiteLLMCompletionResponsesConfig: def _extract_reasoning_output_items( chat_completion_response: ModelResponse, choices: list[Choices], - ) -> list[GenericResponseOutputItem]: + ) -> list[ResponseReasoningItem]: for choice in choices: if hasattr(choice, "message") and choice.message: message = choice.message @@ -3133,22 +3140,23 @@ class LiteLLMCompletionResponsesConfig: if reasoning_content or encrypted_content: # Only check the first choice for reasoning content return [ - GenericResponseOutputItem( + ResponseReasoningItem( type="reasoning", id=mint_reasoning_item_id(), - status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( - choice.finish_reason - ), - role="assistant", + # summary is required by the SDK/spec; the bridge has + # no summarized reasoning, so it stays empty + summary=[], content=[ - OutputText( - type="output_text", - text=text, - annotations=[], - ) + ReasoningContent(type="reasoning_text", text=text) for text in (reasoning_content,) if text ], + status=cast( # cast-ok: the map only returns completed/incomplete, both valid for a reasoning item's narrower status union + "Literal['in_progress', 'completed', 'incomplete']", + LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( + choice.finish_reason + ), + ), encrypted_content=encrypted_content, ) ] diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 8dbc0d97d54..cf3e4ec5c4f 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -64,6 +64,7 @@ from openai.types.responses.response_create_params import ( ) from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall from openai.types.responses.response_function_web_search import ResponseFunctionWebSearch +from openai.types.responses.response_reasoning_item import ResponseReasoningItem from pydantic import ( ConfigDict, Discriminator, @@ -1487,6 +1488,7 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): | ResponseFunctionToolCall | ResponseFunctionWebSearch | CustomToolCallOutputItem + | ResponseReasoningItem | OutputToolSearchCall ] ) diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index f34ca7d0ff8..66f677afe57 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -307,9 +307,10 @@ class TestLiteLLMCompletionResponsesConfig: if hasattr(reasoning_item, "id") and reasoning_item.id: assert reasoning_item.id.startswith("rs_"), f"Expected ID to start with 'rs_', got: {reasoning_item.id}" assert reasoning_item.status == "completed" - assert reasoning_item.role == "assistant" + assert reasoning_item.summary == [] + assert not hasattr(reasoning_item, "role") 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 @@ -319,6 +320,76 @@ class TestLiteLLMCompletionResponsesConfig: message_item = message_items[0] assert message_item.content[0].text == "The answer is 42." + def test_reasoning_output_items_validate_against_openai_sdk(self): + """Bridged reasoning items must carry a summary array and reasoning_text + content parts so ResponseReasoningItem accepts them (issue #45552).""" + from openai.types.responses import ResponseReasoningItem + + chat_completion_response = ModelResponse( + id="test-response-id", + created=1234567890, + model="test-model", + object="chat.completion", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message( + content="The answer is 42.", + role="assistant", + reasoning_content="Thinking about the question.", + ), + ) + ], + ) + + responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + request_input="What is the meaning of life?", + responses_api_request={}, + chat_completion_response=chat_completion_response, + ) + + reasoning_items = [ + item for item in responses_api_response.output if item.type == "reasoning" + ] + assert len(reasoning_items) == 1 + + dumped = reasoning_items[0].model_dump(mode="json") + assert dumped["summary"] == [] + assert dumped["content"][0]["type"] == "reasoning_text" + assert "role" not in dumped + assert "phase" not in dumped + + validated = ResponseReasoningItem.model_validate(dumped) + assert validated.summary == [] + + def test_reasoning_output_item_without_text_still_validates(self): + """Encrypted-only reasoning (empty content) must still carry summary: [] and + validate against the SDK (issue #45552, display: omitted case).""" + from openai.types.responses import ResponseReasoningItem + + message = Message(role="assistant", content="apple") + message.thinking_blocks = [ + {"type": "thinking", "thinking": "", "signature": "sig"} + ] + choice = Choices(index=0, finish_reason="stop", message=message) + + items = LiteLLMCompletionResponsesConfig._extract_reasoning_output_items( + chat_completion_response=ModelResponse( + id="test-response-id", + created=1234567890, + model="test-model", + object="chat.completion", + ), + choices=[choice], + ) + + assert len(items) == 1 + dumped = items[0].model_dump(mode="json") + assert dumped["summary"] == [] + assert dumped["content"] == [] + ResponseReasoningItem.model_validate(dumped) + def test_transform_chat_completion_response_without_reasoning_content(self): """Test that transformation works normally when no reasoning content is present""" # Setup diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 2b174a1141a..ad1688acf59 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -44279,7 +44279,7 @@ export interface components { /** Output */ output: (components["schemas"]["ResponseOutputMessage"] | components["schemas"]["ResponseFileSearchToolCall"] | components["schemas"]["ResponseFunctionToolCall"] | components["schemas"]["ResponseFunctionToolCallOutputItem"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["ResponseComputerToolCall"] | components["schemas"]["ResponseComputerToolCallOutputItem"] | components["schemas"]["ResponseReasoningItem"] | components["schemas"]["ResponseToolSearchCall"] | components["schemas"]["ResponseToolSearchOutputItem"] | components["schemas"]["ResponseCompactionItem"] | components["schemas"]["ImageGenerationCall"] | components["schemas"]["ResponseCodeInterpreterToolCall"] | components["schemas"]["LocalShellCall"] | components["schemas"]["LocalShellCallOutput"] | components["schemas"]["ResponseFunctionShellToolCall"] | components["schemas"]["ResponseFunctionShellToolCallOutput"] | components["schemas"]["ResponseApplyPatchToolCall"] | components["schemas"]["ResponseApplyPatchToolCallOutput"] | components["schemas"]["McpCall"] | components["schemas"]["McpListTools"] | components["schemas"]["McpApprovalRequest"] | components["schemas"]["McpApprovalResponse"] | components["schemas"]["ResponseCustomToolCall"] | components["schemas"]["ResponseCustomToolCallOutputItem"] | { [key: string]: unknown; - })[] | (components["schemas"]["GenericResponseOutputItem"] | components["schemas"]["OutputCodeInterpreterCall"] | components["schemas"]["OutputFunctionToolCall"] | components["schemas"]["OutputImageGenerationCall"] | components["schemas"]["ResponseFunctionToolCall"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["CustomToolCallOutputItem"] | components["schemas"]["OutputToolSearchCall"])[]; + })[] | (components["schemas"]["GenericResponseOutputItem"] | components["schemas"]["OutputCodeInterpreterCall"] | components["schemas"]["OutputFunctionToolCall"] | components["schemas"]["OutputImageGenerationCall"] | components["schemas"]["ResponseFunctionToolCall"] | components["schemas"]["ResponseFunctionWebSearch"] | components["schemas"]["CustomToolCallOutputItem"] | components["schemas"]["ResponseReasoningItem"] | components["schemas"]["OutputToolSearchCall"])[]; /** Parallel Tool Calls */ parallel_tool_calls?: boolean | null; /** Previous Response Id */