From c798ef6d03ec6200ad6d6dad17999f716ea81684 Mon Sep 17 00:00:00 2001 From: kerry Date: Tue, 22 Sep 2026 01:05:48 +0000 Subject: [PATCH 1/5] 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) From 1dacb03ad92f41d0a6bfd4ce9b60c70b0864389a Mon Sep 17 00:00:00 2001 From: kerry Date: Tue, 22 Sep 2026 01:05:48 +0000 Subject: [PATCH 2/5] test(integration): allow unmanaged response ids and serve fal h3 video bytes without auth Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/integration/providers/test_fal_ai_video_wire.py | 3 +++ tests/integration/proxy_config.yaml | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/integration/providers/test_fal_ai_video_wire.py b/tests/integration/providers/test_fal_ai_video_wire.py index 827818c6780..276ea2868a7 100644 --- a/tests/integration/providers/test_fal_ai_video_wire.py +++ b/tests/integration/providers/test_fal_ai_video_wire.py @@ -76,6 +76,9 @@ def test_fal_h3_video_create_uses_canonical_body_and_status_path(gateway: Gatewa request_id: Final = "fal-h3-req-" + uuid.uuid4().hex def respond(request: Request) -> Reply: + if request.target == f"/files/{request_id}.mp4": + assert request.method == "GET" + return Reply(body=_MP4, content_type="video/mp4") assert request.headers["authorization"] == "Key synthetic-fal-key" if request.method == "POST": assert request.target == f"/{_H3_MODEL}" diff --git a/tests/integration/proxy_config.yaml b/tests/integration/proxy_config.yaml index a3b07f76d2f..34e48228dd7 100644 --- a/tests/integration/proxy_config.yaml +++ b/tests/integration/proxy_config.yaml @@ -5,6 +5,7 @@ general_settings: store_model_in_db: true disable_spend_logs: false proxy_batch_write_at: 1 + allow_unmanaged_response_ids: true litellm_settings: enable_redis_auth_cache: true cache: true From 3ae43d35a1dd2501ee0c2e745648a1de1b747d9b Mon Sep 17 00:00:00 2001 From: kerry Date: Tue, 22 Sep 2026 01:13:20 +0000 Subject: [PATCH 3/5] fix(logging): price terminal Responses stream events from their inner response Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/litellm_logging.py | 12 ++++-- litellm/responses/streaming_iterator.py | 7 +--- .../test_litellm_logging.py | 23 +++++++++++ .../responses/test_streaming_iterator.py | 40 ------------------- 4 files changed, 33 insertions(+), 49 deletions(-) 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) From f7b11b3430c2263ca704258d2d43f02ee48a7630 Mon Sep 17 00:00:00 2001 From: kerry Date: Tue, 22 Sep 2026 01:17:20 +0000 Subject: [PATCH 4/5] style(logging): ruff format litellm_logging.py Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/litellm_logging.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 3ef26280e19..7bad711940e 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1769,7 +1769,9 @@ class Logging(LiteLLMLoggingBaseClass): if isinstance(result_hidden_params, dict) else getattr(result_hidden_params, "additional_headers", None) ) - if isinstance(priced_result, (BaseModel, HttpxBinaryResponseContent)) and hasattr(priced_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 From 2f6a9eb073754dae7c016fe2d94e23829697c5a7 Mon Sep 17 00:00:00 2001 From: kerry Date: Tue, 22 Sep 2026 01:20:44 +0000 Subject: [PATCH 5/5] test(logging): drop docstring from terminal event pricing test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/test_litellm/litellm_core_utils/test_litellm_logging.py | 2 -- 1 file changed, 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 d50ac109d87..959b4f01986 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -7470,8 +7470,6 @@ def test_get_assembled_streaming_response_without_usage_cost_leaves_pricing_to_t 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",