Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions haystack/components/generators/chat/openai_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,21 @@ def __init__(
- `summary`: The summary of the reasoning.
- `effort`: The level of effort to put into the reasoning. Can be `low`, `medium` or `high`.
- `generate_summary`: Whether to generate a summary of the reasoning.
- `mode`: The reasoning mode. Can be `standard`, or `pro`. Supported since GPT-5.6.
Note: OpenAI does not return the reasoning tokens, but we can view summary if its enabled.
For details, see the [OpenAI Reasoning documentation](https://platform.openai.com/docs/guides/reasoning).
- `include`: Specify additional output data to include in the model response. Supported values are:
- web_search_call.action.sources: Include the sources of the web search tool call.
- code_interpreter_call.outputs: Includes the outputs of python code execution in code interpreter tool
call items.
- computer_call_output.output.image_url: Include image urls from the computer call output.
- file_search_call.results: Include the search results of the file search tool call.
- message.input_image.image_url: Include image urls from the input message.
- message.output_text.logprobs: Include logprobs with assistant messages.
- reasoning.encrypted_content: Includes an encrypted version of reasoning tokens in reasoning item
outputs. This enables reasoning items to be used in multi-turn conversations when using the
Responses API statelessly (like when the store parameter is set to false, or when an organization
is enrolled in the zero data retention program).
:param timeout:
Timeout for OpenAI client calls. If not set, it defaults to either the
`OPENAI_TIMEOUT` environment variable, or 30 seconds.
Expand Down Expand Up @@ -545,20 +558,32 @@ def _prepare_api_call( # noqa: PLR0913
def _resolve_flattened_kwargs(self, generation_kwargs: dict[str, Any]) -> dict[str, Any]:
generation_kwargs = generation_kwargs.copy()

# avoid mutating the caller's dict
reasoning_overrides = {}
reasoning_effort = generation_kwargs.pop("reasoning_effort", None)
if reasoning_effort is not None:
reasoning = generation_kwargs.setdefault("reasoning", {})
reasoning["effort"] = reasoning_effort
reasoning_overrides["effort"] = reasoning_effort

reasoning_summary = generation_kwargs.pop("reasoning_summary", None)
if reasoning_summary is not None:
reasoning = generation_kwargs.setdefault("reasoning", {})
reasoning["summary"] = reasoning_summary
reasoning_overrides["summary"] = reasoning_summary

reasoning_mode = generation_kwargs.pop("reasoning_mode", None)
if reasoning_mode is not None:
reasoning_overrides["mode"] = reasoning_mode

if reasoning_overrides:
generation_kwargs["reasoning"] = {**generation_kwargs.get("reasoning", {}), **reasoning_overrides}

include_reasoning_encrypted_content = generation_kwargs.pop("include_reasoning_encrypted_content", None)
if include_reasoning_encrypted_content is True:
include = generation_kwargs.get("include", [])
if "reasoning.encrypted_content" not in include:
generation_kwargs["include"] = [*include, "reasoning.encrypted_content"]

verbosity = generation_kwargs.pop("verbosity", None)
if verbosity is not None:
text = generation_kwargs.setdefault("text", {})
text["verbosity"] = verbosity
generation_kwargs["text"] = {**generation_kwargs.get("text", {}), "verbosity": verbosity}

return generation_kwargs

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
features:
- |
Added a ``reasoning_mode`` flattened generation kwarg to ``OpenAIResponsesChatGenerator``, which is merged into the
``reasoning`` dictionary sent to the OpenAI Responses API. This lets you set the reasoning mode (for example
``standard`` or ``pro``, supported since GPT-5.6) without having to construct the nested ``reasoning`` dict yourself.
- |
Added an ``include_reasoning_encrypted_content`` flattened generation kwarg to ``OpenAIResponsesChatGenerator``.
Setting it to ``True`` appends ``reasoning.encrypted_content`` to the ``include`` list sent to the OpenAI
Responses API, so reasoning items can be reused across multi-turn conversations when the Responses API is used
statelessly (for example when ``store`` is ``False`` or zero data retention is enabled).
74 changes: 71 additions & 3 deletions test/components/generators/chat/test_openai_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,14 +535,82 @@ def test_run_with_flattened_generation_kwargs(self, openai_mock_responses, monke
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_messages = [ChatMessage.from_user("What's the capital of France")]
component = OpenAIResponsesChatGenerator(
model="gpt-4",
generation_kwargs={"reasoning_effort": "low", "reasoning_summary": "auto", "verbosity": "low"},
model="gpt-5.6-luna",
generation_kwargs={
"reasoning_effort": "low",
"reasoning_summary": "auto",
"reasoning_mode": "pro",
"verbosity": "low",
},
)
results = component.run(chat_messages)
assert len(results["replies"]) == 1
assert openai_mock_responses.call_args.kwargs["reasoning"] == {"effort": "low", "summary": "auto"}
assert openai_mock_responses.call_args.kwargs["reasoning"] == {
"effort": "low",
"summary": "auto",
"mode": "pro",
}
assert openai_mock_responses.call_args.kwargs["text"] == {"verbosity": "low"}

def test_run_with_reasoning_mode_only(self, openai_mock_responses, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_messages = [ChatMessage.from_user("What's the capital of France")]
component = OpenAIResponsesChatGenerator(model="gpt-5.6-luna", generation_kwargs={"reasoning_mode": "standard"})
results = component.run(chat_messages)
assert len(results["replies"]) == 1
assert openai_mock_responses.call_args.kwargs["reasoning"] == {"mode": "standard"}

def test_run_with_reasoning_mode_merges_with_existing_reasoning_dict(self, openai_mock_responses, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_messages = [ChatMessage.from_user("What's the capital of France")]
component = OpenAIResponsesChatGenerator(
model="gpt-5.6-luna", generation_kwargs={"reasoning": {"effort": "high"}, "reasoning_mode": "pro"}
)
results = component.run(chat_messages)
assert len(results["replies"]) == 1
assert openai_mock_responses.call_args.kwargs["reasoning"] == {"effort": "high", "mode": "pro"}

def test_run_with_include_reasoning_encrypted_content(self, openai_mock_responses, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_messages = [ChatMessage.from_user("What's the capital of France")]
component = OpenAIResponsesChatGenerator(
model="gpt-5.6-luna", generation_kwargs={"include_reasoning_encrypted_content": True}
)
results = component.run(chat_messages)
assert len(results["replies"]) == 1
assert openai_mock_responses.call_args.kwargs["include"] == ["reasoning.encrypted_content"]

def test_run_with_include_reasoning_encrypted_content_merges_with_existing_include_list(
self, openai_mock_responses, monkeypatch
):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_messages = [ChatMessage.from_user("What's the capital of France")]
component = OpenAIResponsesChatGenerator(
model="gpt-5.6-luna",
generation_kwargs={
"include": ["message.output_text.logprobs"],
"include_reasoning_encrypted_content": True,
},
)
results = component.run(chat_messages)
assert len(results["replies"]) == 1
assert openai_mock_responses.call_args.kwargs["include"] == [
"message.output_text.logprobs",
"reasoning.encrypted_content",
]

def test_run_with_include_reasoning_encrypted_content_false_does_not_set_include(
self, openai_mock_responses, monkeypatch
):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
chat_messages = [ChatMessage.from_user("What's the capital of France")]
component = OpenAIResponsesChatGenerator(
model="gpt-5.6-luna", generation_kwargs={"include_reasoning_encrypted_content": False}
)
results = component.run(chat_messages)
assert len(results["replies"]) == 1
assert "include" not in openai_mock_responses.call_args.kwargs

def test_run_with_params_streaming(self, openai_mock_responses_stream_text_delta):
streaming_callback_called = False

Expand Down
Loading