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.
This commit is contained in:
Yucheng Zhu 2026-08-31 18:33:09 -07:00
parent 79bffe1a06
commit f130b2b3f1
2 changed files with 67 additions and 13 deletions

View file

@ -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)

View file

@ -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