You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
OpenAIResponsesChatGenerator keeps only the last reasoning item when a response contains several #13361
A Responses API output can contain more than one reasoning item, for example when the model reasons before each of several function calls. _convert_response_to_chat_message in haystack/components/generators/chat/openai_responses.py assigns the reasoning variable on every reasoning item, so only the last one reaches the ChatMessage. The earlier items, including their id and any encrypted_content, are dropped. ChatMessage.from_assistant accepts a single reasoning, although ChatMessage.reasonings is already a list.
Expected: both rs_1 and rs_2 are available on the message.
Why it matters
When the message is sent back (_convert_chat_message_to_responses_api_format already iterates over message.reasonings), the Responses API expects each function call to be accompanied by its reasoning item, and with include=["reasoning.encrypted_content"] the encrypted content of rs_1 is lost.
The streaming path (_convert_streaming_chunks_to_chat_message) has the same shape: all reasoning chunks are folded into one ReasoningContent (last id, concatenated text, merged extra), so the items cannot be told apart.
Proposed direction
Two options, happy to follow whichever fits the design better:
Let ChatMessage.from_assistant accept a list of ReasoningContent and keep one per reasoning item in both the streaming and non-streaming paths.
Keep a single ReasoningContent and concatenate the text, accepting that per-item ids are lost.
Which would the maintainers prefer? A PR can follow once the direction is clear.
@pcbeingused333, thanks for the repro. I'm working on preserving one ReasoningContent per Responses API reasoning item, including its ID and encrypted content, in both the normal and streaming paths.
The approach is to let the existing ChatMessage.from_assistant(reasoning=...) parameter also accept a list, keeping the current string and single-item forms compatible. Streaming chunks would be grouped by output item instead of merging metadata across items. I'll add regression tests for multiple reasoning items, encrypted-only items, and conversion back to API input.
@anakin87, please let me know if this is already being handled internally or if you prefer a different API shape.
Before discussing a solution, I'd like to see a reproducible end-to-end example using actual OpenAIResponsesChatGenerator executions, without mocks or manually constructed responses.
Thanks @anakin87, that makes sense. The unit cases confirm that conversion drops items, but they don't establish that a live provider returns that shape.
@pcbeingused333, could you share a minimal example using actual OpenAIResponsesChatGenerator.run() calls, along with the model/provider, relevant generation parameters, and SDK version? A redacted response showing the separate reasoning items would also help. Please leave out API keys and encrypted content.
I'll hold off on opening a PR until we can reproduce the behavior end to end.
@anakin87, I was able to reproduce this with actual OpenAIResponsesChatGenerator.run() calls against OpenAI, without mocks or constructed SDK responses.
Tested with gpt-5-mini, OpenAI Python SDK 3.35.1, and Haystack main at 95ee53a:
fromhaystack.components.generators.chatimportOpenAIResponsesChatGeneratorfromhaystack.dataclassesimportChatMessage# Set OPENAI_API_KEY in the environment. This makes paid API requests.stream=False# Repeat with True to check streaming.chunks= []
generator=OpenAIResponsesChatGenerator(
model="gpt-5-mini",
timeout=45,
max_retries=0,
tools=[
{"type": "web_search", "search_context_size": "low"},
{
"type": "function",
"name": "multiply",
"description": "Multiply two integers exactly.",
"parameters": {
"type": "object",
"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
"required": ["a", "b"],
"additionalProperties": False,
},
"strict": True,
},
],
generation_kwargs={
"max_output_tokens": 2048,
"max_tool_calls": 1,
"parallel_tool_calls": True,
"reasoning": {"effort": "low", "summary": "auto"},
"include": ["reasoning.encrypted_content"],
"store": False,
},
)
reply=generator.run(
[ChatMessage.from_user(
"Use web_search to find the year Python 3.0 was first released on python.org. ""Then use multiply to multiply that year by 7. Do the search before the multiplication tool call. ""Do not compute the product yourself."
)],
streaming_callback=chunks.appendifstreamelseNone,
)["replies"][0]
print(len(reply.reasonings), [r.extra.get("id") forrinreply.reasonings])
In both modes the actual API output was reasoning, web_search_call, reasoning, function_call, with distinct reasoning IDs. On main, the message contained only the second ID. The streaming path also combined the two summaries into that single item.
On the fix branch, both IDs survive in order and are included when the message is converted back to API input. I checked the encrypted payloads too: each non-streaming item and each streaming output_item.done payload is retained separately. Follow-up run() calls with the multiplication result completed successfully in both patched modes.
For clarity, the follow-up also completed on main in the non-streaming check. This reproduces loss of earlier reasoning items; it does not demonstrate an API rejection. The live example uses a built-in search followed by a function call, rather than the issue's constructed example with two function calls.
The branch also passes 388 focused unit tests, full type checking, applicable pre-commit hooks, and package builds. @pcbeingused333, does this match the scenario you had in mind?
Thanks @its-amann, yes, that is the scenario: a single Responses API output with more than one reasoning item (one before each function call), where the converted ChatMessage only kept the last one. Glad it reproduces with live calls, and thanks for picking up the fix.
Activity
its-amann commentedon Oct 21, 2026
@pcbeingused333, thanks for the repro. I'm working on preserving one
ReasoningContentper Responses API reasoning item, including its ID and encrypted content, in both the normal and streaming paths.The approach is to let the existing
ChatMessage.from_assistant(reasoning=...)parameter also accept a list, keeping the current string and single-item forms compatible. Streaming chunks would be grouped by output item instead of merging metadata across items. I'll add regression tests for multiple reasoning items, encrypted-only items, and conversion back to API input.@anakin87, please let me know if this is already being handled internally or if you prefer a different API shape.
anakin87 commentedon Oct 21, 2026
Hello!
Before discussing a solution, I'd like to see a reproducible end-to-end example using actual
OpenAIResponsesChatGeneratorexecutions, without mocks or manually constructed responses.its-amann commentedon Oct 21, 2026
Thanks @anakin87, that makes sense. The unit cases confirm that conversion drops items, but they don't establish that a live provider returns that shape.
@pcbeingused333, could you share a minimal example using actual
OpenAIResponsesChatGenerator.run()calls, along with the model/provider, relevant generation parameters, and SDK version? A redacted response showing the separate reasoning items would also help. Please leave out API keys and encrypted content.I'll hold off on opening a PR until we can reproduce the behavior end to end.
its-amann commentedon Oct 21, 2026
@anakin87, I was able to reproduce this with actual
OpenAIResponsesChatGenerator.run()calls against OpenAI, without mocks or constructed SDK responses.Tested with
gpt-5-mini, OpenAI Python SDK 3.35.1, and Haystack main at95ee53a:In both modes the actual API output was
reasoning, web_search_call, reasoning, function_call, with distinct reasoning IDs. On main, the message contained only the second ID. The streaming path also combined the two summaries into that single item.On the fix branch, both IDs survive in order and are included when the message is converted back to API input. I checked the encrypted payloads too: each non-streaming item and each streaming
output_item.donepayload is retained separately. Follow-uprun()calls with the multiplication result completed successfully in both patched modes.For clarity, the follow-up also completed on main in the non-streaming check. This reproduces loss of earlier reasoning items; it does not demonstrate an API rejection. The live example uses a built-in search followed by a function call, rather than the issue's constructed example with two function calls.
The branch also passes 388 focused unit tests, full type checking, applicable pre-commit hooks, and package builds. @pcbeingused333, does this match the scenario you had in mind?
pcbeingused333 commentedon Oct 21, 2026
Thanks @its-amann, yes, that is the scenario: a single Responses API output with more than one
reasoningitem (one before each function call), where the convertedChatMessageonly kept the last one. Glad it reproduces with live calls, and thanks for picking up the fix.