From dcdca603e7e8673bad1ae5b3c25dfc4702d424f9 Mon Sep 17 00:00:00 2001 From: Taranum01 <50813317+Taranum01@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:53:38 +0530 Subject: [PATCH] fix(plugin): fall back to generic listing when category route 404s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #40683 `PluginInstaller.list_plugins_by_category` calls `GET /plugin/{tenant_id}/management/{category}/list` on the plugin daemon. Local plugin daemons (e.g. 0.6.x) don't expose the category-segmented route and return 404. The current code raises this 404, which then breaks the model-provider integration page even though the generic listing route is available. On 404, fall back to `GET /plugin/{tenant_id}/management/list` and filter the result locally by `declaration.category`. The generic route is the same one used by the unrestricted `list_plugins_with_total`. Apply the caller's page/page_size on the filtered result (cap page_size at 1000 for the fallback fetch — adequate for any reasonable category). Added a regression test in `test_plugin_manager.py` that asserts the 404-fallback path produces the correct filtered list with the right `has_more` flag. --- api/core/plugin/impl/plugin.py | 47 ++++++++++++++----- .../core/plugin/test_plugin_manager.py | 34 ++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/api/core/plugin/impl/plugin.py b/api/core/plugin/impl/plugin.py index 8ac33b50297..108fb5aeb8e 100644 --- a/api/core/plugin/impl/plugin.py +++ b/api/core/plugin/impl/plugin.py @@ -98,19 +98,40 @@ class PluginInstaller(BasePluginClient): tags: Sequence[str] = (), language: str = "en_US", ) -> PluginListWithoutTotalResponse: - return self._request_with_plugin_daemon_response( - "GET", - f"plugin/{tenant_id}/management/{category.value}/list", - PluginListWithoutTotalResponse, - params={ - "page": page, - "page_size": page_size, - "response_type": "paged", - "query": query, - "tags": list(tags), - "language": language, - }, - ) + try: + return self._request_with_plugin_daemon_response( + "GET", + f"plugin/{tenant_id}/management/{category.value}/list", + PluginListWithoutTotalResponse, + params={ + "page": page, + "page_size": page_size, + "response_type": "paged", + "query": query, + "tags": list(tags), + "language": language, + }, + ) + except HTTPError as e: + # Local plugin daemons (e.g. 0.6.x) don't expose the + # category-segmented route. Fall back to the generic listing and + # filter by declaration.category on the API side. + message = e.args[0] if e.args else "" + if "404" not in message: + raise + all_plugins = self.list_plugins_with_total( + tenant_id, + page=1, + page_size=1000, + ) + filtered = [p for p in all_plugins.list if p.declaration.category == category] + start = (page - 1) * page_size + end = start + page_size + return PluginListWithoutTotalResponse( + list=filtered[start:end], + has_more=end < len(filtered), + ) + def upload_pkg( self, diff --git a/api/tests/unit_tests/core/plugin/test_plugin_manager.py b/api/tests/unit_tests/core/plugin/test_plugin_manager.py index 290c7301bbe..f8b63e1d166 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_manager.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_manager.py @@ -154,6 +154,40 @@ class TestPluginDiscovery: assert result.list == [mock_plugin_entity] assert result.has_more is True + def test_list_plugins_by_category_falls_back_on_404(self, plugin_installer, mock_plugin_entity): + """When the category-segmented route 404s (local plugin daemon < 0.7), + list_plugins_by_category should fall back to the generic listing and + filter by declaration.category on the API side. Regression for #40683. + """ + from requests import HTTPError + + # 404 from the category route, then a generic listing with three plugins + # — two Tool, one Model — across two pages. + p_tool_1 = mock_plugin_entity + p_tool_2 = mock_plugin_entity + p_model = mock_plugin_entity + p_model.declaration.category = PluginCategory.Model + + with patch.object( + plugin_installer, + "_request_with_plugin_daemon_response", + side_effect=[ + HTTPError("404"), + PluginListResponse( + list=[p_tool_1, p_model, p_tool_2], + total=3, + ), + ], + ): + result = plugin_installer.list_plugins_by_category( + "test-tenant", category=PluginCategory.Tool, page=1, page_size=10 + ) + + # Filters out the Model entry, returns both Tool entries. + assert result.list == [p_tool_1, p_tool_2] + assert result.has_more is False + assert result.has_more == (10 < 2) # page_size 10 < 2 items + def test_list_plugins_empty_result(self, plugin_installer): """Test plugin listing when no plugins are installed.""" # Arrange: Mock empty response