fix: match exact class in callback dedup so a custom subclass does not block a built-in logger (#34804)

This commit is contained in:
yucheng-berri 2026-07-27 12:18:42 -07:00 • committed by GitHub
parent 2b7e01bb7e
commit a7e665620b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 7 deletions

View file

@ -558,10 +558,10 @@ def _custom_logger_class_exists_in_success_callbacks(
e.g if `LangfusePromptManagement` is passed in, it will return True if an instance of `LangfusePromptManagement` exists in litellm.success_callback or litellm._async_success_callback
Prevents double adding a custom logger callback to the litellm callbacks
Matches on the exact class; an instance of a subclass does not count as registered
"""
return any(
isinstance(cb, type(callback_class)) for cb in litellm.success_callback + litellm._async_success_callback
)
return any(type(cb) is type(callback_class) for cb in litellm.success_callback + litellm._async_success_callback)
def _custom_logger_class_exists_in_failure_callbacks(
@ -573,10 +573,10 @@ def _custom_logger_class_exists_in_failure_callbacks(
e.g if `LangfusePromptManagement` is passed in, it will return True if an instance of `LangfusePromptManagement` exists in litellm.failure_callback or litellm._async_failure_callback
Prevents double adding a custom logger callback to the litellm callbacks
Matches on the exact class; an instance of a subclass does not count as registered
"""
return any(
isinstance(cb, type(callback_class)) for cb in litellm.failure_callback + litellm._async_failure_callback
)
return any(type(cb) is type(callback_class) for cb in litellm.failure_callback + litellm._async_failure_callback)
def get_request_guardrails(kwargs: Dict[str, Any]) -> List[str]:
@ -766,7 +766,7 @@ def function_setup(
llm_router=None, # type: ignore
)
if callback is None or any(
isinstance(cb, type(callback)) for cb in litellm._async_success_callback
type(cb) is type(callback) for cb in litellm._async_success_callback
): # don't double add a callback
continue
if callback not in litellm.input_callback:

View file

@ -4916,3 +4916,94 @@ def test_is_prompt_caching_valid_prompt_explicit_min_token_count_overrides_model
is_prompt_caching_valid_prompt(model="claude-opus-4-8", messages=PROMPT_CACHE_MESSAGES, min_token_count=8192)
is False
)
def test_custom_logger_guards_ignore_subclass_instances(monkeypatch: pytest.MonkeyPatch) -> None:
"""Regression LIT-4392: the success/failure existence guards used isinstance, so a user
subclass of a built-in logger already promoted into the callback lists made the guard
report the built-in itself as registered and the configured logger was silently skipped.
The exact-class assertions must hold alongside the subclass assertions: the guards still
have to dedup a second instance of the same class, only a subclass must stop matching."""
from litellm.integrations.custom_logger import CustomLogger
from litellm.utils import (
_custom_logger_class_exists_in_failure_callbacks,
_custom_logger_class_exists_in_success_callbacks,
)
class BuiltinLogger(CustomLogger):
pass
class UserSubclassLogger(BuiltinLogger):
pass
builtin_instance = BuiltinLogger()
monkeypatch.setattr(litellm, "success_callback", [UserSubclassLogger()])
monkeypatch.setattr(litellm, "failure_callback", [UserSubclassLogger()])
monkeypatch.setattr(litellm, "_async_success_callback", [])
monkeypatch.setattr(litellm, "_async_failure_callback", [])
assert _custom_logger_class_exists_in_success_callbacks(builtin_instance) is False
assert _custom_logger_class_exists_in_failure_callbacks(builtin_instance) is False
monkeypatch.setattr(litellm, "success_callback", [BuiltinLogger()])
monkeypatch.setattr(litellm, "failure_callback", [BuiltinLogger()])
assert _custom_logger_class_exists_in_success_callbacks(builtin_instance) is True
assert _custom_logger_class_exists_in_failure_callbacks(builtin_instance) is True
@pytest.mark.asyncio
async def test_s3_v2_success_callback_registers_alongside_user_subclass(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression LIT-4392: with a user S3Logger subclass registered via litellm_settings.callbacks
and success_callback ["s3_v2"], the built-in s3_v2 logger was never added and S3 logs were
silently dropped while requests kept returning 200."""
from litellm.integrations.s3_v2 import S3Logger
from litellm.utils import _add_custom_logger_callback_to_specific_event
class UserS3Logger(S3Logger):
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
pass
user_logger = UserS3Logger()
monkeypatch.setattr(litellm, "success_callback", [user_logger, "s3_v2"])
monkeypatch.setattr(litellm, "_async_success_callback", [user_logger])
monkeypatch.setattr(litellm, "failure_callback", [])
monkeypatch.setattr(litellm, "_async_failure_callback", [])
_add_custom_logger_callback_to_specific_event("s3_v2", "success")
assert any(type(cb) is S3Logger for cb in litellm.success_callback)
assert any(type(cb) is S3Logger for cb in litellm._async_success_callback)
assert "s3_v2" not in litellm.success_callback
assert user_logger in litellm.success_callback
@pytest.mark.asyncio
async def test_builtin_string_callback_registers_when_subclass_already_active(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression LIT-4392, litellm.callbacks path: the inline dedup in function_setup also
matched subclass instances, so a built-in name in litellm.callbacks was dropped whenever a
user subclass was already promoted into _async_success_callback."""
from litellm.integrations.s3_v2 import S3Logger
class UserS3Logger(S3Logger):
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
pass
user_logger = UserS3Logger()
monkeypatch.setattr(litellm, "callbacks", ["s3_v2"])
monkeypatch.setattr(litellm, "input_callback", [])
monkeypatch.setattr(litellm, "success_callback", [user_logger])
monkeypatch.setattr(litellm, "failure_callback", [])
monkeypatch.setattr(litellm, "_async_success_callback", [user_logger])
monkeypatch.setattr(litellm, "_async_failure_callback", [])
await litellm.acompletion(
model="gpt-5.6",
messages=[{"role": "user", "content": "hi"}],
mock_response="ok",
)
assert any(type(cb) is S3Logger for cb in litellm._async_success_callback)