mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-03 08:23:16 +00:00
[Feat] Return Cost for Responses API Streaming requests (#15053)
* test_basic_openai_responses_api_streaming * _transform_chat_completion_usage_to_responses_usage * ResponseAPIUsage.cost * test fixes for anthropic cost with /responses * fix mypy typng
This commit is contained in:
@@ -42,6 +42,7 @@ guardrails:
|
||||
|
||||
litellm_settings:
|
||||
callbacks: ["datadog"]
|
||||
include_cost_in_streaming_usage: true
|
||||
datadog_params:
|
||||
turn_off_message_logging: true
|
||||
datadog_llm_observability_params:
|
||||
|
||||
@@ -49,6 +49,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||
self.litellm_metadata: Optional[dict] = litellm_metadata or {}
|
||||
self.collected_chat_completion_chunks: List[ModelResponseStream] = []
|
||||
self.finished: bool = False
|
||||
self.litellm_logging_obj = litellm_custom_stream_wrapper.logging_obj
|
||||
|
||||
async def __anext__(
|
||||
self,
|
||||
@@ -167,8 +168,16 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||
def _emit_response_completed_event(self) -> Optional[ResponseCompletedEvent]:
|
||||
litellm_model_response: Optional[
|
||||
Union[ModelResponse, TextCompletionResponse]
|
||||
] = stream_chunk_builder(chunks=self.collected_chat_completion_chunks)
|
||||
] = stream_chunk_builder(chunks=self.collected_chat_completion_chunks, logging_obj=self.litellm_logging_obj)
|
||||
if litellm_model_response and isinstance(litellm_model_response, ModelResponse):
|
||||
# Add cost to usage object if include_cost_in_streaming_usage is True
|
||||
if litellm.include_cost_in_streaming_usage and self.litellm_logging_obj is not None:
|
||||
usage = getattr(litellm_model_response, "usage", None)
|
||||
if usage is not None:
|
||||
setattr(
|
||||
usage, "cost", self.litellm_logging_obj._response_cost_calculator(result=litellm_model_response)
|
||||
)
|
||||
|
||||
# Transform the response
|
||||
responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input=self.request_input,
|
||||
|
||||
@@ -851,8 +851,15 @@ class LiteLLMCompletionResponsesConfig:
|
||||
output_tokens=0,
|
||||
total_tokens=0,
|
||||
)
|
||||
return ResponseAPIUsage(
|
||||
|
||||
response_usage = ResponseAPIUsage(
|
||||
input_tokens=usage.prompt_tokens,
|
||||
output_tokens=usage.completion_tokens,
|
||||
total_tokens=usage.total_tokens,
|
||||
)
|
||||
|
||||
# Preserve cost field if it exists (for streaming usage with cost calculation)
|
||||
if hasattr(usage, "cost") and usage.cost is not None:
|
||||
setattr(response_usage, "cost", usage.cost)
|
||||
|
||||
return response_usage
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.constants import STREAM_SSE_DONE_STRING
|
||||
from litellm.litellm_core_utils.asyncify import run_async_function
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
@@ -13,6 +14,7 @@ from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfi
|
||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||
from litellm.types.llms.openai import (
|
||||
OutputTextDeltaEvent,
|
||||
ResponseAPIUsage,
|
||||
ResponseCompletedEvent,
|
||||
ResponsesAPIResponse,
|
||||
ResponsesAPIStreamEvents,
|
||||
@@ -95,6 +97,20 @@ class BaseResponsesAPIStreamingIterator:
|
||||
== ResponsesAPIStreamEvents.RESPONSE_COMPLETED
|
||||
):
|
||||
self.completed_response = openai_responses_api_chunk
|
||||
# Add cost to usage object if include_cost_in_streaming_usage is True
|
||||
if litellm.include_cost_in_streaming_usage and self.logging_obj is not None:
|
||||
response_obj: Optional[ResponsesAPIResponse] = getattr(openai_responses_api_chunk, "response", None)
|
||||
if response_obj:
|
||||
usage_obj: Optional[ResponseAPIUsage] = getattr(response_obj, "usage", None)
|
||||
if usage_obj is not None:
|
||||
try:
|
||||
cost: Optional[float] = self.logging_obj._response_cost_calculator(result=response_obj)
|
||||
if cost is not None:
|
||||
setattr(usage_obj, "cost", cost)
|
||||
except Exception:
|
||||
# If cost calculation fails, continue without cost
|
||||
pass
|
||||
|
||||
self._handle_logging_completed_response()
|
||||
|
||||
return openai_responses_api_chunk
|
||||
|
||||
@@ -1033,6 +1033,9 @@ class ResponseAPIUsage(BaseLiteLLMOpenAIResponseObject):
|
||||
total_tokens: int
|
||||
"""The total number of tokens used."""
|
||||
|
||||
cost: Optional[float] = None
|
||||
"""The cost of the request."""
|
||||
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ class BaseResponsesAPITest(ABC):
|
||||
@pytest.mark.flaky(retries=3, delay=2)
|
||||
async def test_basic_openai_responses_api_streaming(self, sync_mode):
|
||||
litellm._turn_on_debug()
|
||||
# Enable cost calculation for streaming usage
|
||||
litellm.include_cost_in_streaming_usage = True
|
||||
base_completion_call_args = self.get_base_completion_call_args()
|
||||
collected_content_string = ""
|
||||
response_completed_event = None
|
||||
@@ -208,6 +210,14 @@ class BaseResponsesAPITest(ABC):
|
||||
+ response_completed_event.response.usage.output_tokens
|
||||
)
|
||||
|
||||
# assert the response completed event includes cost when include_cost_in_streaming_usage is True
|
||||
assert hasattr(response_completed_event.response.usage, "cost"), "Cost should be included in streaming responses API usage object"
|
||||
assert response_completed_event.response.usage.cost > 0, "Cost should be greater than 0"
|
||||
print(f"Cost found in streaming response: {response_completed_event.response.usage.cost}")
|
||||
|
||||
# Reset the setting
|
||||
litellm.include_cost_in_streaming_usage = False
|
||||
|
||||
@pytest.mark.parametrize("sync_mode", [False, True])
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_openai_responses_delete_endpoint(self, sync_mode):
|
||||
|
||||
Reference in New Issue
Block a user