Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
ResponseInputParam,
ResponsesAPIOptionalRequestParams,
ResponsesAPIResponse,
ResponsesAPIStatus,
)
from litellm.types.responses.main import (
GenericResponseOutputItem,
Expand Down Expand Up @@ -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],
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions litellm/types/llms/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
Loading