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
7 changes: 5 additions & 2 deletions haystack/components/generators/chat/openai_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def _client_kwargs(self) -> dict[str, Any]:

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

openai_tools = {"tools": tool_definitions}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
fixes:
- |
``OpenAIResponsesChatGenerator`` no longer mutates the ``parameters`` schema of the ``Tool``
objects passed to it. Previously every run wrote ``additionalProperties: False`` into the
user's live ``Tool.parameters``, silently altering the tool for any other generator that
shared the same ``Tool`` instance and making serialization round trips unstable.
- |
``OpenAIResponsesChatGenerator`` no longer raises ``IndexError`` when it is warmed up with an
empty ``tools`` list.
23 changes: 23 additions & 0 deletions test/components/generators/chat/test_openai_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,14 @@ def test_warm_up_with_no_tools_does_not_raise(self, monkeypatch):
generator.warm_up()
assert generator._tools_warmed_up

def test_warm_up_with_empty_tools_list_does_not_raise(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key")
# An empty list is a valid ``tools`` value (e.g. when built programmatically); warming up
# must not index ``tools[0]`` unguarded.
generator = OpenAIResponsesChatGenerator(tools=[])
generator.warm_up()
assert generator._tools_warmed_up

def test_warm_up_with_openai_tools_does_not_raise(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "fake-api-key")
generator = OpenAIResponsesChatGenerator(
Expand Down Expand Up @@ -493,6 +501,21 @@ def test_run_fail_with_duplicate_tool_names(self, monkeypatch, tools):
component = OpenAIResponsesChatGenerator(tools=duplicate_tools)
component.run(chat_messages)

def test_prepare_api_call_does_not_mutate_tool_parameters(self, monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-api-key")
parameters = {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
tool = Tool(name="weather", description="get the weather", parameters=parameters, function=print)

generator = OpenAIResponsesChatGenerator(tools_strict=False)
api_args = generator._prepare_api_call(messages=[ChatMessage.from_user("hi")], tools=[tool])

# The tool's own schema must not be modified in place, otherwise the same Tool passed to
# another generator (or serialized) would carry an additionalProperties flag it never had.
assert "additionalProperties" not in tool.parameters
assert tool.parameters == {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
# ...while the payload actually sent to the API still carries additionalProperties=False.
assert api_args["tools"][0]["parameters"]["additionalProperties"] is False

def test_run_with_wrong_model(self):
mock_client = MagicMock()
mock_client.responses.create.side_effect = OpenAIError("Invalid model name")
Expand Down
Loading