From da8fcba969fa0f7e0985ba35e7ab30b9eb887363 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 10 Mar 2026 08:27:05 +0100 Subject: [PATCH] Fix invalid function schema error for tools with None parameters Normalize tool function parameters before forwarding to OpenAI API. When parameters is None or missing, default to a valid empty object schema {"type": "object", "properties": {}}. This prevents the BadRequestError reported in #22016. Fixes https://github.com/galaxyproject/galaxy/issues/22016 --- lib/galaxy/webapps/galaxy/api/plugins.py | 3 ++ test/integration/test_plugins.py | 59 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/lib/galaxy/webapps/galaxy/api/plugins.py b/lib/galaxy/webapps/galaxy/api/plugins.py index 4e7e84f8f0b..34e1af5aeb0 100644 --- a/lib/galaxy/webapps/galaxy/api/plugins.py +++ b/lib/galaxy/webapps/galaxy/api/plugins.py @@ -238,6 +238,9 @@ class FastAPIPlugins: if len(original_tools) <= MAX_TOOLS: for tool in original_tools: tool_dict = tool.model_dump() + func = tool_dict.get("function", {}) + if func.get("parameters") is None: + func["parameters"] = {"type": "object", "properties": {}} size = len(json.dumps(tool_dict, separators=(",", ":")).encode("utf-8")) if size > MAX_TOOL_BYTES: return self._create_error("Tool schema too large.") diff --git a/test/integration/test_plugins.py b/test/integration/test_plugins.py index d620f3bbc8e..1100a8f4b65 100644 --- a/test/integration/test_plugins.py +++ b/test/integration/test_plugins.py @@ -216,6 +216,65 @@ class TestVisualizationPluginsApi(IntegrationTestCase): forwarded_tools = call_kwargs["tools"] assert forwarded_tools[0]["function"]["description"] == "Select a processing step" + @patch("galaxy.webapps.galaxy.api.plugins.AsyncOpenAI") + def test_tool_with_none_parameters_normalized(self, mock_client): + mock_response = MagicMock() + mock_response.model_dump.return_value = {"id": "test", "choices": []} + mock_instance = MagicMock() + mock_instance.chat.completions.create = AsyncMock(return_value=mock_response) + mock_client.return_value = mock_instance + payload = _create_chat_payload( + { + "tools": [ + { + "type": "function", + "function": { + "name": "choose_process", + "description": "Select a processing step", + "parameters": None, + }, + } + ] + } + ) + response = self._post_payload(payload) + self._assert_status_code_is(response, 200) + call_kwargs = mock_instance.chat.completions.create.call_args.kwargs + forwarded_tools = call_kwargs["tools"] + assert forwarded_tools[0]["function"]["parameters"] == { + "type": "object", + "properties": {}, + } + + @patch("galaxy.webapps.galaxy.api.plugins.AsyncOpenAI") + def test_tool_with_missing_parameters_normalized(self, mock_client): + mock_response = MagicMock() + mock_response.model_dump.return_value = {"id": "test", "choices": []} + mock_instance = MagicMock() + mock_instance.chat.completions.create = AsyncMock(return_value=mock_response) + mock_client.return_value = mock_instance + payload = _create_chat_payload( + { + "tools": [ + { + "type": "function", + "function": { + "name": "choose_process", + "description": "Select a processing step", + }, + } + ] + } + ) + response = self._post_payload(payload) + self._assert_status_code_is(response, 200) + call_kwargs = mock_instance.chat.completions.create.call_args.kwargs + forwarded_tools = call_kwargs["tools"] + assert forwarded_tools[0]["function"]["parameters"] == { + "type": "object", + "properties": {}, + } + @patch("galaxy.webapps.galaxy.api.plugins.AsyncOpenAI") def test_provider_error_body_forwarded(self, mock_client): class MockOpenAIError(APIError):