fix(responses-adapter): skip null reasoning summary text and keep thinking-only assistant turns

This commit is contained in:
mateo-berri 2026-08-22 16:29:35 -07:00
parent 68bda94995
commit 8a1fe281fd
4 changed files with 41 additions and 3 deletions

View file

@ -428,12 +428,14 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
if role == "assistant":
input_items.extend(_reasoning_input_items(msg))
input_items.append(
{
{ # mutable-ok: API message payload
"type": "message",
"role": role,
"content": self._convert_content_to_responses_format(content, cast(str, role)),
}
)
elif role == "assistant":
input_items.extend(_reasoning_input_items(msg))
return input_items, instructions

View file

@ -110,8 +110,8 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
def _summary_part_text(part: object) -> str:
if isinstance(part, Mapping):
mapping: Final = cast(Mapping[str, Any], part) # cast-ok: summary parts are untyped provider json
return str(mapping.get("text", ""))
return str(getattr(part, "text", ""))
return str(mapping.get("text") or "")
return str(getattr(part, "text", "") or "")
@classmethod
def _thinking_blocks_from_reasoning_item(

View file

@ -3824,6 +3824,28 @@ def test_assistant_thinking_blocks_become_a_reasoning_input_item():
assert "id" not in reasoning_item
def test_thinking_only_assistant_turn_still_sends_its_reasoning():
"""An assistant turn can be pure reasoning, with no visible text and no tool call."""
handler = LiteLLMResponsesTransformationHandler()
messages = [
{"role": "user", "content": "What is the weather in Denver?"},
{
"role": "assistant",
"content": None,
"thinking_blocks": [
{"type": "thinking", "thinking": "August in Denver is dry.", "signature": "sig1"}
],
},
{"role": "user", "content": "Why?"},
]
input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
reasoning_items = [item for item in input_items if item.get("type") == "reasoning"]
assert len(reasoning_items) == 1
assert reasoning_items[0]["summary"] == [{"type": "summary_text", "text": "August in Denver is dry."}]
def test_stored_reasoning_items_win_over_thinking_blocks():
"""A minted reasoning id beats a re-derived one, so the two must not both be sent."""
handler = LiteLLMResponsesTransformationHandler()

View file

@ -1249,6 +1249,20 @@ class TestTranslateResponse:
result: Any = _ADAPTER.translate_response(response)
assert result["content"] == []
def test_null_summary_text_skipped_rather_than_stringified(self):
"""A summary part whose text is null must not reach the client as the word "None"."""
response = _make_mock_response(
output=[
{
"type": "reasoning",
"id": "rs_null_1",
"summary": [{"type": "summary_text", "text": None}],
}
]
)
result: Any = _ADAPTER.translate_response(response)
assert result["content"] == []
def test_reasoning_item_id_never_becomes_a_thinking_signature(self):
"""Only Anthropic can sign a thinking block, so a stand-in signature is never invented."""
reasoning = _make_reasoning_item(["Part one.", "Part two."], item_id="rs_abc123")