From a5be969ae77e77de07d4ec9caea409fac8ccf48e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:09:15 +0000 Subject: [PATCH 1/2] test(logging): lock in streaming response_cost survival across sync success handler --- .../test_litellm_logging.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 edc257f4c3f..cf888974928 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2796,6 +2796,39 @@ async def test_async_success_handler_preserves_response_cost_for_pass_through_en assert slo is not None assert slo["response_cost"] > 0 +@pytest.mark.asyncio +async def test_sync_success_handler_preserves_streaming_cost_computed_by_async_handler(): + """Regression for #34875: the sync success_handler used to reset response_cost to None + for streams, so custom callbacks reading kwargs["response_cost"] saw None whenever it ran + after the async handler had already computed the cost on the shared model_call_details.""" + import litellm + + litellm.callbacks = [] + litellm.success_callback = [] + + response = await litellm.acompletion( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "hi"}], + stream=True, + mock_response="hello there", + stream_options={"include_usage": True}, + ) + logging_obj = response.logging_obj + async for _chunk in response: + pass + await asyncio.sleep(0.5) + + cost_from_async_handler = logging_obj.model_call_details.get("response_cost") + assert cost_from_async_handler is not None and cost_from_async_handler > 0 + + assembled = logging_obj.model_call_details.get( + "async_complete_streaming_response" + ) or logging_obj.model_call_details.get("complete_streaming_response") + + for _ in range(2): + logging_obj.success_handler(assembled, None, None, False) + assert logging_obj.model_call_details["response_cost"] == cost_from_async_handler + def test_process_hidden_params_recalculates_cost_after_failure_handler_zero(): """ From 2a5bd9562dda19c5050d538f4287ef30555cbb42 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:37:13 +0000 Subject: [PATCH 2/2] test(logging): poll for the async streaming cost instead of a fixed sleep --- .../litellm_core_utils/test_litellm_logging.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 cf888974928..67c976c6ab7 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -1,6 +1,7 @@ import os import sys import asyncio +from typing import Optional from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -2796,6 +2797,7 @@ async def test_async_success_handler_preserves_response_cost_for_pass_through_en assert slo is not None assert slo["response_cost"] > 0 + @pytest.mark.asyncio async def test_sync_success_handler_preserves_streaming_cost_computed_by_async_handler(): """Regression for #34875: the sync success_handler used to reset response_cost to None @@ -2816,9 +2818,16 @@ async def test_sync_success_handler_preserves_streaming_cost_computed_by_async_h logging_obj = response.logging_obj async for _chunk in response: pass - await asyncio.sleep(0.5) - cost_from_async_handler = logging_obj.model_call_details.get("response_cost") + async def wait_for_async_handler_cost() -> Optional[float]: + for _ in range(200): + cost = logging_obj.model_call_details.get("response_cost") + if cost: + return cost + await asyncio.sleep(0.05) + return None + + cost_from_async_handler = await wait_for_async_handler_cost() assert cost_from_async_handler is not None and cost_from_async_handler > 0 assembled = logging_obj.model_call_details.get(