Skip to content

Commit d71f9ab

Browse files
fix: do not mutate Tool schemas (and handle empty tools) in OpenAIResponsesChatGenerator (#12067)
Co-authored-by: Timur Rakhmatullin <174210871+TimurRakhmatullin86@users.noreply.github.com> Co-authored-by: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com>
1 parent a7a4806 commit d71f9ab

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

haystack/components/generators/chat/openai_responses.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def _client_kwargs(self) -> dict[str, Any]:
228228

229229
def _warm_up_tools(self) -> None:
230230
if not self._tools_warmed_up:
231-
is_openai_tool = isinstance(self.tools, list) and isinstance(self.tools[0], dict)
231+
is_openai_tool = isinstance(self.tools, list) and bool(self.tools) and isinstance(self.tools[0], dict)
232232
# We only warm up Haystack tools, not OpenAI/MCP tools
233233
# The type ignore is needed because mypy cannot infer the type correctly
234234
if not is_openai_tool:
@@ -539,7 +539,10 @@ def _prepare_api_call( # noqa: PLR0913
539539
function_spec = {**t.tool_spec}
540540
if not tools_strict:
541541
function_spec["strict"] = False
542-
function_spec["parameters"]["additionalProperties"] = False
542+
# Copy the parameters schema before editing it. tool_spec exposes
543+
# Tool.parameters by reference, so mutating it here would permanently alter
544+
# the user's Tool (and any other generator that shares the same Tool instance).
545+
function_spec["parameters"] = {**function_spec["parameters"], "additionalProperties": False}
543546
tool_definitions.append({"type": "function", **function_spec})
544547

545548
openai_tools = {"tools": tool_definitions}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
fixes:
3+
- |
4+
``OpenAIResponsesChatGenerator`` no longer mutates the ``parameters`` schema of the ``Tool``
5+
objects passed to it. Previously every run wrote ``additionalProperties: False`` into the
6+
user's live ``Tool.parameters``, silently altering the tool for any other generator that
7+
shared the same ``Tool`` instance and making serialization round trips unstable.
8+
- |
9+
``OpenAIResponsesChatGenerator`` no longer raises ``IndexError`` when it is warmed up with an
10+
empty ``tools`` list.

test/components/generators/chat/test_openai_responses.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,14 @@ def test_warm_up_with_no_tools_does_not_raise(self, monkeypatch):
420420
generator.warm_up()
421421
assert generator._tools_warmed_up
422422

423+
def test_warm_up_with_empty_tools_list_does_not_raise(self, monkeypatch):
424+
monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key")
425+
# An empty list is a valid ``tools`` value (e.g. when built programmatically); warming up
426+
# must not index ``tools[0]`` unguarded.
427+
generator = OpenAIResponsesChatGenerator(tools=[])
428+
generator.warm_up()
429+
assert generator._tools_warmed_up
430+
423431
def test_warm_up_with_openai_tools_does_not_raise(self, monkeypatch):
424432
monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key")
425433
generator = OpenAIResponsesChatGenerator(
@@ -493,6 +501,21 @@ def test_run_fail_with_duplicate_tool_names(self, monkeypatch, tools):
493501
component = OpenAIResponsesChatGenerator(tools=duplicate_tools)
494502
component.run(chat_messages)
495503

504+
def test_prepare_api_call_does_not_mutate_tool_parameters(self, monkeypatch):
505+
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
506+
parameters = {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
507+
tool = Tool(name="weather", description="get the weather", parameters=parameters, function=print)
508+
509+
generator = OpenAIResponsesChatGenerator(tools_strict=False)
510+
api_args = generator._prepare_api_call(messages=[ChatMessage.from_user("hi")], tools=[tool])
511+
512+
# The tool's own schema must not be modified in place, otherwise the same Tool passed to
513+
# another generator (or serialized) would carry an additionalProperties flag it never had.
514+
assert "additionalProperties" not in tool.parameters
515+
assert tool.parameters == {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
516+
# ...while the payload actually sent to the API still carries additionalProperties=False.
517+
assert api_args["tools"][0]["parameters"]["additionalProperties"] is False
518+
496519
def test_run_with_wrong_model(self):
497520
mock_client = MagicMock()
498521
mock_client.responses.create.side_effect = OpenAIError("Invalid model name")

0 commit comments

Comments
 (0)