From 856c22a1cd0c8fdd6497b4864448a8fb2a3af393 Mon Sep 17 00:00:00 2001 From: kerry Date: Wed, 28 Oct 2026 03:10:24 +0000 Subject: [PATCH] fix(cost): keep a $0 stream bill in hidden response_cost so the egress shows it A free or fully discounted stream used to leave the upstream usage.cost on the client frame while the spend row read 0, because the builder skipped stamping a zero bill Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../streaming_chunk_builder_utils.py | 2 +- .../unit/responses/test_streaming_iterator.py | 23 ++++++++++++++---- tests/unit/test_main.py | 24 +++++++++++++++++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index 7cf024be2a1..daa816179a1 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -1415,7 +1415,7 @@ def _stamp_streaming_response_cost(response: ModelResponse, logging_obj: Optiona if logging_obj is None: return computed_cost: Final = logging_obj.response_cost_calculator(result=response) - if not isinstance(computed_cost, (int, float)) or computed_cost <= 0: + if isinstance(computed_cost, bool) or not isinstance(computed_cost, (int, float)) or computed_cost < 0: return _set_hidden_response_cost(response, float(computed_cost)) diff --git a/tests/unit/responses/test_streaming_iterator.py b/tests/unit/responses/test_streaming_iterator.py index 710bcb43e25..854719783bc 100644 --- a/tests/unit/responses/test_streaming_iterator.py +++ b/tests/unit/responses/test_streaming_iterator.py @@ -885,16 +885,29 @@ def test_normalize_responses_usage_never_writes_a_cost_the_provider_did_not_repo def test_normalize_responses_usage_stamps_the_logging_objects_bill_as_hidden_response_cost(): from litellm.responses.streaming_iterator import normalize_responses_usage - response = _responses_api_response_with_usage() - logging_obj = MagicMock() - logging_obj.response_cost_calculator.return_value = 0.00032 + from litellm.litellm_core_utils.litellm_logging import Logging + + response: Final = _responses_api_response_with_usage() + logging_obj: Final = Logging( + model="mantle-claude", + messages=[{"role": "user", "content": "q"}], + stream=True, + call_type="aresponses", + start_time=None, + litellm_call_id="call", + function_id="fn", + ) + logging_obj.update_environment_variables( + litellm_params={"custom_llm_provider": "openai", "input_cost_per_token": 2e-6, "output_cost_per_token": 6e-6}, + optional_params={}, + ) + logging_obj.custom_cost_per_token = {"input_cost_per_token": 2e-6, "output_cost_per_token": 6e-6} normalize_responses_usage(response, logging_obj) hidden: Final = get_hidden_params(response) - assert hidden is not None and hidden["response_cost"] == 0.00032 + assert hidden is not None and hidden["response_cost"] == pytest.approx(20 * 2e-6 + 60 * 6e-6) assert getattr(response.usage, "cost", None) is None, "the client field is written at the proxy egress, not here" - assert logging_obj.response_cost_calculator.call_args.kwargs["result"] is response def _capture_dispatch(logged: list): diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 13663cf4dd5..08861ea7df8 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -4907,6 +4907,26 @@ def test_stream_chunk_builder_bills_a_stream_without_writing_into_its_usage(monk assert response._hidden_params["response_cost"] > 0 +def test_stream_chunk_builder_keeps_a_zero_bill_as_hidden_response_cost() -> None: + logging_obj: Final = _stream_builder_logging_obj() + logging_obj.update_environment_variables( + litellm_params={"custom_llm_provider": "openai", "input_cost_per_token": 0.0, "output_cost_per_token": 0.0}, + optional_params={}, + ) + logging_obj.custom_cost_per_token = {"input_cost_per_token": 0.0, "output_cost_per_token": 0.0} + chunks: Final = [ + _stream_builder_text_chunk("gpt-4o", "Hello "), + _stream_builder_text_chunk("gpt-4o", "world.", finish_reason="stop"), + ] + + response: Final = litellm.stream_chunk_builder( + chunks=chunks, messages=[{"role": "user", "content": "hi"}], logging_obj=logging_obj + ) + + assert response is not None + assert response._hidden_params["response_cost"] == 0.0, "a free stream still tells the egress what was billed" + + def test_stream_chunk_builder_stamped_cost_is_not_re_read_as_xai_reported_cost(): import time as time_module @@ -4949,7 +4969,7 @@ def test_stream_chunk_builder_stamped_cost_is_not_re_read_as_xai_reported_cost() assert spend_row_cost == pytest.approx(usage_cost) -def test_stream_chunk_builder_skips_stamp_when_cost_is_unpriceable(): +def test_stream_chunk_builder_stamps_the_zero_bill_when_cost_is_unpriceable(): import time as time_module from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging @@ -4978,7 +4998,7 @@ def test_stream_chunk_builder_skips_stamp_when_cost_is_unpriceable(): assert response is not None assert getattr(response.usage, "cost", None) is None - assert response._hidden_params.get("response_cost") is None + assert response._hidden_params.get("response_cost") == 0.0, "the spend row reads 0, so the egress must too" def test_stream_chunk_builder_prices_alias_response_at_its_deployment_model():