diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 18e9deb53b..bfb25416cf 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -87,7 +87,7 @@ class BaseAWSLLM: """ import litellm from litellm.secret_managers.main import str_to_bool - + # Check environment variable first (highest priority) ssl_verify = os.getenv("SSL_VERIFY", litellm.ssl_verify) @@ -1232,15 +1232,20 @@ class BaseAWSLLM: else: headers = {"Content-Type": "application/json"} + aws_signature_headers = self._filter_headers_for_aws_signature(headers) request = AWSRequest( method="POST", url=api_base, data=json.dumps(request_data), - headers=headers, + headers=aws_signature_headers, ) sigv4.add_auth(request) request_headers_dict = dict(request.headers) + # Add back original headers after signing. Only headers in SignedHeaders + # are integrity-protected; forwarded headers (x-forwarded-*) must remain unsigned. + for header_name, header_value in headers.items(): + request_headers_dict[header_name] = header_value if ( headers is not None and "Authorization" in headers ): # prevent sigv4 from overwriting the auth header diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index 81ba717ab3..c9677cf9ed 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -729,8 +729,6 @@ class BedrockLLM(BaseAWSLLM): client: Optional[Union[AsyncHTTPHandler, HTTPHandler]] = None, ) -> Union[ModelResponse, CustomStreamWrapper]: try: - from botocore.auth import SigV4Auth - from botocore.awsrequest import AWSRequest from botocore.credentials import Credentials except ImportError: raise ImportError("Missing boto3 to call bedrock. Run 'pip install boto3'.") @@ -808,8 +806,6 @@ class BedrockLLM(BaseAWSLLM): endpoint_url = f"{endpoint_url}/model/{modelId}/invoke" proxy_endpoint_url = f"{proxy_endpoint_url}/model/{modelId}/invoke" - sigv4 = SigV4Auth(credentials, "bedrock", aws_region_name) - prompt, chat_history = self.convert_messages_to_prompt( model, messages, provider, custom_prompt_dict ) @@ -970,15 +966,14 @@ class BedrockLLM(BaseAWSLLM): headers = {"Content-Type": "application/json"} if extra_headers is not None: headers = {"Content-Type": "application/json", **extra_headers} - request = AWSRequest( - method="POST", url=endpoint_url, data=data, headers=headers + prepped = self.get_request_headers( + credentials=credentials, + aws_region_name=aws_region_name, + extra_headers=extra_headers, + endpoint_url=endpoint_url, + data=data, + headers=headers, ) - sigv4.add_auth(request) - if ( - extra_headers is not None and "Authorization" in extra_headers - ): # prevent sigv4 from overwriting the auth header - request.headers["Authorization"] = extra_headers["Authorization"] - prepped = request.prepare() ## LOGGING logging_obj.pre_call( diff --git a/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py b/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py index 155af6b6a9..3a9663f88b 100644 --- a/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py +++ b/tests/pass_through_unit_tests/test_bedrock_anthropic_messages_test.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import AsyncIterator, Dict, Any import asyncio import unittest.mock -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock import pytest from litellm.router import Router @@ -98,3 +98,44 @@ async def test_anthropic_messages_bedrock_converse_with_thinking(): # Verify response INSTANCE_BASE_ANTHROPIC_MESSAGES_TEST._validate_response(response) + + +@pytest.mark.asyncio +async def test_should_not_fail_with_forwarded_headers_bedrock_invoke_messages(): + """ + E2E test for Bedrock invoke messages with header forwarding enabled. + This calls the real Bedrock endpoint (no mocks) and should not raise + SigV4 signature mismatch errors when forwarded headers are present. + """ + router = Router( + model_list=[ + { + "model_name": "claude-sonnet-4-5-20250929", + "litellm_params": { + "model": "bedrock/invoke/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "aws_region_name": os.getenv("AWS_REGION_NAME"), + }, + } + ] + ) + + forwarded_headers = { + "x-forwarded-for": "10.11.232.194", + "x-forwarded-port": "443", + "x-forwarded-proto": "https", + "x-app": "cli", + } + + response = await router.aanthropic_messages( + messages=[{"role": "user", "content": "hi"}], + model="claude-sonnet-4-5-20250929", + max_tokens=5, + stream=False, + headers=forwarded_headers, # simulates forward_client_headers_to_llm_api + aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), + aws_region_name=os.getenv("AWS_REGION_NAME"), + ) + print("INVOKE API RESPONSE: ", response) + + INSTANCE_BASE_ANTHROPIC_MESSAGES_TEST._validate_response(response)