diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index ab4e5c25498..a3e53349af2 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -48,6 +48,7 @@ from litellm.types.utils import GenericStreamingChunk as GChunk from ..common_utils import ( BedrockError, BedrockEventStreamResponseDict, + bedrock_answered_json, bedrock_error_in_success_body, bedrock_event_stream_header, bedrock_event_stream_response, @@ -193,10 +194,6 @@ def response_format_schema_from_request(data: str, json_mode: bool | None) -> Re return JsonModeRequest.model_validate(body).tool_schema() -def _answered_json(response: httpx.Response) -> bool: - return "application/json" in (response.headers.get("content-type") or "") - - def _stream_decoder( bedrock_invoke_provider: litellm.BEDROCK_INVOKE_PROVIDERS_LITERAL | None, *, @@ -264,7 +261,7 @@ async def make_call( response=response, ) - if _answered_json(response): + if bedrock_answered_json(response): await response.aread() success_body_error: Final = bedrock_error_in_success_body(response) if success_body_error is not None: @@ -366,7 +363,7 @@ def make_sync_call( response=response, ) - if _answered_json(response): + if bedrock_answered_json(response): response.read() success_body_error: Final = bedrock_error_in_success_body(response) if success_body_error is not None: diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 98a6b87d0bc..29cbbcfa6d9 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -10,7 +10,7 @@ import json import os import re import warnings -from collections.abc import Iterator, Mapping, Sequence +from collections.abc import AsyncIterator, Iterator, Mapping, Sequence from dataclasses import dataclass from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias @@ -147,6 +147,22 @@ def bedrock_error_in_success_body(response: httpx.Response) -> BedrockError | No ) +def bedrock_answered_json(response: httpx.Response) -> bool: + return "application/json" in (response.headers.get("content-type") or "") + + +async def _json_body_or_bedrock_error(response: httpx.Response) -> AsyncIterator[bytes]: + body: Final = await response.aread() + error: Final = bedrock_error_in_success_body(response) + if error is not None: + raise error + yield body + + +def bedrock_event_stream_bytes(response: httpx.Response) -> AsyncIterator[bytes]: + return _json_body_or_bedrock_error(response) if bedrock_answered_json(response) else response.aiter_bytes() + + _BEDROCK_AWS_AUTH_PARAMETER_KEYS: Final[tuple[str, ...]] = (*AWS_AUTH_PARAM_KEYS, "aws_region_name") diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py index e5d6a168d57..d7eac6f7f8c 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py @@ -39,6 +39,8 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation from litellm.llms.bedrock.common_utils import ( BedrockError, apply_bedrock_invoke_structured_output, + bedrock_error_in_success_body, + bedrock_event_stream_bytes, bedrock_supports_tool_search, ensure_bedrock_anthropic_messages_tool_names, get_anthropic_beta_from_headers, @@ -62,6 +64,7 @@ from litellm.types.llms.anthropic import ( ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER, ANTHROPIC_TOOL_SEARCH_BETA_HEADER, ) +from litellm.types.llms.anthropic_messages.anthropic_response import AnthropicMessagesResponse from litellm.types.llms.bedrock import BedrockInvokeAnthropicMessagesRequest from litellm.types.llms.openai import AllMessageValues from litellm.types.router import GenericLiteLLMParams @@ -868,6 +871,17 @@ class AmazonAnthropicClaudeMessagesConfig( return anthropic_messages_request + def transform_anthropic_messages_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> AnthropicMessagesResponse: + success_body_error: Final = bedrock_error_in_success_body(raw_response) + if success_body_error is not None: + raise success_body_error + return super().transform_anthropic_messages_response(model, raw_response, logging_obj) + def get_async_streaming_response_iterator( self, model: str, @@ -879,7 +893,7 @@ class AmazonAnthropicClaudeMessagesConfig( model=model, ) completion_stream: Final = aws_decoder.aiter_bytes( - httpx_response.aiter_bytes(), response_headers=httpx_response.headers + bedrock_event_stream_bytes(httpx_response), response_headers=httpx_response.headers ) # Convert decoded Bedrock events to Server-Sent Events expected by Anthropic clients. return self.bedrock_sse_wrapper( diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py index 0bd480dcdbc..4ff48a9856a 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py @@ -33,6 +33,7 @@ from litellm.llms.anthropic.messages.native.mid_conversation_system import ( as_system_content_blocks, ) from litellm.llms.bedrock.common_utils import ( + BedrockError, ensure_bedrock_anthropic_messages_tool_names, normalize_custom_field_on_tools, normalize_tool_input_schema_types_for_bedrock_invoke, @@ -4553,3 +4554,42 @@ async def test_bedrock_chat_fallback_to_anthropic_sends_the_tool_bedrock_cleaned (body,) = upstream.anthropic_bodies assert body["tools"] == [{"name": "get_weather", "input_schema": schema, **sent_extras}] + + +_UNKNOWN_OPERATION_BODY: Final = { + "Output": {"__type": "com.amazon.coral.service#UnknownOperationException"}, + "Version": "1.0", +} + + +@pytest.mark.usefixtures("local_model_cost_map") +@pytest.mark.parametrize( + ("stream", "raised"), + [(False, litellm.BadRequestError), (True, BedrockError)], + ids=["non-stream", "stream"], +) +async def test_bedrock_messages_raise_a_400_when_bedrock_answers_200_with_an_unknown_operation_body( + stream: bool, + raised: type[litellm.BadRequestError] | type[BedrockError], +) -> None: + bedrock: Final = ScriptedBedrockInvoke(replies=((200, _UNKNOWN_OPERATION_BODY),)) + + async def _call_and_drain() -> None: + response: Final = await litellm.anthropic_messages( + model="bedrock/invoke/anthropic/us.anthropic.claude-haiku-5-5", + messages=[{"role": "user", "content": "Say hi in three words"}], + max_tokens=20, + stream=stream, + aws_access_key_id="test-access-key", + aws_secret_access_key="test-secret-key", + aws_region_name="us-east-1", + client=AsyncHTTPHandler(transport=httpx.MockTransport(bedrock)), + ) + if isinstance(response, AsyncIterator): + _ = [chunk async for chunk in response] + + with pytest.raises(raised) as error: + await _call_and_drain() + + assert error.value.status_code == 400 + assert "UnknownOperationException" in str(error.value)