mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(ui): stop the interception panel from disabling a config-driven proxy
Self-review found four ways the new settings page could take web search interception down instead of configuring it. A proxy that activates interception through litellm_settings.callbacks stores no enabled flag, so the page reported it as off while it was serving, and saving anything on that page persisted that answer and the next poll removed the running callback. Reads now resolve the flag from the callbacks list, and a stored block without an explicit flag no longer touches the callback list at all. An empty provider list is the page's own default, but the handler reads it as "match no provider" rather than falling back to Bedrock, so enabling the feature without naming a provider switched it on and intercepted nothing. The empty list is now dropped so the handler default applies. The replacement logger is also built before the old one is removed, so a loop ceiling the handler refuses no longer leaves the proxy with none and retrying every poll, and a stored "false" string now reads as off rather than as a truthy string.
This commit is contained in:
parent
e12cbb4e13
commit
014f5cbf68
4 changed files with 157 additions and 9 deletions
|
|
@ -4787,6 +4787,20 @@ def _swap_in_model_cost_map(new_model_cost_map: dict) -> int:
|
|||
return adopt_model_cost_map(new_model_cost_map)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
params: Final = {key: value for key, value in stored.items() if key != "enabled"}
|
||||
if not params.get("enabled_providers"):
|
||||
params.pop("enabled_providers", None)
|
||||
return params
|
||||
|
||||
|
||||
def should_load_db_object(object_type: str | SupportedDBObjectType) -> bool:
|
||||
"""
|
||||
Check if an object type should be loaded from the database based on general_settings.supported_db_objects.
|
||||
|
|
@ -7803,23 +7817,26 @@ class ProxyConfig:
|
|||
|
||||
websearch_config: Final = litellm_settings.get("websearch_interception_params", None)
|
||||
|
||||
# Absent means nobody stored params, so a callbacks-list proxy keeps its callback.
|
||||
if websearch_config is None:
|
||||
if not isinstance(websearch_config, Mapping) or "enabled" not in websearch_config:
|
||||
return
|
||||
|
||||
enabled: Final = bool(websearch_config.get("enabled", True))
|
||||
enabled: Final = bool(coerce_bool(websearch_config["enabled"]))
|
||||
registered: Final = bool(
|
||||
litellm.logging_callback_manager.get_custom_loggers_for_type(WebSearchInterceptionLogger)
|
||||
)
|
||||
if self._last_websearch_interception_config == websearch_config and registered == enabled:
|
||||
return
|
||||
|
||||
replacement: Final = (
|
||||
WebSearchInterceptionLogger.from_config_yaml(_websearch_handler_params(websearch_config))
|
||||
if enabled
|
||||
else None
|
||||
)
|
||||
|
||||
litellm.logging_callback_manager.remove_callbacks_by_type(litellm.callbacks, WebSearchInterceptionLogger)
|
||||
|
||||
if enabled:
|
||||
litellm.logging_callback_manager.add_litellm_callback(
|
||||
WebSearchInterceptionLogger.from_config_yaml(websearch_config)
|
||||
)
|
||||
if replacement is not None:
|
||||
litellm.logging_callback_manager.add_litellm_callback(replacement)
|
||||
verbose_proxy_logger.info("Web search interception reinitialized from DB")
|
||||
else:
|
||||
verbose_proxy_logger.info("Web search interception disabled")
|
||||
|
|
|
|||
|
|
@ -506,6 +506,36 @@ class WebSearchInterceptionSettingsResponse(SettingsResponse):
|
|||
"""Response model for web search interception settings"""
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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,
|
||||
}
|
||||
return {
|
||||
**config,
|
||||
"litellm_settings": {**litellm_settings, "websearch_interception_params": resolved},
|
||||
}
|
||||
|
||||
|
||||
def _as_settings_section(value: object) -> Mapping[str, object]:
|
||||
return cast("Mapping[str, object]", value) if isinstance(value, Mapping) else MappingProxyType({})
|
||||
|
||||
|
||||
@router.get(
|
||||
"/get/allowed_ips",
|
||||
tags=["Budget & Spend Tracking"],
|
||||
|
|
@ -1461,7 +1491,7 @@ async def get_websearch_interception_settings(
|
|||
return await _get_settings_with_schema(
|
||||
settings_key="websearch_interception_params",
|
||||
settings_class=WebSearchInterceptionSettings,
|
||||
config=config,
|
||||
config=_with_websearch_enabled_resolved(config),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4558,12 +4558,25 @@ def test_init_websearch_interception_absent_key_leaves_callbacks_untouched(monke
|
|||
assert litellm.callbacks == [config_registered]
|
||||
|
||||
|
||||
def test_init_websearch_interception_enables_when_enabled_key_missing(monkeypatch):
|
||||
def test_init_websearch_interception_without_enabled_key_leaves_callbacks_untouched(monkeypatch):
|
||||
logger_cls = _websearch_logger_cls()
|
||||
config_registered = logger_cls(search_tool_name="from-config-yaml")
|
||||
|
||||
_run_websearch_init(
|
||||
monkeypatch,
|
||||
stored_params={"search_tool_name": "stored-tool"},
|
||||
starting_callbacks=[config_registered],
|
||||
)
|
||||
|
||||
assert litellm.callbacks == [config_registered]
|
||||
|
||||
|
||||
def test_init_websearch_interception_registers_when_explicitly_enabled(monkeypatch):
|
||||
logger_cls = _websearch_logger_cls()
|
||||
|
||||
_run_websearch_init(
|
||||
monkeypatch,
|
||||
stored_params={"enabled": True, "search_tool_name": "stored-tool"},
|
||||
starting_callbacks=[],
|
||||
)
|
||||
|
||||
|
|
@ -4572,6 +4585,46 @@ def test_init_websearch_interception_enables_when_enabled_key_missing(monkeypatc
|
|||
assert registered[0].search_tool_name == "stored-tool"
|
||||
|
||||
|
||||
def test_init_websearch_interception_treats_string_false_as_disabled(monkeypatch):
|
||||
logger_cls = _websearch_logger_cls()
|
||||
existing = logger_cls(search_tool_name="stored-tool")
|
||||
|
||||
_run_websearch_init(
|
||||
monkeypatch,
|
||||
stored_params={"enabled": "false", "search_tool_name": "stored-tool"},
|
||||
starting_callbacks=[existing],
|
||||
)
|
||||
|
||||
assert [cb for cb in litellm.callbacks if isinstance(cb, logger_cls)] == []
|
||||
|
||||
|
||||
def test_init_websearch_interception_empty_providers_falls_back_to_handler_default(monkeypatch):
|
||||
logger_cls = _websearch_logger_cls()
|
||||
|
||||
_run_websearch_init(
|
||||
monkeypatch,
|
||||
stored_params={"enabled": True, "enabled_providers": [], "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_keeps_working_callback_when_new_one_cannot_be_built(monkeypatch):
|
||||
logger_cls = _websearch_logger_cls()
|
||||
working = logger_cls(search_tool_name="stored-tool", max_agentic_loops=3)
|
||||
|
||||
_run_websearch_init(
|
||||
monkeypatch,
|
||||
stored_params={"enabled": True, "search_tool_name": "stored-tool", "max_agentic_loops": 0},
|
||||
starting_callbacks=[working],
|
||||
)
|
||||
|
||||
assert litellm.callbacks == [working]
|
||||
|
||||
|
||||
def test_init_websearch_interception_disabled_removes_the_callback(monkeypatch):
|
||||
logger_cls = _websearch_logger_cls()
|
||||
existing = logger_cls(search_tool_name="stored-tool")
|
||||
|
|
|
|||
|
|
@ -3184,6 +3184,54 @@ 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(
|
||||
self, mock_proxy_config, mock_auth, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
|
||||
mock_proxy_config["config"]["litellm_settings"]["callbacks"] = ["websearch_interception"]
|
||||
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {
|
||||
"enabled_providers": ["bedrock"],
|
||||
"search_tool_name": "my-perplexity-search",
|
||||
}
|
||||
|
||||
resp = client.get("/get/websearch_interception_settings")
|
||||
|
||||
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
|
||||
):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
|
||||
mock_proxy_config["config"]["litellm_settings"].pop("callbacks", None)
|
||||
mock_proxy_config["config"]["litellm_settings"]["websearch_interception_params"] = {
|
||||
"enabled_providers": ["bedrock"],
|
||||
}
|
||||
|
||||
resp = client.get("/get/websearch_interception_settings")
|
||||
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["values"]["enabled"] is False
|
||||
|
||||
def test_update_reapplies_settings_to_the_running_proxy(self, mock_proxy_config, monkeypatch):
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", object())
|
||||
reapply = AsyncMock()
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_config.init_websearch_interception_settings_in_db",
|
||||
reapply,
|
||||
)
|
||||
self._override_auth(LitellmUserRoles.PROXY_ADMIN)
|
||||
try:
|
||||
resp = client.patch("/update/websearch_interception_settings", json={"enabled": True})
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert resp.status_code == 200, resp.text
|
||||
reapply.assert_awaited_once()
|
||||
|
||||
def test_update_rejects_zero_max_agentic_loops(self, mock_proxy_config, monkeypatch):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
|
||||
self._override_auth(LitellmUserRoles.PROXY_ADMIN)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue