diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 76e37010109..c673ec1219c 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1639,6 +1639,13 @@ class Logging(LiteLLMLoggingBaseClass): logging_result, start_time, end_time ) + if ( + standard_logging_payload := self.model_call_details.get( + "standard_logging_object" + ) + ) is not None: + emit_standard_logging_payload(standard_logging_payload) + def _build_standard_logging_payload( self, init_response_obj: Any, start_time: Any, end_time: Any ) -> Any: @@ -1752,6 +1759,12 @@ class Logging(LiteLLMLoggingBaseClass): ] = self._build_standard_logging_payload( result, start_time, end_time ) + if ( + standard_logging_payload := self.model_call_details.get( + "standard_logging_object" + ) + ) is not None: + emit_standard_logging_payload(standard_logging_payload) elif standard_logging_object is not None: self.model_call_details[ "standard_logging_object" @@ -3211,6 +3224,8 @@ class Logging(LiteLLMLoggingBaseClass): is_async: bool, streaming_chunks: List[Any], ) -> Optional[Union[ModelResponse, TextCompletionResponse, ResponsesAPIResponse]]: + if self.stream is not True: + return None if isinstance(result, ModelResponse): return result elif isinstance(result, TextCompletionResponse): diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 3cc91869289..0c9adecf5c5 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -14,7 +14,7 @@ import time from litellm.constants import SENTRY_DENYLIST, SENTRY_PII_DENYLIST from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging from litellm.litellm_core_utils.litellm_logging import set_callbacks -from litellm.types.utils import ModelResponse +from litellm.types.utils import ModelResponse, TextCompletionResponse @pytest.fixture @@ -264,7 +264,7 @@ async def test_logging_result_for_bridge_calls(logging_obj): mock_response="Hello, world!", ) await asyncio.sleep(1) - assert mock_should_run_logging.call_count == 2 # called twice per call + assert mock_should_run_logging.call_count == 1 @pytest.mark.asyncio @@ -1357,3 +1357,116 @@ def test_get_error_information_error_code_priority(): assert result["error_class"] == "NoCodeException" +# ────────────────────────────────────────────────────────────────────── +# Tests for _get_assembled_streaming_response non-streaming early return +# ────────────────────────────────────────────────────────────────────── + + +def _make_logging_obj(stream: bool) -> LitellmLogging: + return LitellmLogging( + model="openai/codex-mini-latest", + messages=[{"role": "user", "content": "Hey"}], + stream=stream, + call_type="completion", + start_time=time.time(), + litellm_call_id="test-123", + function_id="test-fn", + ) + + +def test_get_assembled_streaming_response_returns_none_for_non_streaming(): + """Non-streaming requests should return None so the streaming block is skipped.""" + import datetime + + logging_obj = _make_logging_obj(stream=False) + result = ModelResponse(id="resp-1", choices=[], model="test") + assembled = logging_obj._get_assembled_streaming_response( + result=result, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + is_async=True, + streaming_chunks=[], + ) + assert assembled is None + + +def test_get_assembled_streaming_response_returns_result_for_streaming(): + """Streaming requests should return the ModelResponse for further processing.""" + import datetime + + logging_obj = _make_logging_obj(stream=True) + result = ModelResponse(id="resp-1", choices=[], model="test") + assembled = logging_obj._get_assembled_streaming_response( + result=result, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + is_async=True, + streaming_chunks=[], + ) + assert assembled is result + + +def test_get_assembled_streaming_response_returns_none_for_non_streaming_text_completion(): + """Non-streaming TextCompletionResponse should also return None.""" + import datetime + + logging_obj = _make_logging_obj(stream=False) + result = TextCompletionResponse(id="resp-1", choices=[], model="test") + assembled = logging_obj._get_assembled_streaming_response( + result=result, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + is_async=True, + streaming_chunks=[], + ) + assert assembled is None + + +@pytest.mark.asyncio +async def test_non_streaming_computes_standard_logging_object_once(): + """ + Non-streaming acompletion should call get_standard_logging_object_payload + exactly once, not twice. + """ + import asyncio + + import litellm + + with patch.object( + litellm.litellm_core_utils.litellm_logging, + "get_standard_logging_object_payload", + ) as mock_payload: + await litellm.acompletion( + max_tokens=100, + messages=[{"role": "user", "content": "Hey"}], + model="openai/codex-mini-latest", + mock_response="Hello, world!", + ) + await asyncio.sleep(1) + assert mock_payload.call_count == 1 + + +@pytest.mark.asyncio +async def test_emit_standard_logging_payload_called_for_non_streaming(): + """ + emit_standard_logging_payload should still be called for non-streaming + requests (moved from the streaming block to _process_hidden_params_and_response_cost). + """ + import asyncio + + import litellm + + with patch.object( + litellm.litellm_core_utils.litellm_logging, + "emit_standard_logging_payload", + ) as mock_emit: + await litellm.acompletion( + max_tokens=100, + messages=[{"role": "user", "content": "Hey"}], + model="openai/codex-mini-latest", + mock_response="Hello, world!", + ) + await asyncio.sleep(1) + assert mock_emit.call_count >= 1 + +