From e4b6859c6810e18c105d0d1d1e8d6c9e8eb66877 Mon Sep 17 00:00:00 2001 From: Varshith Date: Fri, 22 May 2026 11:55:00 -0500 Subject: [PATCH] fix(logging): handle ResponseCompletedEvent in anthropic_messages spend_logs path --- litellm/litellm_core_utils/litellm_logging.py | 16 +++- .../test_litellm_logging.py | 96 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 2ab037afb0d..25e9f57cddb 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3424,7 +3424,9 @@ class Logging(LiteLLMLoggingBaseClass): else: return None - def _handle_anthropic_messages_response_logging(self, result: Any) -> ModelResponse: + def _handle_anthropic_messages_response_logging( + self, result: Any + ) -> Union[ModelResponse, ResponsesAPIResponse]: """ Handles logging for Anthropic messages responses. @@ -3444,6 +3446,18 @@ class Logging(LiteLLMLoggingBaseClass): elif isinstance(result, ModelResponse): return result + # Responses API bridge: streaming delivers a ResponseCompletedEvent (or its + # incomplete/failed variants) rather than an AnthropicResponse. Extract the + # nested ResponsesAPIResponse so downstream logging (_transform_usage_objects, + # spend_logs) can process it without attempting AnthropicResponse.model_validate. + if isinstance( + result, + (ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent), + ): + return result.response + elif isinstance(result, ResponsesAPIResponse): + return result + httpx_response = self.model_call_details.get("httpx_response", None) if httpx_response and isinstance(httpx_response, httpx.Response): result = litellm.AnthropicConfig().transform_response( 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 07ab29c5231..508d2cd58bf 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2872,3 +2872,99 @@ class TestFirstApiCallStartTimeSetOnce: assert obj.model_call_details["api_call_start_time"] > first assert obj.model_call_details["first_api_call_start_time"] == first assert user_meta == {} + + +class TestHandleAnthropicMessagesResponseLoggingResponsesBridge: + """ + Regression tests for _handle_anthropic_messages_response_logging when the + Responses API bridge is active (non-Anthropic backend behind /v1/messages). + + Before the fix, result arrived as a ResponseCompletedEvent and + AnthropicResponse.model_validate(result) raised a ValidationError that was + swallowed as Non-Blocking, silently dropping the spend_logs row. + """ + + def _make_logging_obj(self, stream: bool) -> LitellmLogging: + return LitellmLogging( + model="openai/gpt-4o", + messages=[{"role": "user", "content": "hi"}], + stream=stream, + call_type="anthropic_messages", + start_time=time.time(), + litellm_call_id="test-responses-bridge-123", + function_id="test-fn", + ) + + def _make_responses_api_response(self) -> "ResponsesAPIResponse": + from litellm.types.llms.openai import ResponsesAPIResponse + + return ResponsesAPIResponse( + id="resp_abc123", + created_at=1700000000, + model="gpt-4o", + output=[], + usage={ + "input_tokens": 20, + "output_tokens": 10, + "total_tokens": 30, + }, + ) + + def test_handle_anthropic_messages_response_logging_streaming_response_completed_event_returns_responses_api_response( + self, + ): + from litellm.types.llms.openai import ( + ResponseCompletedEvent, + ResponsesAPIResponse, + ) + + logging_obj = self._make_logging_obj(stream=True) + inner_response = self._make_responses_api_response() + completed_event = ResponseCompletedEvent( + type="response.completed", + response=inner_response, + ) + + result = logging_obj._handle_anthropic_messages_response_logging( + result=completed_event + ) + + assert isinstance(result, ResponsesAPIResponse) + assert result.id == "resp_abc123" + + def test_handle_anthropic_messages_response_logging_response_incomplete_event_returns_responses_api_response( + self, + ): + from litellm.types.llms.openai import ( + ResponseIncompleteEvent, + ResponsesAPIResponse, + ) + + logging_obj = self._make_logging_obj(stream=True) + inner_response = self._make_responses_api_response() + incomplete_event = ResponseIncompleteEvent( + type="response.incomplete", + response=inner_response, + ) + + result = logging_obj._handle_anthropic_messages_response_logging( + result=incomplete_event + ) + + assert isinstance(result, ResponsesAPIResponse) + assert result.id == "resp_abc123" + + def test_handle_anthropic_messages_response_logging_responses_api_response_passthrough( + self, + ): + from litellm.types.llms.openai import ResponsesAPIResponse + + logging_obj = self._make_logging_obj(stream=False) + responses_api_response = self._make_responses_api_response() + + result = logging_obj._handle_anthropic_messages_response_logging( + result=responses_api_response + ) + + assert isinstance(result, ResponsesAPIResponse) + assert result.id == "resp_abc123"