fix(logging): skip replaced global callbacks on the input path too

replace_global_callback already kept a same-key global logger out of
success and failure dispatch. pre_call and post_call still sent it the
input events, so a logger with an input hook could count a request start
it never saw finish. Both now share one filter with the success path
This commit is contained in:
mateo-berri 2026-10-20 18:27:56 -07:00
parent a92ffe05b4
commit f0d3436496
2 changed files with 45 additions and 12 deletions

View file

@ -1572,7 +1572,10 @@ class Logging(LiteLLMLoggingBaseClass):
"api_call_start_time"
]
# Input Integration Logging -> If you want to log the fact that an attempt to call the model was made
callbacks: Final = litellm.input_callback + (self.dynamic_input_callbacks or [])
callbacks: Final = (
*self._without_replaced_global_callbacks(litellm.input_callback),
*(self.dynamic_input_callbacks or ()),
)
for callback in callbacks:
try:
if callback == "supabase" and supabaseClient is not None:
@ -1745,7 +1748,10 @@ class Logging(LiteLLMLoggingBaseClass):
)
# Input Integration Logging -> If you want to log the fact that an attempt to call the model was made
callbacks: Final = litellm.input_callback + (self.dynamic_input_callbacks or [])
callbacks: Final = (
*self._without_replaced_global_callbacks(litellm.input_callback),
*(self.dynamic_input_callbacks or ()),
)
for callback in callbacks:
try:
if callback == "sentry" and add_breadcrumb:
@ -4492,23 +4498,26 @@ class Logging(LiteLLMLoggingBaseClass):
_filtered_failure_callbacks = self._remove_internal_litellm_callbacks(_filtered_failure_callbacks)
return len(_filtered_failure_callbacks) > 0
def _without_replaced_global_callbacks(
self, global_callbacks: Iterable[str | Callable[..., object] | CustomLogger]
) -> tuple[str | Callable[..., object] | CustomLogger, ...]:
if not self.replaced_global_callback_keys:
return tuple(global_callbacks)
return tuple(
litellm.logging_callback_manager.without_custom_logger_keys(
global_callbacks, self.replaced_global_callback_keys
)
)
def get_combined_callback_list(
self,
dynamic_success_callbacks: list[str | Callable[..., object] | CustomLogger] | None,
global_callbacks: list[str | Callable[..., object] | CustomLogger],
) -> list[str | Callable[..., object] | CustomLogger]:
kept_global_callbacks: Final = (
list(
litellm.logging_callback_manager.without_custom_logger_keys(
global_callbacks, self.replaced_global_callback_keys
)
)
if self.replaced_global_callback_keys
else global_callbacks
)
kept_global_callbacks: Final = self._without_replaced_global_callbacks(global_callbacks)
if dynamic_success_callbacks is None:
return list(kept_global_callbacks)
return list(dict.fromkeys(dynamic_success_callbacks + kept_global_callbacks))
return list(dict.fromkeys((*dynamic_success_callbacks, *kept_global_callbacks)))
def _remove_internal_litellm_callbacks(self, callbacks: list) -> list:
"""

View file

@ -402,6 +402,30 @@ def test_get_combined_callback_list_preserves_insertion_order(logging_obj):
) == ["prometheus", "langfuse", "datadog", "otel", "s3", "gcs_bucket", "arize", "logfire"]
class _InputEventRecorder(CustomLogger):
def __init__(self, label: str, events: list[str]) -> None:
self._label = label
self.events = events
def log_pre_api_call(self, model, messages, kwargs) -> None:
self.events.append(f"{self._label} pre")
def log_post_api_call(self, kwargs, response_obj, start_time, end_time) -> None:
self.events.append(f"{self._label} post")
def test_input_events_reach_the_replacement_and_not_the_global_callback_it_replaced(logging_obj, monkeypatch):
events: Final[list[str]] = []
monkeypatch.setattr(litellm, "input_callback", [_InputEventRecorder("global", events)])
logging_obj.replace_global_callback(_InputEventRecorder("replacement", events))
logging_obj.update_environment_variables(litellm_params={}, optional_params={})
logging_obj.pre_call(input="", api_key="")
logging_obj.post_call(original_response="ok", input="", api_key="")
assert events == ["replacement pre", "replacement post"]
def test_get_masked_api_base(logging_obj):
api_base = "https://api.openai.com/v1"
masked_api_base = logging_obj._get_masked_api_base(api_base)