From 73db7a3f78bc475bdb1c9b2c6ff84b9b772f2a33 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:07:41 +0000 Subject: [PATCH] fix(logging): normalize dict response on streaming responses success logger --- litellm/litellm_core_utils/litellm_logging.py | 13 +++--- .../test_litellm_logging.py | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 936d79b22d6..474f45cd2d1 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3161,13 +3161,14 @@ class Logging(LiteLLMLoggingBaseClass): (ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent), ): ## return unified Usage object - if isinstance(result.response.usage, ResponseAPIUsage): - transformed_usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( - result.response.usage - ) + response = result.response + if isinstance(response, dict): + response = ResponsesAPIResponse.model_validate(response) + if isinstance(response.usage, ResponseAPIUsage): + transformed_usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(response.usage) # Set as dict instead of Usage object so model_dump() serializes it correctly setattr( - result.response, + response, "usage", ( transformed_usage.model_dump() @@ -3175,7 +3176,7 @@ class Logging(LiteLLMLoggingBaseClass): else dict(transformed_usage) ), ) - return result.response + return response else: return None 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 0523ed7ecb1..53cb280c59b 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2331,6 +2331,49 @@ def test_get_assembled_streaming_response_returns_result_for_streaming(): assert assembled is result +def test_get_assembled_streaming_response_handles_dict_response_on_completed_event(): + """ + Regression for streaming /v1/responses success logging: the ResponseCompletedEvent + can carry a plain dict in its `.response` field at runtime (the models allow extra + and the event may be constructed without validation). Accessing `result.response.usage` + then raised `'dict' object has no attribute 'usage'`, which crashed the success handler + before the spend log was written. The assembler must normalize the dict and compute usage. + """ + import datetime + + from litellm.types.llms.openai import ResponseCompletedEvent, ResponsesAPIResponse + + logging_obj = _make_logging_obj(stream=True) + response_dict = { + "id": "resp-dict-1", + "created_at": 1234567890, + "output": [], + "usage": { + "input_tokens": 11, + "output_tokens": 22, + "total_tokens": 33, + }, + } + event = ResponseCompletedEvent.model_construct( + type="response.completed", response=response_dict + ) + assert isinstance(event.response, dict) + + assembled = logging_obj._get_assembled_streaming_response( + result=event, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + is_async=True, + streaming_chunks=[], + ) + + assert isinstance(assembled, ResponsesAPIResponse) + assert assembled.id == "resp-dict-1" + assert assembled.usage["prompt_tokens"] == 11 + assert assembled.usage["completion_tokens"] == 22 + assert assembled.usage["total_tokens"] == 33 + + def test_streaming_success_handler_includes_vertex_ai_metadata_in_standard_logging(): """Assembled streaming responses should include Vertex AI metadata in logging payload.""" import datetime