From 70ec31e23b4096e89715426007e8e77b384cb2ac Mon Sep 17 00:00:00 2001 From: sanyamk23 Date: Fri, 3 Jul 2026 00:47:27 +0530 Subject: [PATCH 1/3] fix(bedrock-mantle): use correct SigV4 signing service name The bedrock-mantle endpoint requires SigV4 signing with service_name="bedrock-mantle", not "bedrock". Using the wrong service name causes IAM permission errors because AWS evaluates the request against the bedrock service namespace instead of bedrock-mantle. Fixed in two locations: - bedrock_mantle/common_utils.py: standalone Mantle provider - bedrock/chat/mantle/transformation.py: added sign_request override to AmazonMantleConfig The base_invoke_transformation.py sign_request is left unchanged as it serves regular Bedrock requests that correctly use service_name="bedrock". Fixes #31475, #31113, #31196 --- .../bedrock/chat/mantle/transformation.py | 24 +++++++++++++++++++ litellm/llms/bedrock_mantle/common_utils.py | 4 ++-- ...bedrock_mantle_responses_transformation.py | 10 ++++---- .../test_bedrock_mantle_transformation.py | 10 ++++---- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/litellm/llms/bedrock/chat/mantle/transformation.py b/litellm/llms/bedrock/chat/mantle/transformation.py index b293d6ddf7f1..eceeb7e87077 100644 --- a/litellm/llms/bedrock/chat/mantle/transformation.py +++ b/litellm/llms/bedrock/chat/mantle/transformation.py @@ -51,6 +51,30 @@ def get_complete_url( region=region, ) + def sign_request( + self, + headers: dict, + optional_params: dict, + request_data: dict, + api_base: str, + api_key: Optional[str] = None, + model: Optional[str] = None, + stream: Optional[bool] = None, + fake_stream: Optional[bool] = None, + ) -> tuple[dict, Optional[bytes]]: + """Sign request with bedrock-mantle service name instead of bedrock.""" + return self._sign_request( + service_name="bedrock-mantle", + headers=headers, + optional_params=optional_params, + request_data=request_data, + api_base=api_base, + api_key=api_key, + model=model, + stream=stream, + fake_stream=fake_stream, + ) + def validate_environment( self, headers: dict, diff --git a/litellm/llms/bedrock_mantle/common_utils.py b/litellm/llms/bedrock_mantle/common_utils.py index d3c0af7f9323..c5ca3c3a3e6f 100644 --- a/litellm/llms/bedrock_mantle/common_utils.py +++ b/litellm/llms/bedrock_mantle/common_utils.py @@ -3,7 +3,7 @@ Mantle authenticates with a Bearer token when one is available (litellm_params.api_key, BEDROCK_MANTLE_API_KEY, or the standard AWS_BEARER_TOKEN_BEDROCK); otherwise it falls back to AWS SigV4 (service -"bedrock") over the standard credential chain (IAM role / access key / profile / +"bedrock-mantle") over the standard credential chain (IAM role / access key / profile / web identity). The Chat Completions and Responses backends share this behaviour through BedrockMantleAuthMixin so the two paths can never drift apart. @@ -83,7 +83,7 @@ def sign_request( headers = {k: v for k, v in headers.items() if k.lower() != "authorization"} try: return self._aws_signer._sign_request( - service_name="bedrock", + service_name="bedrock-mantle", headers=headers, optional_params=optional_params, request_data=request_data, diff --git a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py index bea979aec64a..a4f1bdd9eda0 100644 --- a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py +++ b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py @@ -1211,7 +1211,7 @@ def test_access_key_produces_sigv4_headers(self, monkeypatch): ) assert headers["Authorization"].startswith("AWS4-HMAC-SHA256") assert "Credential=AKIAEXAMPLE/" in headers["Authorization"] - assert "/us-east-2/bedrock/aws4_request" in headers["Authorization"] + assert "/us-east-2/bedrock-mantle/aws4_request" in headers["Authorization"] assert "X-Amz-Date" in headers assert headers["X-Amz-Security-Token"] == "session-token-test" assert signed_body == b'{"input": "hi"}' @@ -1250,7 +1250,7 @@ def test_assume_role_path_produces_sigv4_headers(self, monkeypatch): assert call["aws_role_name"] == "arn:aws:iam::000000000000:role/test-role" assert call["aws_session_name"] == "litellm-test" assert headers["Authorization"].startswith("AWS4-HMAC-SHA256") - assert "/us-east-2/bedrock/aws4_request" in headers["Authorization"] + assert "/us-east-2/bedrock-mantle/aws4_request" in headers["Authorization"] def test_signed_body_matches_final_data_after_normalize(self, monkeypatch): """Core regression: the signed bytes must equal the bytes actually sent. @@ -1300,7 +1300,7 @@ def test_region_comes_from_optional_params(self, monkeypatch): api_base="https://bedrock-mantle.eu-west-1.api.aws/openai/v1/responses", api_key=None, ) - assert "/eu-west-1/bedrock/aws4_request" in headers["Authorization"] + assert "/eu-west-1/bedrock-mantle/aws4_request" in headers["Authorization"] def test_url_region_and_sigv4_region_agree_from_litellm_params(self, monkeypatch): """Adversarial-review regression: a caller-supplied aws_region_name (no region @@ -1334,7 +1334,7 @@ def test_url_region_and_sigv4_region_agree_from_litellm_params(self, monkeypatch api_base=url, api_key=None, ) - assert "/ap-southeast-2/bedrock/aws4_request" in headers["Authorization"] + assert "/ap-southeast-2/bedrock-mantle/aws4_request" in headers["Authorization"] def test_injected_default_region_base_does_not_override_aws_region_name( self, monkeypatch @@ -1372,7 +1372,7 @@ def test_injected_default_region_base_does_not_override_aws_region_name( api_base=url, api_key=None, ) - assert "/us-east-2/bedrock/aws4_request" in headers["Authorization"] + assert "/us-east-2/bedrock-mantle/aws4_request" in headers["Authorization"] assert "us-east-1" not in headers["Authorization"] def test_custom_proxy_host_is_preserved(self, monkeypatch): diff --git a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py index 275fb460b9f7..dd39089f2f8e 100644 --- a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py +++ b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py @@ -329,7 +329,7 @@ def test_no_bearer_signs_with_sigv4(self, monkeypatch): assert headers["Authorization"].startswith("AWS4-HMAC-SHA256") assert "Credential=AKIAEXAMPLE/" in headers["Authorization"] - assert "/us-east-2/bedrock/aws4_request" in headers["Authorization"] + assert "/us-east-2/bedrock-mantle/aws4_request" in headers["Authorization"] assert headers["X-Amz-Security-Token"] == "session-token-test" assert json.loads(signed_body) == { "model": "openai.gpt-oss-120b", @@ -364,7 +364,7 @@ def test_sigv4_region_resolved_from_api_base_host(self, monkeypatch): api_key=None, ) - assert "/eu-west-1/bedrock/aws4_request" in headers["Authorization"] + assert "/eu-west-1/bedrock-mantle/aws4_request" in headers["Authorization"] def test_sigv4_scope_matches_api_base_when_aws_region_name_disagrees( self, monkeypatch @@ -399,8 +399,8 @@ def test_sigv4_scope_matches_api_base_when_aws_region_name_disagrees( api_key=None, ) - assert "/eu-west-1/bedrock/aws4_request" in headers["Authorization"] - assert "/us-west-2/bedrock/aws4_request" not in headers["Authorization"] + assert "/eu-west-1/bedrock-mantle/aws4_request" in headers["Authorization"] + assert "/us-west-2/bedrock-mantle/aws4_request" not in headers["Authorization"] def test_no_bearer_and_no_credentials_raises_value_error(self, monkeypatch): from unittest.mock import MagicMock @@ -486,7 +486,7 @@ def mock_post(self, url, data=None, headers=None, **kwargs): assert len(requests) == 1 authorization = requests[0]["headers"]["Authorization"] assert authorization.startswith("AWS4-HMAC-SHA256") - assert "/us-east-2/bedrock/aws4_request" in authorization + assert "/us-east-2/bedrock-mantle/aws4_request" in authorization assert requests[0]["url"].startswith("https://bedrock-mantle.us-east-2.api.aws") From ff6597c60d1c02de66dad0baf9166d6f1f81a45c Mon Sep 17 00:00:00 2001 From: sanyamk23 Date: Mon, 3 Aug 2026 02:51:32 +0530 Subject: [PATCH 2/3] fix(bedrock-mantle): use PEP 604 X | None syntax to fix UP045 lint budget --- litellm/llms/bedrock/chat/mantle/transformation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/llms/bedrock/chat/mantle/transformation.py b/litellm/llms/bedrock/chat/mantle/transformation.py index eceeb7e87077..3fc0807cb297 100644 --- a/litellm/llms/bedrock/chat/mantle/transformation.py +++ b/litellm/llms/bedrock/chat/mantle/transformation.py @@ -57,11 +57,11 @@ def sign_request( optional_params: dict, request_data: dict, api_base: str, - api_key: Optional[str] = None, - model: Optional[str] = None, - stream: Optional[bool] = None, - fake_stream: Optional[bool] = None, - ) -> tuple[dict, Optional[bytes]]: + api_key: str | None = None, + model: str | None = None, + stream: bool | None = None, + fake_stream: bool | None = None, + ) -> tuple[dict, bytes | None]: """Sign request with bedrock-mantle service name instead of bedrock.""" return self._sign_request( service_name="bedrock-mantle", From bc3688e8def9805c0c7d96cca440f6fd81bff93e Mon Sep 17 00:00:00 2001 From: sanyamk23 Date: Tue, 4 Aug 2026 21:10:56 +0530 Subject: [PATCH 3/3] fix(bedrock-mantle): suppress LIT001 mutable dict violations in sign_request The sign_request override matches the parent class signature which uses bare dict annotations. Suppress type-discipline budget violations with mutable-ok comments to match the existing pattern. --- litellm/llms/bedrock/chat/mantle/transformation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm/llms/bedrock/chat/mantle/transformation.py b/litellm/llms/bedrock/chat/mantle/transformation.py index 3fc0807cb297..87e2b793a26c 100644 --- a/litellm/llms/bedrock/chat/mantle/transformation.py +++ b/litellm/llms/bedrock/chat/mantle/transformation.py @@ -53,15 +53,15 @@ def get_complete_url( def sign_request( self, - headers: dict, - optional_params: dict, - request_data: dict, + headers: dict, # mutable-ok: matches parent class signature + optional_params: dict, # mutable-ok: matches parent class signature + request_data: dict, # mutable-ok: matches parent class signature api_base: str, api_key: str | None = None, model: str | None = None, stream: bool | None = None, fake_stream: bool | None = None, - ) -> tuple[dict, bytes | None]: + ) -> tuple[dict, bytes | None]: # mutable-ok: matches parent class signature """Sign request with bedrock-mantle service name instead of bedrock.""" return self._sign_request( service_name="bedrock-mantle",