mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(responses): return an empty assistant turn for a completed response with no output
A Responses API response with `status: completed` and `output: []` (or
reasoning items only) is a legal silent turn. gpt-5.6 on Bedrock Mantle
emits one after a tool result when it has nothing to add, and OpenAI's
own Chat Completions endpoint answers the same situation with an empty
assistant message and `finish_reason: stop`.
The Responses-to-Chat bridge converted that response to zero choices and
raised `ValueError("Unknown items in responses API response: []")`, which
the proxy surfaced as HTTP 500 on an otherwise successful request. Retries
reproduce the same silent turn, so the caller can never recover.
Mirror Chat Completions: when the response is completed and carries no
message and no tool call, return one empty assistant choice with
`finish_reason: stop` (reasoning content preserved, as for the incomplete
case) and the reported usage. A non-completed empty response and any
unrecognized item type still raise.
Fixes #37039.
This commit is contained in:
parent
9dbfb060bd
commit
599c9e2623
2 changed files with 68 additions and 3 deletions
|
|
@ -262,6 +262,19 @@ def _tool_call_dict_from_output_item(item: Mapping[str, Any], index: int) -> _Ch
|
|||
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)
|
||||
|
|
@ -779,7 +792,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
|
||||
|
||||
|
|
@ -908,7 +921,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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue