This commit is contained in:
Arpit Gupta 2026-09-13 05:38:54 +08:00 committed by GitHub
commit 07b3bd1d64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 3 deletions

View file

@ -262,6 +262,19 @@ def tool_call_dict_from_output_item(item: Mapping[str, Any], index: int) -> _Cha
return tool_call_dict
def _is_silent_output(output_items: Sequence[object]) -> bool:
"""True when a completed response carries no message and no tool call.
Covers an empty ``output`` list and a reasoning-only list. Both are legal
``status: completed`` responses that convert to zero chat choices.
"""
for item in output_items:
item_type = item.get("type") if isinstance(item, Mapping) else getattr(item, "type", None)
if item_type != "reasoning":
return False
return True
def _flat_responses_tool_choice(choice_type: str, name: str) -> ToolChoiceFunctionParam | ToolChoiceCustomParam:
if choice_type == "custom":
return ToolChoiceCustomParam(type="custom", name=name)
@ -784,7 +797,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
@staticmethod
def _build_empty_incomplete_choice(
output_items: Sequence[object],
finish_reason: Literal["length", "content_filter"],
finish_reason: Literal["length", "content_filter", "stop"],
) -> "Choices":
from litellm.types.utils import Choices, Message
@ -913,7 +926,15 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
)
if len(choices) == 0 and not response_is_incomplete:
raise ValueError(f"Unknown items in responses API response: {output_items}")
if raw_response.status == "completed" and _is_silent_output(output_items):
# A completed turn with nothing to say: no message and no tool
# call (empty output, or reasoning only). Chat Completions
# returns an empty assistant message with finish_reason "stop"
# for this case; mirror it instead of failing a successful
# request with a 500.
choices.append(self._build_empty_incomplete_choice(output_items, "stop"))
else:
raise ValueError(f"Unknown items in responses API response: {output_items}")
if response_is_incomplete:
incomplete_finish_reason: Final = _map_incomplete_reason_to_finish_reason(

View file

@ -3662,14 +3662,58 @@ def test_transform_response_incomplete_content_filter_maps_finish_reason():
assert result.choices[0].message.content == ""
def test_transform_response_zero_choices_not_incomplete_still_raises():
def test_transform_response_zero_choices_not_completed_still_raises():
handler = LiteLLMResponsesTransformationHandler()
raw_response = _make_empty_responses_api_response()
raw_response.status = "in_progress"
with pytest.raises(ValueError, match="Unknown items"):
_call_transform_response(handler, raw_response)
def test_transform_response_unknown_completed_item_still_raises():
handler = LiteLLMResponsesTransformationHandler()
raw_response = _make_empty_responses_api_response()
raw_response.output = [{"type": "mystery_item", "id": "x_1"}]
with pytest.raises(ValueError, match="Unknown items"):
_call_transform_response(handler, raw_response)
def test_transform_response_empty_completed_output_returns_silent_stop_choice():
"""A ``status: completed`` response with ``output: []`` is a legal silent turn
(seen live from gpt-5.6 on Bedrock Mantle right after a tool result). The
bridge mirrors Chat Completions: one empty assistant message with
finish_reason "stop", usage preserved, instead of a 500 (#37039)."""
handler = LiteLLMResponsesTransformationHandler()
raw_response = _make_empty_responses_api_response()
result = _call_transform_response(handler, raw_response)
assert len(result.choices) == 1
choice = result.choices[0]
assert choice.finish_reason == "stop"
assert choice.index == 0
assert choice.message.role == "assistant"
assert choice.message.content == ""
assert not choice.message.tool_calls
assert result.usage.total_tokens == 2
def test_transform_response_reasoning_only_completed_output_returns_silent_stop_choice():
handler = LiteLLMResponsesTransformationHandler()
raw_response = _make_empty_responses_api_response()
raw_response.output = [_make_reasoning_only_output_item()]
result = _call_transform_response(handler, raw_response)
assert len(result.choices) == 1
choice = result.choices[0]
assert choice.finish_reason == "stop"
assert choice.message.content == ""
assert choice.message.reasoning_items[0]["encrypted_content"] == "enc_abc"
def test_transform_response_completed_with_reasonless_incomplete_details_keeps_stop():
from openai.types.responses import ResponseOutputMessage, ResponseOutputText