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>
This commit is contained in:
kerry 2026-09-22 01:13:20 +00:00
parent 1dacb03ad9
commit 3ae43d35a1
4 changed files with 33 additions and 49 deletions

View file

@ -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),

View file

@ -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,

View file

@ -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

View file

@ -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)