fix(proxy): reconcile interception callbacks when config clears the list

The DB overlay merge treats an empty list as "no value" so it never
overwrites the file config. Clearing the last entry from
litellm_settings.callbacks therefore reaches _add_callbacks_from_db_config
as an absent key, not as [], and reconciliation was skipped: the
interception logger kept running (and kept paying for searches) after the
capability was revoked.

Reconciliation now also runs when the merged config lists no callbacks at
all. To keep a transient config-load failure from being read as "nothing is
configured", _update_llm_router only feeds the callback path once
get_config() has actually returned, so an empty dict from the failure branch
can no longer uninstall a working callback.
This commit is contained in:
mateo-berri 2026-08-02 03:25:37 +00:00
parent 037660b16b
commit 0e918025c6
No known key found for this signature in database
2 changed files with 39 additions and 7 deletions

View file

@ -5453,9 +5453,11 @@ class ProxyConfig:
# Load config separately so a timeout here doesn't block model loading
config_data: dict = {}
config_loaded = False
search_tools = None
try:
config_data = await proxy_config.get_config()
config_loaded = True
search_tools = self.parse_search_tools(config_data)
except Exception as e:
verbose_proxy_logger.warning(
@ -5509,7 +5511,8 @@ class ProxyConfig:
llm_model_list = llm_router.get_model_list()
# check if user set any callbacks in Config Table
self._add_callbacks_from_db_config(config_data)
if config_loaded:
self._add_callbacks_from_db_config(config_data)
# router settings
await self._add_router_settings_from_db_config(
@ -5576,8 +5579,9 @@ class ProxyConfig:
existing_callbacks=litellm.failure_callback,
)
if callbacks is not None and isinstance(callbacks, list):
for callback in callbacks:
if callbacks is None or isinstance(callbacks, list):
configured_callbacks = callbacks or ()
for callback in configured_callbacks:
if isinstance(callback, str) and install_config_parameterized_callback(
callback=callback,
litellm_settings=litellm_settings,
@ -5589,7 +5593,7 @@ class ProxyConfig:
event_types=["success", "failure"],
existing_callbacks=litellm.callbacks,
)
uninstall_deconfigured_parameterized_callbacks(callbacks)
uninstall_deconfigured_parameterized_callbacks(configured_callbacks)
def _encrypt_env_variables(self, environment_variables: dict, new_encryption_key: str | None = None) -> dict:
"""

View file

@ -2025,10 +2025,11 @@ def test_ProxyConfig__add_callbacks_from_db_config_uninstalls_logger_when_callba
[
{},
{"litellm_settings": {}},
{"litellm_settings": {"callbacks": []}},
{"litellm_settings": {"success_callback": ["langfuse"]}},
],
)
def test_ProxyConfig__add_callbacks_from_db_config_keeps_logger_when_config_omits_callbacks(
def test_ProxyConfig__add_callbacks_from_db_config_uninstalls_logger_when_config_omits_callbacks(
monkeypatch, config_without_callbacks
):
from litellm.integrations.websearch_interception.handler import (
@ -2039,10 +2040,37 @@ def test_ProxyConfig__add_callbacks_from_db_config_keeps_logger_when_config_omit
pc = ProxyConfig()
pc._add_callbacks_from_db_config(_websearch_db_config("tavily-search", ["bedrock"]))
installed_before = [cb for cb in litellm.callbacks if isinstance(cb, WebSearchInterceptionLogger)]
pc._add_callbacks_from_db_config(config_without_callbacks)
installed = [cb for cb in litellm.callbacks if isinstance(cb, WebSearchInterceptionLogger)]
assert len(installed) == 1
remaining = [cb for cb in litellm.callbacks if isinstance(cb, WebSearchInterceptionLogger)]
snapshot = {"installed_before": len(installed_before), "remaining": len(remaining)}
assert snapshot == {"installed_before": 1, "remaining": 0}
@pytest.mark.asyncio
async def test_ProxyConfig__update_llm_router_keeps_logger_when_config_load_fails(monkeypatch):
from litellm.integrations.websearch_interception.handler import (
WebSearchInterceptionLogger,
)
_reset_callback_lists(monkeypatch)
pc = ProxyConfig()
pc._add_callbacks_from_db_config(_websearch_db_config("tavily-search", ["bedrock"]))
installed_before = [cb for cb in litellm.callbacks if isinstance(cb, WebSearchInterceptionLogger)]
class _FailingProxyConfig:
async def get_config(self) -> dict:
raise TimeoutError("transient DB timeout")
monkeypatch.setattr(litellm.proxy.proxy_server, "proxy_config", _FailingProxyConfig())
await pc._update_llm_router(new_models=[], proxy_logging_obj=MagicMock())
remaining = [cb for cb in litellm.callbacks if isinstance(cb, WebSearchInterceptionLogger)]
snapshot = {"installed_before": len(installed_before), "remaining": len(remaining)}
assert snapshot == {"installed_before": 1, "remaining": 1}
def test_ProxyConfig__add_callbacks_from_db_config_bad_config_raises():