mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
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:
parent
79bffe1a06
commit
f130b2b3f1
2 changed files with 67 additions and 13 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue