From 59cae5eb00ea30f321da58c3cefe3de78909d228 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:08:57 +0000 Subject: [PATCH] fix(logging): preserve prompt_tokens/completion_tokens for non-streaming /v1/responses usage --- litellm/types/llms/openai.py | 3 ++- .../test_litellm_logging.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index daac1e4506f..80a36ee2e8f 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -76,6 +76,7 @@ from pydantic import ( ConfigDict, Discriminator, PrivateAttr, + SerializeAsAny, field_serializer, field_validator, ) @@ -1270,7 +1271,7 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): status: Optional[str] = None text: Optional[Union["ResponseText", Dict[str, Any]]] = None truncation: Optional[Literal["auto", "disabled"]] = None - usage: Optional[ResponseAPIUsage] = None + usage: Optional[SerializeAsAny[ResponseAPIUsage]] = None user: Optional[str] = None store: Optional[bool] = None # Define private attributes using PrivateAttr 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 5bffda126fe..fc6dece21df 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -3609,6 +3609,33 @@ def test_handle_anthropic_messages_response_logging_translates_bare_responses_ap assert result.usage.total_tokens == 18 # type: ignore[attr-defined] +def test_transform_usage_objects_responses_api_keeps_prompt_and_completion_tokens(): + """Regression for #33688. Non-streaming /v1/responses logging runs the result + through _transform_usage_objects, which swaps the ResponseAPIUsage for a chat + Usage carrying prompt_tokens/completion_tokens. Serializing the returned + ResponsesAPIResponse must preserve those fields; before the SerializeAsAny fix + the ResponseAPIUsage-typed field silently dropped them, leaving only + total_tokens for downstream loggers (custom callbacks, Prometheus).""" + logging_obj = LitellmLogging( + model="gpt-4o", + messages=[{"role": "user", "content": "hi"}], + stream=False, + call_type="aresponses", + litellm_call_id="test-call-id", + start_time=time.time(), + function_id="test-fn", + ) + + result = logging_obj._transform_usage_objects( + result=_responses_api_response_with_text("hello world") + ) + + serialized_usage = result.model_dump()["usage"] + assert serialized_usage["prompt_tokens"] == 11 + assert serialized_usage["completion_tokens"] == 7 + assert serialized_usage["total_tokens"] == 18 + + def test_handle_anthropic_messages_response_logging_passes_model_response_through(): """Anthropic-native path already yields a ModelResponse; it must be returned unchanged.""" logging_obj = _anthropic_messages_logging_obj()