fix(ui): report interception as the proxy is actually running it

A second review pass found two more ways a write through the generic
config endpoint, which validates nothing, could strand the feature.

Dropping the enabled flag from a settings block the proxy had already
applied stopped the poller from reconciling it ever again, so the
callback served the old search tool forever. The poller now yields to
litellm_settings.callbacks only while it has applied nothing itself;
once it owns the callback it keeps reconciling.

A provider list written as a bare string was iterated one character at a
time, so interception matched no real provider - the same failure the
empty list already had. Anything that is not a non-empty list is now
dropped so the handler default applies.

The page also derives its toggle from whether the callback is registered
rather than from a stored flag, because a block can be live with no flag
in it at all, and the toggle is what an admin saves back.
This commit is contained in:
Yuneng Jiang 2026-09-19 12:22:44 -07:00
parent 014f5cbf68
commit 0243d268bc
No known key found for this signature in database
4 changed files with 78 additions and 22 deletions

View file

@ -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)
)

View file

@ -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,

View file

@ -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")

View file

@ -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"],
}