mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
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:
parent
014f5cbf68
commit
0243d268bc
4 changed files with 78 additions and 22 deletions
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue