Skip to content

Commit a17ac98

Browse files
tstadelanakin87claude
authored
feat: flattened arg support for reasoning mode in GPT-5.6 (#11964)
Co-authored-by: anakin87 <stefanofiorucci@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 630c44b commit a17ac98

3 files changed

Lines changed: 113 additions & 9 deletions

File tree

haystack/components/generators/chat/openai_responses.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,21 @@ def __init__(
160160
- `summary`: The summary of the reasoning.
161161
- `effort`: The level of effort to put into the reasoning. Can be `low`, `medium` or `high`.
162162
- `generate_summary`: Whether to generate a summary of the reasoning.
163+
- `mode`: The reasoning mode. Can be `standard`, or `pro`. Supported since GPT-5.6.
163164
Note: OpenAI does not return the reasoning tokens, but we can view summary if its enabled.
164165
For details, see the [OpenAI Reasoning documentation](https://platform.openai.com/docs/guides/reasoning).
166+
- `include`: Specify additional output data to include in the model response. Supported values are:
167+
- web_search_call.action.sources: Include the sources of the web search tool call.
168+
- code_interpreter_call.outputs: Includes the outputs of python code execution in code interpreter tool
169+
call items.
170+
- computer_call_output.output.image_url: Include image urls from the computer call output.
171+
- file_search_call.results: Include the search results of the file search tool call.
172+
- message.input_image.image_url: Include image urls from the input message.
173+
- message.output_text.logprobs: Include logprobs with assistant messages.
174+
- reasoning.encrypted_content: Includes an encrypted version of reasoning tokens in reasoning item
175+
outputs. This enables reasoning items to be used in multi-turn conversations when using the
176+
Responses API statelessly (like when the store parameter is set to false, or when an organization
177+
is enrolled in the zero data retention program).
165178
:param timeout:
166179
Timeout for OpenAI client calls. If not set, it defaults to either the
167180
`OPENAI_TIMEOUT` environment variable, or 30 seconds.
@@ -545,20 +558,32 @@ def _prepare_api_call( # noqa: PLR0913
545558
def _resolve_flattened_kwargs(self, generation_kwargs: dict[str, Any]) -> dict[str, Any]:
546559
generation_kwargs = generation_kwargs.copy()
547560

561+
# avoid mutating the caller's dict
562+
reasoning_overrides = {}
548563
reasoning_effort = generation_kwargs.pop("reasoning_effort", None)
549564
if reasoning_effort is not None:
550-
reasoning = generation_kwargs.setdefault("reasoning", {})
551-
reasoning["effort"] = reasoning_effort
565+
reasoning_overrides["effort"] = reasoning_effort
552566

553567
reasoning_summary = generation_kwargs.pop("reasoning_summary", None)
554568
if reasoning_summary is not None:
555-
reasoning = generation_kwargs.setdefault("reasoning", {})
556-
reasoning["summary"] = reasoning_summary
569+
reasoning_overrides["summary"] = reasoning_summary
570+
571+
reasoning_mode = generation_kwargs.pop("reasoning_mode", None)
572+
if reasoning_mode is not None:
573+
reasoning_overrides["mode"] = reasoning_mode
574+
575+
if reasoning_overrides:
576+
generation_kwargs["reasoning"] = {**generation_kwargs.get("reasoning", {}), **reasoning_overrides}
577+
578+
include_reasoning_encrypted_content = generation_kwargs.pop("include_reasoning_encrypted_content", None)
579+
if include_reasoning_encrypted_content is True:
580+
include = generation_kwargs.get("include", [])
581+
if "reasoning.encrypted_content" not in include:
582+
generation_kwargs["include"] = [*include, "reasoning.encrypted_content"]
557583

558584
verbosity = generation_kwargs.pop("verbosity", None)
559585
if verbosity is not None:
560-
text = generation_kwargs.setdefault("text", {})
561-
text["verbosity"] = verbosity
586+
generation_kwargs["text"] = {**generation_kwargs.get("text", {}), "verbosity": verbosity}
562587

563588
return generation_kwargs
564589

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
features:
3+
- |
4+
Added a ``reasoning_mode`` flattened generation kwarg to ``OpenAIResponsesChatGenerator``, which is merged into the
5+
``reasoning`` dictionary sent to the OpenAI Responses API. This lets you set the reasoning mode (for example
6+
``standard`` or ``pro``, supported since GPT-5.6) without having to construct the nested ``reasoning`` dict yourself.
7+
- |
8+
Added an ``include_reasoning_encrypted_content`` flattened generation kwarg to ``OpenAIResponsesChatGenerator``.
9+
Setting it to ``True`` appends ``reasoning.encrypted_content`` to the ``include`` list sent to the OpenAI
10+
Responses API, so reasoning items can be reused across multi-turn conversations when the Responses API is used
11+
statelessly (for example when ``store`` is ``False`` or zero data retention is enabled).

test/components/generators/chat/test_openai_responses.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,14 +535,82 @@ def test_run_with_flattened_generation_kwargs(self, openai_mock_responses, monke
535535
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
536536
chat_messages = [ChatMessage.from_user("What's the capital of France")]
537537
component = OpenAIResponsesChatGenerator(
538-
model="gpt-4",
539-
generation_kwargs={"reasoning_effort": "low", "reasoning_summary": "auto", "verbosity": "low"},
538+
model="gpt-5.6-luna",
539+
generation_kwargs={
540+
"reasoning_effort": "low",
541+
"reasoning_summary": "auto",
542+
"reasoning_mode": "pro",
543+
"verbosity": "low",
544+
},
540545
)
541546
results = component.run(chat_messages)
542547
assert len(results["replies"]) == 1
543-
assert openai_mock_responses.call_args.kwargs["reasoning"] == {"effort": "low", "summary": "auto"}
548+
assert openai_mock_responses.call_args.kwargs["reasoning"] == {
549+
"effort": "low",
550+
"summary": "auto",
551+
"mode": "pro",
552+
}
544553
assert openai_mock_responses.call_args.kwargs["text"] == {"verbosity": "low"}
545554

555+
def test_run_with_reasoning_mode_only(self, openai_mock_responses, monkeypatch):
556+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
557+
chat_messages = [ChatMessage.from_user("What's the capital of France")]
558+
component = OpenAIResponsesChatGenerator(model="gpt-5.6-luna", generation_kwargs={"reasoning_mode": "standard"})
559+
results = component.run(chat_messages)
560+
assert len(results["replies"]) == 1
561+
assert openai_mock_responses.call_args.kwargs["reasoning"] == {"mode": "standard"}
562+
563+
def test_run_with_reasoning_mode_merges_with_existing_reasoning_dict(self, openai_mock_responses, monkeypatch):
564+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
565+
chat_messages = [ChatMessage.from_user("What's the capital of France")]
566+
component = OpenAIResponsesChatGenerator(
567+
model="gpt-5.6-luna", generation_kwargs={"reasoning": {"effort": "high"}, "reasoning_mode": "pro"}
568+
)
569+
results = component.run(chat_messages)
570+
assert len(results["replies"]) == 1
571+
assert openai_mock_responses.call_args.kwargs["reasoning"] == {"effort": "high", "mode": "pro"}
572+
573+
def test_run_with_include_reasoning_encrypted_content(self, openai_mock_responses, monkeypatch):
574+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
575+
chat_messages = [ChatMessage.from_user("What's the capital of France")]
576+
component = OpenAIResponsesChatGenerator(
577+
model="gpt-5.6-luna", generation_kwargs={"include_reasoning_encrypted_content": True}
578+
)
579+
results = component.run(chat_messages)
580+
assert len(results["replies"]) == 1
581+
assert openai_mock_responses.call_args.kwargs["include"] == ["reasoning.encrypted_content"]
582+
583+
def test_run_with_include_reasoning_encrypted_content_merges_with_existing_include_list(
584+
self, openai_mock_responses, monkeypatch
585+
):
586+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
587+
chat_messages = [ChatMessage.from_user("What's the capital of France")]
588+
component = OpenAIResponsesChatGenerator(
589+
model="gpt-5.6-luna",
590+
generation_kwargs={
591+
"include": ["message.output_text.logprobs"],
592+
"include_reasoning_encrypted_content": True,
593+
},
594+
)
595+
results = component.run(chat_messages)
596+
assert len(results["replies"]) == 1
597+
assert openai_mock_responses.call_args.kwargs["include"] == [
598+
"message.output_text.logprobs",
599+
"reasoning.encrypted_content",
600+
]
601+
602+
def test_run_with_include_reasoning_encrypted_content_false_does_not_set_include(
603+
self, openai_mock_responses, monkeypatch
604+
):
605+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
606+
chat_messages = [ChatMessage.from_user("What's the capital of France")]
607+
component = OpenAIResponsesChatGenerator(
608+
model="gpt-5.6-luna", generation_kwargs={"include_reasoning_encrypted_content": False}
609+
)
610+
results = component.run(chat_messages)
611+
assert len(results["replies"]) == 1
612+
assert "include" not in openai_mock_responses.call_args.kwargs
613+
546614
def test_run_with_params_streaming(self, openai_mock_responses_stream_text_delta):
547615
streaming_callback_called = False
548616

0 commit comments

Comments
 (0)