Skip to content

Commit 1028849

Browse files
fix: OpenAIResponsesChatGenerator filter streaming event fields from reasoning extra before call to Responses API (#11720)
1 parent 258976a commit 1028849

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

haystack/components/generators/chat/openai_responses.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,13 @@ def _convert_response_to_chat_message(responses: Response | ParsedResponse) -> C
590590
extra = output.to_dict()
591591
# we dont need the summary in the extra
592592
extra.pop("summary")
593+
if output.content:
594+
logger.warning(
595+
"OpenAI returned a non-empty 'content' field on a reasoning item ({_id}). "
596+
"The content is preserved in ReasoningContent.extra['content'] but is NOT "
597+
"reflected in ReasoningContent.reasoning_text.",
598+
_id=output.id,
599+
)
593600
reasoning_text = "\n".join([summary.text for summary in summaries if summaries])
594601
reasoning = ReasoningContent(reasoning_text=reasoning_text, extra=extra)
595602

@@ -675,6 +682,15 @@ def _convert_response_chunk_to_streaming_chunk( # noqa: PLR0911
675682
# event falls through to the generic default and reasoning=None, so encrypted_content
676683
# is never available for multi-turn conversations.
677684
if chunk.item.type == "reasoning":
685+
if chunk.item.content:
686+
logger.warning(
687+
"OpenAI returned a non-empty 'content' field on a reasoning item ({_id}). "
688+
"This field is currently undocumented and was never observed in practice. "
689+
"The content is preserved in ReasoningContent.extra['content'] but is NOT "
690+
"reflected in ReasoningContent.reasoning_text. Please report this at "
691+
"https://github.com/deepset-ai/haystack/issues so we can update the mapping.",
692+
_id=chunk.item.id,
693+
)
678694
reasoning = ReasoningContent(reasoning_text="", extra=chunk.item.to_dict())
679695
return StreamingChunk(
680696
content="",
@@ -927,7 +943,15 @@ def convert_part(part: Any) -> dict[str, str | None]:
927943
if reasonings:
928944
formatted_reasonings = []
929945
for reasoning in reasonings:
930-
reasoning_item = {"summary": [], **(reasoning.extra)}
946+
# Streaming events (e.g. response.reasoning_summary_text.delta) store event-level
947+
# fields like item_id, output_index, summary_index, event_id, sequence_number into
948+
# reasoning.extra. Those are not valid reasoning input item fields and the API
949+
# rejects them with "Unknown parameter" when sent back in subsequent turns.
950+
# Valid fields per ResponseReasoningItem schema: id, type, summary (handled separately),
951+
# content, encrypted_content, status.
952+
_valid_reasoning_fields = {"id", "type", "encrypted_content", "status", "content"}
953+
filtered_extra = {k: v for k, v in reasoning.extra.items() if k in _valid_reasoning_fields}
954+
reasoning_item = {"summary": [], **filtered_extra}
931955
if reasoning.reasoning_text:
932956
reasoning_item["summary"] = [{"text": reasoning.reasoning_text, "type": "summary_text"}]
933957
formatted_reasonings.append(reasoning_item)

test/components/generators/chat/test_openai_responses_conversion.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,37 @@ def test_convert_assistant_message_w_tool_call(self):
13681368
{"content": "I need to use the functions.weather tool.", "role": "assistant"},
13691369
]
13701370

1371+
def test_convert_assistant_message_reasoning_strips_invalid_streaming_fields(self):
1372+
chat_message = ChatMessage(
1373+
_role=ChatRole.ASSISTANT,
1374+
_content=[
1375+
ReasoningContent(
1376+
reasoning_text="Let me think.",
1377+
extra={
1378+
"id": "rs_abc",
1379+
"type": "reasoning",
1380+
"encrypted_content": "enc123",
1381+
"status": "completed",
1382+
"item_id": "some_item",
1383+
"output_index": 0,
1384+
"summary_index": 1,
1385+
"event_id": "ev_xyz",
1386+
"sequence_number": 42,
1387+
},
1388+
)
1389+
],
1390+
)
1391+
result = _convert_chat_message_to_responses_api_format(chat_message)
1392+
assert result == [
1393+
{
1394+
"id": "rs_abc",
1395+
"type": "reasoning",
1396+
"encrypted_content": "enc123",
1397+
"status": "completed",
1398+
"summary": [{"text": "Let me think.", "type": "summary_text"}],
1399+
}
1400+
]
1401+
13711402
def test_convert_tool_message(self):
13721403
tool_call_result = ChatMessage(
13731404
_role=ChatRole.TOOL,

0 commit comments

Comments
 (0)