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