From f130b2b3f1c750d76472b2cdb8a0b3af8983d1ed Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Mon, 31 Aug 2026 18:33:09 -0700 Subject: [PATCH] fix: dedupe dotted-path callbacks by instance module instead of marking them read-only A dotted-path callback loaded from config registers as an object, so it surfaces at runtime under its class name and never matched the configured string, producing a second row. Marking the config row read_only hid the duplicate but also hid delete, which does work for these rows. Match the live instance back to its configured entry by module and drop it from the runtime rows, so the callback stays a single editable row. --- litellm/proxy/proxy_server.py | 25 ++++----- .../proxy/proxy_server/test_routes_config.py | 55 +++++++++++++++++++ 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index b5ecf36c813..980c2ffd949 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -17029,7 +17029,15 @@ def _hidden_runtime_callback_names(configured_callback_names: frozenset[str]) -> CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE[configured_name] ) ) - return guardrail_names | configured_instance_names + configured_modules: Final = frozenset( + configured_name.rsplit(".", 1)[0] for configured_name in configured_callback_names if "." in configured_name + ) + dotted_instance_names: Final = frozenset( + _callback_display_name(instance) + for instance in litellm.logging_callback_manager.get_custom_loggers_for_type(CustomLogger) + if type(instance).__module__ in configured_modules + ) + return guardrail_names | configured_instance_names | dotted_instance_names def _is_runtime_logging_callback(callback_name: str, hidden_callback_names: frozenset[str]) -> bool: @@ -17113,22 +17121,13 @@ async def get_config( """ for _callback in _success_callbacks: - row: Final = process_callback(_callback, "success", environment_variables) - if isinstance(_callback, str) and "." in _callback: - row["read_only"] = True - _data_to_return.append(row) + _data_to_return.append(process_callback(_callback, "success", environment_variables)) for _callback in _failure_callbacks: - row: Final = process_callback(_callback, "failure", environment_variables) - if isinstance(_callback, str) and "." in _callback: - row["read_only"] = True - _data_to_return.append(row) + _data_to_return.append(process_callback(_callback, "failure", environment_variables)) for _callback in _success_and_failure_callbacks: - row: Final = process_callback(_callback, "success_and_failure", environment_variables) - if isinstance(_callback, str) and "." in _callback: - row["read_only"] = True - _data_to_return.append(row) + _data_to_return.append(process_callback(_callback, "success_and_failure", environment_variables)) configured_callback_names: Final = frozenset( _normalize_callback_alias(callback) diff --git a/tests/test_litellm/proxy/proxy_server/test_routes_config.py b/tests/test_litellm/proxy/proxy_server/test_routes_config.py index a9bce13beff..2de9afe4f03 100644 --- a/tests/test_litellm/proxy/proxy_server/test_routes_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_routes_config.py @@ -1069,6 +1069,61 @@ def test_get_config_callbacks_deduplicates_configured_and_runtime(client, auth_a assert {callback["name"] for callback in callbacks} == {"langfuse"} +def test_get_config_callbacks_deduplicates_dotted_path_callback(client, auth_as, mock_prisma, monkeypatch): + """A dotted-path callback stays a single editable row instead of duplicating under its class name.""" + from litellm.proxy import proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + _install_litellm_config(mock_prisma) + monkeypatch.setattr(ps, "prisma_client", mock_prisma) + monkeypatch.setattr(ps, "llm_router", None) + + import litellm + from litellm.integrations.custom_logger import CustomLogger + + class _DottedPathTestHandler(CustomLogger): + pass + + dotted_handler = _DottedPathTestHandler() + dotted_path = f"{__name__}.dotted_handler" + + fake_proxy_config = MagicMock() + fake_proxy_config.get_config = AsyncMock( + return_value={ + "litellm_settings": {"success_callback": [dotted_path]}, + "general_settings": {}, + "environment_variables": dict(_CALLBACK_ENV_FIXTURE), + } + ) + monkeypatch.setattr(ps, "proxy_config", fake_proxy_config) + + monkeypatch.setattr(litellm, "callbacks", [dotted_handler]) + monkeypatch.setattr(litellm, "success_callback", []) + monkeypatch.setattr(litellm, "failure_callback", []) + monkeypatch.setattr(litellm, "_async_success_callback", []) + monkeypatch.setattr(litellm, "_async_failure_callback", []) + monkeypatch.setattr( + litellm.logging_callback_manager, + "get_callbacks_by_type", + MagicMock( + return_value={ + "success": [], + "failure": [], + "success_and_failure": ["_DottedPathTestHandler"], + } + ), + ) + + with auth_as(LitellmUserRoles.PROXY_ADMIN): + response = client.get("/get/config/callbacks") + + assert response.status_code == 200 + callbacks = response.json()["callbacks"] + assert [(callback["name"], callback["type"], callback.get("read_only", False)) for callback in callbacks] == [ + (dotted_path, "success", False) + ] + + def test_get_config_callbacks_lists_dict_shaped_config_callbacks(client, auth_as, mock_prisma, monkeypatch): """Dict-shaped success_callback config values list their keys as editable rows.""" from litellm.proxy import proxy_server as ps