diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index b34f1b3aafd..3ef26280e19 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1757,13 +1757,19 @@ class Logging(LiteLLMLoggingBaseClass): if transformed_result is not None: result = transformed_result - result_hidden_params: Final = getattr(result, "_hidden_params", None) or MappingProxyType({}) + priced_result: Final = ( + result.response + if isinstance(result, (ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent)) + else result + ) + + result_hidden_params: Final = getattr(priced_result, "_hidden_params", None) or MappingProxyType({}) result_additional_headers: Final = ( result_hidden_params.get("additional_headers") if isinstance(result_hidden_params, dict) else getattr(result_hidden_params, "additional_headers", None) ) - if isinstance(result, (BaseModel, HttpxBinaryResponseContent)) and hasattr(result, "_hidden_params"): + if isinstance(priced_result, (BaseModel, HttpxBinaryResponseContent)) and hasattr(priced_result, "_hidden_params"): hidden_params: Final = result_hidden_params if ( "response_cost" in hidden_params and hidden_params["response_cost"] is not None @@ -1799,7 +1805,7 @@ class Logging(LiteLLMLoggingBaseClass): try: response_cost_calculator_kwargs: Final = { - "response_object": result, + "response_object": priced_result, "model": litellm_model_name or self.model, "cache_hit": cache_hit, "custom_llm_provider": self.model_call_details.get("custom_llm_provider", None), diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index e0a54ed7969..59655800af6 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -801,13 +801,8 @@ class BaseResponsesAPIStreamingIterator: request_payload["litellm_params"] = {} try: - response_obj: Final = self._get_completed_response_object() update_response_metadata( - result=( - type(response_obj).model_validate(response_obj.model_dump()) - if response_obj is not None - else self.completed_response - ), + result=self.completed_response, logging_obj=self.logging_obj, model=self.model, kwargs=request_payload, 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 277ae33a076..d50ac109d87 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -7467,3 +7467,26 @@ def test_get_assembled_streaming_response_without_usage_cost_leaves_pricing_to_t assert "additional_headers" not in assembled._hidden_params price_map_cost = logging_obj._response_cost_calculator(result=assembled) assert price_map_cost is not None and 0 < price_map_cost != 0.0042 + + +def test_response_cost_calculator_prices_terminal_responses_event_from_its_response(): + """A terminal Responses stream event carries no usage itself; pricing must unwrap + it so the stored cost_breakdown is not overwritten with zeros.""" + logging_obj: Final = _responses_stream_logging_obj() + inner_response: Final = ResponsesAPIResponse( + id="resp-priced", + created_at=1, + object="response", + status="completed", + model="gpt-4o-mini", + output=[], + usage=ResponseAPIUsage(input_tokens=1840, output_tokens=412, total_tokens=2252), + ) + event: Final = ResponseCompletedEvent(type="response.completed", response=inner_response) + + event_cost: Final = logging_obj._response_cost_calculator(result=event) + inner_cost: Final = logging_obj._response_cost_calculator(result=inner_response) + + assert event_cost is not None and event_cost > 0 + assert event_cost == inner_cost + assert logging_obj.cost_breakdown["input_cost"] is not None and logging_obj.cost_breakdown["input_cost"] > 0 diff --git a/tests/test_litellm/responses/test_streaming_iterator.py b/tests/test_litellm/responses/test_streaming_iterator.py index 7dcd5595c32..dbf54ec3b9b 100644 --- a/tests/test_litellm/responses/test_streaming_iterator.py +++ b/tests/test_litellm/responses/test_streaming_iterator.py @@ -342,46 +342,6 @@ def test_run_post_success_hooks_does_not_report_generation_time_as_overhead(): assert "litellm_overhead_time_ms" not in iterator.completed_response._hidden_params -def test_run_post_success_hooks_prices_the_completed_response_not_the_event(): - """The terminal ResponseCompletedEvent carries no usage, so pricing it recomputes - cost as 0 and clobbers the cost_breakdown the stream already stored.""" - inner_response: Final = ResponsesAPIResponse( - id="resp_pricing", - created_at=0, - status="completed", - model="gpt-4o-mini", - object="response", - output=[], - usage=ResponseAPIUsage(input_tokens=1840, output_tokens=412, total_tokens=2252), - ) - event: Final = ResponseCompletedEvent( - type=ResponsesAPIStreamEvents.RESPONSE_COMPLETED, - response=inner_response, - ) - - logging_obj = _logging_obj_stub() - priced_results: Final[list[object]] = [] - - def _cost_calculator(*, result, **_kwargs): - priced_results.append(result) - return 0.0156 - - logging_obj._response_cost_calculator.side_effect = _cost_calculator - - iterator = _make_iterator(sse_events=[], logging_obj=logging_obj) - iterator.completed_response = event - iterator.start_time = datetime(2025, 1, 1, 0, 0, 0) - - iterator._run_post_success_hooks(datetime(2025, 1, 1, 0, 0, 10)) - - assert priced_results and all( - isinstance(result, ResponsesAPIResponse) and result is not event and result.usage is not None - for result in priced_results - ) - assert priced_results[0]._hidden_params["response_cost"] == 0.0156 - assert event.response._hidden_params == {} - - def _mock_config_with_completed_response(response: ResponsesAPIResponse) -> Mock: mock_config = Mock(spec=BaseResponsesAPIConfig)