mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
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:
parent
1dacb03ad9
commit
3ae43d35a1
4 changed files with 33 additions and 49 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue