diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 3234f6d0a06..9579807d01c 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4792,11 +4792,13 @@ def _websearch_handler_params(stored: Mapping[str, object]) -> dict[str, object] Translate stored web search interception settings into handler kwargs. Drops ``enabled``, which gates the callback rather than configuring it, and - drops an empty ``enabled_providers`` so the handler applies its own default - instead of matching no provider at all. + drops an ``enabled_providers`` that is not a non-empty list so the handler + applies its own default. An empty list otherwise matches no provider at all, + and a bare string is iterated one character at a time. """ params: Final = {key: value for key, value in stored.items() if key != "enabled"} - if not params.get("enabled_providers"): + providers: Final = params.get("enabled_providers") + if not isinstance(providers, list) or not providers: params.pop("enabled_providers", None) return params @@ -7817,10 +7819,17 @@ class ProxyConfig: websearch_config: Final = litellm_settings.get("websearch_interception_params", None) - if not isinstance(websearch_config, Mapping) or "enabled" not in websearch_config: + if not isinstance(websearch_config, Mapping): return - enabled: Final = bool(coerce_bool(websearch_config["enabled"])) + if "enabled" not in websearch_config and self._last_websearch_interception_config is None: + verbose_proxy_logger.debug( + "Web search interception: stored settings carry no 'enabled' flag and none were applied " + "before, so litellm_settings.callbacks keeps ownership of the callback." + ) + return + + enabled: Final = bool(coerce_bool(websearch_config.get("enabled", True))) registered: Final = bool( litellm.logging_callback_manager.get_custom_loggers_for_type(WebSearchInterceptionLogger) ) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 0af13fc9304..36b90d37da8 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -508,23 +508,23 @@ class WebSearchInterceptionSettingsResponse(SettingsResponse): def _with_websearch_enabled_resolved(config: Mapping[str, object]) -> dict[str, object]: """ - Report interception as on when the config file activates it through litellm_settings.callbacks. + Report whether interception is actually running, rather than what a stored flag claims. - Such a proxy stores no ``enabled`` flag, and reporting the field's own - default would tell an admin the feature is off while it is serving, then - persist that answer the moment they saved anything on the page. + A proxy can activate it through litellm_settings.callbacks, which stores no + flag at all, and a write through the generic config endpoint can drop the + flag from a block that is still live. Either way the field's own default + would tell an admin the feature is off while it is serving, and saving the + page would then persist that answer. """ + from litellm.integrations.websearch_interception.handler import ( + WebSearchInterceptionLogger, + ) + litellm_settings: Final[Mapping[str, object]] = _as_settings_section(config.get("litellm_settings")) stored: Final[Mapping[str, object]] = _as_settings_section(litellm_settings.get("websearch_interception_params")) - if "enabled" in stored: - return dict(config) - - callbacks: Final = litellm_settings.get("callbacks") resolved: Final = { **stored, - "enabled": isinstance(callbacks, Sequence) - and not isinstance(callbacks, (str, bytes)) - and "websearch_interception" in callbacks, + "enabled": bool(litellm.logging_callback_manager.get_custom_loggers_for_type(WebSearchInterceptionLogger)), } return { **config, diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index 13bcfb3d872..5606ffda02d 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -4549,6 +4549,41 @@ def _run_websearch_init(monkeypatch, stored_params, starting_callbacks): return pc +def _poll_websearch_init(pc, monkeypatch, stored_params): + monkeypatch.setattr( + "litellm.proxy.proxy_server.get_config_param", + AsyncMock(return_value=SimpleNamespace(param_value={"websearch_interception_params": stored_params})), + ) + asyncio.run(pc.init_websearch_interception_settings_in_db(prisma_client=MagicMock())) + + +def test_init_websearch_interception_resyncs_after_a_write_drops_the_enabled_flag(monkeypatch): + logger_cls = _websearch_logger_cls() + pc = ProxyConfig() + monkeypatch.setattr(litellm, "callbacks", []) + + _poll_websearch_init(pc, monkeypatch, {"enabled": True, "search_tool_name": "old-tool"}) + _poll_websearch_init(pc, monkeypatch, {"search_tool_name": "new-tool"}) + + registered = [cb for cb in litellm.callbacks if isinstance(cb, logger_cls)] + assert len(registered) == 1 + assert registered[0].search_tool_name == "new-tool" + + +def test_init_websearch_interception_ignores_a_non_list_providers_value(monkeypatch): + logger_cls = _websearch_logger_cls() + + _run_websearch_init( + monkeypatch, + stored_params={"enabled": True, "enabled_providers": "bedrock", "search_tool_name": "stored-tool"}, + starting_callbacks=[], + ) + + registered = [cb for cb in litellm.callbacks if isinstance(cb, logger_cls)] + assert len(registered) == 1 + assert registered[0].enabled_providers == ["bedrock"] + + def test_init_websearch_interception_absent_key_leaves_callbacks_untouched(monkeypatch): logger_cls = _websearch_logger_cls() config_registered = logger_cls(search_tool_name="from-config-yaml") diff --git a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py index 448f6bd3405..3288966e1e3 100644 --- a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py +++ b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py @@ -3138,7 +3138,13 @@ class TestWebSearchInterceptionSettingsEndpoints: ) def test_get_returns_stored_values_and_field_schema(self, mock_proxy_config, mock_auth, monkeypatch): + import litellm + from litellm.integrations.websearch_interception.handler import ( + WebSearchInterceptionLogger, + ) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object()) + monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(search_tool_name="running")]) mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = { "enabled": True, "enabled_providers": ["bedrock", "vertex_ai"], @@ -3184,11 +3190,16 @@ class TestWebSearchInterceptionSettingsEndpoints: assert mock_proxy_config["save_call_count"]() == 1 assert mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] == payload - def test_get_reports_enabled_when_the_config_file_activates_the_callback( + def test_get_reports_enabled_while_the_callback_is_running_without_a_stored_flag( self, mock_proxy_config, mock_auth, monkeypatch ): + import litellm + from litellm.integrations.websearch_interception.handler import ( + WebSearchInterceptionLogger, + ) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object()) - mock_proxy_config["config"]["litellm_settings"]["callbacks"] = ["websearch_interception"] + monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(search_tool_name="from-config")]) mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = { "enabled_providers": ["bedrock"], "search_tool_name": "my-perplexity-search", @@ -3199,12 +3210,13 @@ class TestWebSearchInterceptionSettingsEndpoints: assert resp.status_code == 200, resp.text assert resp.json()["values"]["enabled"] is True - def test_get_reports_disabled_when_nothing_activates_the_callback( - self, mock_proxy_config, mock_auth, monkeypatch - ): + def test_get_reports_disabled_when_the_callback_is_not_running(self, mock_proxy_config, mock_auth, monkeypatch): + import litellm + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object()) - mock_proxy_config["config"]["litellm_settings"].pop("callbacks", None) + monkeypatch.setattr(litellm, "callbacks", []) mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = { + "enabled": True, "enabled_providers": ["bedrock"], }