From 9734defeada568ed2530ec1f96d2d4b69b89db0b Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Mon, 20 Oct 2025 17:23:16 -0700 Subject: [PATCH 1/3] _map_chat_completion_finish_reason_to_responses_status --- .../transformation.py | 47 +++++++++++++++++-- litellm/types/llms/openai.py | 9 ++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 9324ab0aa0c6..31637f6657eb 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -33,6 +33,7 @@ ResponseInputParam, ResponsesAPIOptionalRequestParams, ResponsesAPIResponse, + ResponsesAPIStatus, ) from litellm.types.responses.main import ( GenericResponseOutputItem, @@ -619,6 +620,34 @@ def transform_chat_completion_tools_to_responses_tools( ) return responses_tools + @staticmethod + def _map_chat_completion_finish_reason_to_responses_status( + finish_reason: Optional[str], + ) -> ResponsesAPIStatus: + """ + Map chat completion finish_reason to responses API status. + + Chat completion finish_reason values include: "stop", "length", "tool_calls", "content_filter", "function_call" + Responses API status values are: "completed", "failed", "in_progress", "cancelled", "queued", "incomplete" + + Args: + finish_reason: The finish_reason from a chat completion response + + Returns: + The corresponding responses API status value (one of ResponsesAPIStatus) + """ + if finish_reason is None: + return "completed" + + # Map finish reasons to status + if finish_reason in ["stop", "tool_calls", "function_call"]: + return "completed" + elif finish_reason in ["length", "content_filter"]: + return "incomplete" + else: + # Default to completed for unknown finish reasons + return "completed" + @staticmethod def transform_chat_completion_response_to_responses_api_response( request_input: Union[str, ResponseInputParam], @@ -630,6 +659,12 @@ def transform_chat_completion_response_to_responses_api_response( """ if isinstance(chat_completion_response, dict): chat_completion_response = ModelResponse(**chat_completion_response) + # Get finish_reason from the first choice to determine overall status + finish_reason: Optional[str] = None + choices: List[Choices] = getattr(chat_completion_response, "choices", []) + if choices and len(choices) > 0: + finish_reason = choices[0].finish_reason + responses_api_response: ResponsesAPIResponse = ResponsesAPIResponse( id=chat_completion_response.id, created_at=chat_completion_response.created, @@ -659,7 +694,9 @@ def transform_chat_completion_response_to_responses_api_response( chat_completion_response, "previous_response_id", None ), reasoning=Reasoning(), - status=getattr(chat_completion_response, "status", "completed"), + status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( + finish_reason + ), text={}, truncation=getattr(chat_completion_response, "truncation", None), usage=LiteLLMCompletionResponsesConfig._transform_chat_completion_usage_to_responses_usage( @@ -709,7 +746,9 @@ def _extract_reasoning_output_items( GenericResponseOutputItem( type="reasoning", id=f"rs_{hash(str(message.reasoning_content))}", - status=choice.finish_reason, + status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( + choice.finish_reason + ), role="assistant", content=[ OutputText( @@ -733,7 +772,9 @@ def _extract_message_output_items( GenericResponseOutputItem( type="message", id=chat_completion_response.id, - status=choice.finish_reason, + status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( + choice.finish_reason + ), role=choice.message.role, content=[ LiteLLMCompletionResponsesConfig._transform_chat_message_to_response_output_text( diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index a48d9a29911a..b2d170514af3 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1037,6 +1037,15 @@ class ResponseAPIUsage(BaseLiteLLMOpenAIResponseObject): model_config = {"extra": "allow"} +ResponsesAPIStatus = Literal[ + "completed", "failed", "in_progress", "cancelled", "queued", "incomplete" +] +""" +The status of the response generation. +One of: completed, failed, in_progress, cancelled, queued, or incomplete. +""" + + class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): id: str created_at: int From 1790b24bc54db89e6963fa26be2a2c7c53f6aea0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Mon, 20 Oct 2025 17:25:01 -0700 Subject: [PATCH 2/3] test_transform_chat_completion_response_with_reasoning_content --- .../test_litellm_completion_responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9c8da2e60e0c..20e9e453e485 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 @@ -266,7 +266,7 @@ def test_transform_chat_completion_response_with_reasoning_content(self): reasoning_item = reasoning_items[0] assert reasoning_item.id.startswith("rs_"), f"Expected ID to start with 'rs_', got: {reasoning_item.id}" - assert reasoning_item.status == "stop" + assert reasoning_item.status == "completed" assert reasoning_item.role == "assistant" assert len(reasoning_item.content) == 1 assert reasoning_item.content[0].type == "output_text" From 86d2c98d64c05576387a4e34b50005153c2e09f4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Mon, 20 Oct 2025 17:26:17 -0700 Subject: [PATCH 3/3] test_transform_chat_completion_response_output_item_status --- .../test_litellm_completion_responses.py | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) 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 20e9e453e485..333fb36cada3 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 @@ -369,7 +369,94 @@ def test_transform_chat_completion_response_multiple_choices_with_reasoning(self ] assert len(message_items) == 2, "Should have two message items" + def test_transform_chat_completion_response_status_with_stop(self): + """ + Test that transforming a chat completion response with 'stop' finish_reason + results in 'completed' status in the responses API response. + + This is the main test case for GitHub issue #15714. + """ + chat_completion_response = ModelResponse( + id="test-response-id", + created=1234567890, + model="gemini-2.5-flash-preview-09-2025", + object="chat.completion", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message( + content="That's completely fine! How can I help you with your test?", + role="assistant", + ), + ) + ], + ) + + responses_api_response = ( + LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + request_input="this is a test", + responses_api_request={}, + chat_completion_response=chat_completion_response, + ) + ) + + assert responses_api_response.status == "completed" + assert responses_api_response.status in [ + "completed", + "failed", + "in_progress", + "cancelled", + "queued", + "incomplete", + ] + + def test_transform_chat_completion_response_output_item_status(self): + """ + Test that output items in the transformed response also have valid status values. + + This verifies the fix for GitHub issue #15714. + """ + chat_completion_response = ModelResponse( + id="test-response-id", + created=1234567890, + model="gemini-2.5-flash-preview-09-2025", + object="chat.completion", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message( + content="Test message", + role="assistant", + ), + ) + ], + ) + + responses_api_response = ( + LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + request_input="this is a test", + responses_api_request={}, + chat_completion_response=chat_completion_response, + ) + ) + message_items = [ + item for item in responses_api_response.output if item.type == "message" + ] + assert len(message_items) > 0 + + for item in message_items: + assert item.status in [ + "completed", + "failed", + "in_progress", + "cancelled", + "queued", + "incomplete", + ] + assert item.status != "stop" class TestFunctionCallTransformation: