From 8b895849b26d98b4be987c5186b21cc0ef84f2e1 Mon Sep 17 00:00:00 2001 From: mateo <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:11:57 +0000 Subject: [PATCH] fix(logging): initialize callable callbacks during synchronous success dispatch Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/litellm_logging.py | 20 ++++---- .../test_litellm_logging.py | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 97c019a2180..06f3a439762 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -415,6 +415,13 @@ in_memory_dynamic_logger_cache: Final = DynamicLoggingCache() _PrometheusLogger = None +def _get_custom_logger() -> CustomLogger: + global customLogger + if customLogger is None: + customLogger = CustomLogger() # rebind-ok: process-wide callable callback adapter shared by logging handlers + return customLogger + + def _get_cached_prometheus_logger(): """ Get cached PrometheusLogger class. @@ -4117,12 +4124,10 @@ class Logging(LiteLLMLoggingBaseClass): start_time=start_time, end_time=end_time, ) - if ( - callable(callback) is True and is_sync_request and customLogger is not None - ): # custom logger functions + if callable(callback) and is_sync_request: print_verbose(f"success callbacks: Running Custom Callback Function - {callback}") - customLogger.log_event( + _get_custom_logger().log_event( kwargs=call_details, response_obj=result, start_time=start_time, @@ -4496,12 +4501,9 @@ class Logging(LiteLLMLoggingBaseClass): end_time=end_time, ) if callable(callback): # custom logger functions - global customLogger - if customLogger is None: - customLogger = CustomLogger() if self.stream: if "async_complete_streaming_response" in self.model_call_details: - await customLogger.async_log_event( + await _get_custom_logger().async_log_event( kwargs=self.model_call_details, response_obj=self.model_call_details["async_complete_streaming_response"], start_time=start_time, @@ -4510,7 +4512,7 @@ class Logging(LiteLLMLoggingBaseClass): callback_func=callback, ) else: - await customLogger.async_log_event( + await _get_custom_logger().async_log_event( kwargs=self.model_call_details, response_obj=result, start_time=start_time, diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index 13e181b2b92..a2754da6448 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -8932,6 +8932,53 @@ async def test_logger_in_the_global_failure_callback_lists_gets_one_failure_hook assert recorder.hooks == ["async_failure"] +@pytest.mark.usefixtures("no_global_callbacks") +@pytest.mark.parametrize("event", ["success", "failure"]) +def test_sync_callable_after_string_callback_setup_receives_the_call( + monkeypatch: pytest.MonkeyPatch, event: str +) -> None: + monkeypatch.setattr(litellm.utils, "callback_list", []) + monkeypatch.setattr(litellm_logging, "customLogger", None) + monkeypatch.setattr(litellm_logging, "promptLayerLogger", None) + monkeypatch.setattr(litellm, "input_callback", []) + monkeypatch.setattr(litellm, "_async_input_callback", []) + monkeypatch.setattr(litellm, "success_callback", ["promptlayer"]) + start: Final = datetime.datetime(2026, 1, 1) + end: Final = start + datetime.timedelta(seconds=1) + litellm.utils.function_setup( + original_function="completion", + rules_obj=litellm.utils.Rules(), + start_time=start, + is_async_call=False, + model="gpt-4o", + messages=[{"role": "user", "content": "hi"}], + litellm_call_id="string-callback", + ) + calls: Final[list[tuple[object, object, object, object]]] = [] + + def callback(kwargs: dict[str, object], response: object, started: object, ended: object) -> None: + calls.append((kwargs["litellm_call_id"], response, started, ended)) + + monkeypatch.setattr(litellm, "success_callback", [callback] if event == "success" else []) + monkeypatch.setattr(litellm, "failure_callback", [callback] if event == "failure" else []) + logging_obj, _ = litellm.utils.function_setup( + original_function="completion", + rules_obj=litellm.utils.Rules(), + start_time=start, + is_async_call=False, + model="gpt-4o", + messages=[{"role": "user", "content": "hi"}], + litellm_call_id="callable-callback", + ) + response: Final = ModelResponse(model="gpt-4o") + if event == "success": + logging_obj.success_handler(response, start, end) + else: + logging_obj.failure_handler(ValueError("upstream rejected"), "traceback", start, end) + + assert calls == [("callable-callback", response if event == "success" else None, start, end)] + + @pytest.mark.usefixtures("no_global_callbacks") def test_failure_handler_run_without_an_async_failure_run_keeps_the_sync_hook() -> None: recorder: Final = _BothFailureHooksRecorder()