diff --git a/litellm/proxy/pass_through_endpoints/streaming_handler.py b/litellm/proxy/pass_through_endpoints/streaming_handler.py index 19d8b063dd7..6cda69bd8fe 100644 --- a/litellm/proxy/pass_through_endpoints/streaming_handler.py +++ b/litellm/proxy/pass_through_endpoints/streaming_handler.py @@ -12,6 +12,7 @@ from litellm.litellm_core_utils.asyncify import asyncify from litellm.litellm_core_utils.core_helpers import bind_budget_reservation_to_callbacks from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER +from litellm.llms.anthropic.common_utils import AnthropicError from litellm.proxy._types import PassThroughEndpointLoggingResultValues from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.proxy.common_utils.sse_keepalive import split_complete_sse_frames @@ -297,6 +298,7 @@ class PassThroughStreamingHandler: from litellm.llms.anthropic.pass_through.messages.streaming_iterator import ( _is_message_stop_chunk, # pyright: ignore[reportPrivateUsage] # both native stream paths share terminal-event detection _is_provider_error_chunk, # pyright: ignore[reportPrivateUsage] # provider errors must not become cache evidence + parse_anthropic_error_event, ) # Transport reads can split event names and JSON payloads. Recognize terminal @@ -312,6 +314,24 @@ class PassThroughStreamingHandler: and _is_message_stop_chunk(complete_frames) and not _is_provider_error_chunk(complete_frames) ) + # An in-band ``event: error`` on a cleanly closed 200 stream is an upstream + # failure, not a success: log it as one (partial usage included). + provider_error: Final = parse_anthropic_error_event(complete_frames) if complete_frames else None + if endpoint_type == EndpointType.ANTHROPIC and provider_error is not None: + _, error_message, error_status_code = provider_error + await PassThroughStreamingHandler.schedule_stream_failure_logging( + litellm_logging_obj=litellm_logging_obj, + endpoint_type=endpoint_type, + request_body=request_body, + raw_bytes=raw_bytes, + exception=AnthropicError(status_code=error_status_code, message=error_message), + stream_context=PassThroughStreamContext( + passthrough_success_handler_obj=passthrough_success_handler_obj, + url_route=url_route, + start_time=start_time, + ), + ) + return try: # TinyFish billing is owned by the detached poller; the $0 fallback below is only for streams with no run_id if endpoint_type == EndpointType.TINYFISH: