From c798ef6d03ec6200ad6d6dad17999f716ea81684 Mon Sep 17 00:00:00 2001 From: kerry Date: Tue, 22 Sep 2026 01:05:48 +0000 Subject: [PATCH] fix(responses): price the completed response, not the terminal event, in post-success hooks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/responses/streaming_iterator.py | 7 +++- .../responses/test_streaming_iterator.py | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 59655800af6..e0a54ed7969 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -801,8 +801,13 @@ class BaseResponsesAPIStreamingIterator: request_payload["litellm_params"] = {} try: + response_obj: Final = self._get_completed_response_object() update_response_metadata( - result=self.completed_response, + result=( + type(response_obj).model_validate(response_obj.model_dump()) + if response_obj is not None + else self.completed_response + ), logging_obj=self.logging_obj, model=self.model, kwargs=request_payload, diff --git a/tests/test_litellm/responses/test_streaming_iterator.py b/tests/test_litellm/responses/test_streaming_iterator.py index dbf54ec3b9b..7dcd5595c32 100644 --- a/tests/test_litellm/responses/test_streaming_iterator.py +++ b/tests/test_litellm/responses/test_streaming_iterator.py @@ -342,6 +342,46 @@ 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)