diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index ce57a9bd4a5..6240bfb2774 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -427,6 +427,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. @@ -4074,12 +4081,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, @@ -4451,12 +4456,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, @@ -4465,7 +4467,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 e8ea0c04f6f..56c49ea7f30 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()