Skip to content

refactor(bedrock-mantle): align SigV4 signing service name with canonical "bedrock-mantle" - #31476

Open
laiweihwa wants to merge 2 commits into
BerriAI:litellm_internal_stagingfrom
laiweihwa:fix/bedrock-mantle-sigv4-service-name
Open

refactor(bedrock-mantle): align SigV4 signing service name with canonical "bedrock-mantle"#31476
laiweihwa wants to merge 2 commits into
BerriAI:litellm_internal_stagingfrom
laiweihwa:fix/bedrock-mantle-sigv4-service-name

Conversation

@laiweihwa

@laiweihwa laiweihwa commented Jun 26, 2026

Copy link
Copy Markdown

Relevant issues

Related: #31475, #31113, #31196, #30714

Type

Refactor / Naming Alignment

Changes

The bedrock-mantle endpoint (bedrock-mantle.{region}.api.aws) declares bedrock-mantle as its canonical SigV4 signing service name. However, the endpoint's credential-scope authorizer also accepts bedrock (the legacy name used by bedrock-runtime), so the previous signing was not broken -- both return HTTP 200 for real inference. This is a naming alignment, not a bug fix.

This PR aligns all Bedrock Mantle signing paths with the canonical service name "bedrock-mantle" to match the service's own IAM namespace and the naming convention used by the AWS-managed AmazonBedrockMantle*Access policies.

Evidence: this is cosmetic, not a fix

Tested with a role scoped to only AmazonBedrockMantleInferenceAccess (bedrock-mantle:* actions) -- the narrowest possible permission set:

POST https://bedrock-mantle.us-east-1.api.aws/v1/chat/completions
  (role: only bedrock-mantle:* via AmazonBedrockMantleInferenceAccess)

  service="bedrock-mantle"  ->  HTTP 200  (real completion)
  service="bedrock"         ->  HTTP 200  (real completion)
  service="mantle"          ->  HTTP 401  "Credential should be scoped to correct service: 'bedrock-mantle'."

Both bedrock and bedrock-mantle authenticate successfully, even with restricted IAM. The endpoint rejects only genuinely unrecognized names like mantle. The credential-scope service name affects signature verification routing, not IAM action authorization.

Why align anyway?
  • bedrock-mantle is the canonical name declared by the endpoint itself (the 401 message names it)
  • It matches the IAM action namespace (bedrock-mantle:*) and ARN namespace (arn:aws:bedrock-mantle:*)
  • It follows the pattern of bedrock-agentcore (owns its IAM namespace, signs as itself)
  • Forward-correctness: if AWS ever tightens the authorizer to reject bedrock, this code is already correct

Code changes

  1. litellm/llms/bedrock/base_aws_llm.py -- add "bedrock-mantle" to the Literal on _sign_request (and correct the docstring return type to Optional[bytes]).
  2. litellm/llms/bedrock/chat/mantle/transformation.py -- AmazonMantleConfig.sign_request override signing with bedrock-mantle (the bedrock/mantle/ chat route).
  3. litellm/llms/bedrock/messages/mantle_transformation.py -- AmazonMantleMessagesConfig.sign_request override signing with bedrock-mantle (the bedrock/mantle/ messages route).
  4. litellm/llms/bedrock_mantle/common_utils.py -- BedrockMantleAuthMixin.sign_request signs with bedrock-mantle (the standalone bedrock_mantle/ provider, shared by chat + responses).
  5. Docstrings in the bedrock_mantle/ module updated to say bedrock-mantle.
  6. Tests: all SigV4 credential-scope assertions updated from /bedrock/aws4_request to /bedrock-mantle/aws4_request; added unit tests asserting the signing service, plus a real-signature test asserting the actual credential scope for both bedrock/mantle/ configs.
Full diff
diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py
index b71f37023e..27a43f76aa 100644
--- a/litellm/llms/bedrock/base_aws_llm.py
+++ b/litellm/llms/bedrock/base_aws_llm.py
@@ -1554,6 +1554,7 @@ class BaseAWSLLM:
         self,
         service_name: Literal[
             "bedrock",
+            "bedrock-mantle",
             "sagemaker",
             "bedrock-agentcore",
             "s3vectors",
@@ -1572,7 +1573,7 @@ class BaseAWSLLM:
         Sign a request for Bedrock or Sagemaker
 
         Returns:
-            Tuple[dict, Optional[str]]: A tuple containing the headers and the json str body of the request
+            Tuple[dict, Optional[bytes]]: A tuple containing the headers and the json str body of the request
         """
         if api_key is not None:
             aws_bearer_token: Optional[str] = api_key
diff --git a/litellm/llms/bedrock/chat/mantle/transformation.py b/litellm/llms/bedrock/chat/mantle/transformation.py
index 93306025b0..eb3612b009 100644
--- a/litellm/llms/bedrock/chat/mantle/transformation.py
+++ b/litellm/llms/bedrock/chat/mantle/transformation.py
@@ -33,6 +33,29 @@ class AmazonMantleConfig(AmazonAnthropicClaudeConfig):
     Usage: model="bedrock/mantle/anthropic.claude-mythos-preview"
     """
 
+    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]]:
+        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 get_complete_url(
         self,
         api_base: Optional[str],
diff --git a/litellm/llms/bedrock/messages/mantle_transformation.py b/litellm/llms/bedrock/messages/mantle_transformation.py
index 94e7f90b71..5758359ead 100644
--- a/litellm/llms/bedrock/messages/mantle_transformation.py
+++ b/litellm/llms/bedrock/messages/mantle_transformation.py
@@ -30,6 +30,29 @@ class AmazonMantleMessagesConfig(AmazonAnthropicClaudeMessagesConfig):
     model ID in the request body (unlike Bedrock Invoke which puts it in the URL).
     """
 
+    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]]:
+        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 get_complete_url(
         self,
         api_base: Optional[str],
diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py
index f688cea10f..d7e3b9e2a7 100644
--- a/litellm/llms/bedrock_mantle/chat/transformation.py
+++ b/litellm/llms/bedrock_mantle/chat/transformation.py
@@ -6,7 +6,7 @@ API docs: https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.ht
 Base URL: https://bedrock-mantle.{region}.api.aws/v1
 Auth: Bearer token (litellm_params.api_key, BEDROCK_MANTLE_API_KEY, or the
       standard AWS_BEARER_TOKEN_BEDROCK) when present; otherwise AWS SigV4
-      (service "bedrock") over the standard credential chain. See
+      (service "bedrock-mantle") over the standard credential chain. See
       BedrockMantleAuthMixin in common_utils.
 """
 
diff --git a/litellm/llms/bedrock_mantle/common_utils.py b/litellm/llms/bedrock_mantle/common_utils.py
index d517ab940c..cda409a20d 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.
 
@@ -90,7 +90,7 @@ class BedrockMantleAuthMixin:
             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/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py
index 2e30f85fd0..72a99ba5e7 100644
--- a/litellm/llms/bedrock_mantle/responses/transformation.py
+++ b/litellm/llms/bedrock_mantle/responses/transformation.py
@@ -10,7 +10,7 @@ only the endpoint URL and authentication.
 
 Auth: Bearer token (BEDROCK_MANTLE_API_KEY or the standard
 AWS_BEARER_TOKEN_BEDROCK, or litellm_params.api_key) when present; otherwise
-AWS SigV4 (service name "bedrock") using the standard credential chain (IAM
+AWS SigV4 (service name "bedrock-mantle") using the standard credential chain (IAM
 role / access key / profile / web identity), signed via the shared
 BaseAWSLLM._sign_request after the request body is finalized.
 """
diff --git a/tests/test_litellm/llms/bedrock/test_mantle.py b/tests/test_litellm/llms/bedrock/test_mantle.py
index f7f8f582ab..aa1b30c172 100644
--- a/tests/test_litellm/llms/bedrock/test_mantle.py
+++ b/tests/test_litellm/llms/bedrock/test_mantle.py
@@ -347,3 +347,88 @@ async def test_mantle_anthropic_messages_routes_to_vpc_api_base():
     assert len(urls) == 1
     assert urls[0] == f"{_VPC_ENDPOINT}/anthropic/v1/messages"
     assert "api.aws" not in urls[0]
+
+
+def test_mantle_chat_config_signs_with_bedrock_mantle_service():
+    """The bedrock/mantle/ chat route must SigV4-sign with service 'bedrock-mantle'."""
+    config = AmazonMantleConfig()
+    signed_service = None
+
+    def capture_sign(self, *, service_name, **kwargs):
+        nonlocal signed_service
+        signed_service = service_name
+        return {"Authorization": "AWS4-HMAC-SHA256 ..."}, b"{}"
+
+    with patch(
+        "litellm.llms.bedrock.base_aws_llm.BaseAWSLLM._sign_request",
+        capture_sign,
+    ):
+        config.sign_request(
+            headers={"Content-Type": "application/json"},
+            optional_params={
+                "aws_access_key_id": "fake",
+                "aws_secret_access_key": "fake",
+                "aws_region_name": "us-east-1",
+            },
+            request_data={"model": "anthropic.claude-opus-4-8", "messages": []},
+            api_base="https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
+        )
+
+    assert signed_service == "bedrock-mantle"
+
+
+def test_mantle_messages_config_signs_with_bedrock_mantle_service():
+    """The bedrock/mantle/ messages route must SigV4-sign with service 'bedrock-mantle'."""
+    config = AmazonMantleMessagesConfig()
+    signed_service = None
+
+    def capture_sign(self, *, service_name, **kwargs):
+        nonlocal signed_service
+        signed_service = service_name
+        return {"Authorization": "AWS4-HMAC-SHA256 ..."}, b"{}"
+
+    with patch(
+        "litellm.llms.bedrock.base_aws_llm.BaseAWSLLM._sign_request",
+        capture_sign,
+    ):
+        config.sign_request(
+            headers={"Content-Type": "application/json"},
+            optional_params={
+                "aws_access_key_id": "fake",
+                "aws_secret_access_key": "fake",
+                "aws_region_name": "us-east-1",
+            },
+            request_data={"model": "anthropic.claude-opus-4-8", "messages": []},
+            api_base="https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
+        )
+
+    assert signed_service == "bedrock-mantle"
+
+
+def test_mantle_configs_sign_real_sigv4_scope_is_bedrock_mantle(monkeypatch):
+    """Both bedrock/mantle/ configs must produce a *real* SigV4 signature whose
+    credential scope names service 'bedrock-mantle' (not 'bedrock').
+
+    Unlike the two tests above this does NOT mock _sign_request: it drives the
+    real botocore SigV4Auth with fake static creds (offline, no network), so a
+    regression in the signed service name would change the credential scope and
+    fail here. Cred setup mirrors test_no_bearer_signs_with_sigv4 in
+    tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py.
+    """
+    # No Bearer token -> _sign_request must take the SigV4 path, not Bearer auth.
+    monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
+
+    for config in (AmazonMantleConfig(), AmazonMantleMessagesConfig()):
+        headers, _ = config.sign_request(
+            headers={"Content-Type": "application/json"},
+            optional_params={
+                "aws_access_key_id": "AKIAEXAMPLE",
+                "aws_secret_access_key": "c2VjcmV0LXRlc3Qtc2VjcmV0LXRlc3Qtc2VjcmV0",
+                "aws_region_name": "us-east-1",
+            },
+            request_data={"model": "anthropic.claude-opus-4-8", "messages": []},
+            api_base="https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
+        )
+
+        assert headers["Authorization"].startswith("AWS4-HMAC-SHA256")
+        assert "/us-east-1/bedrock-mantle/aws4_request" in headers["Authorization"]
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 94efc7c51e..c99ba034b1 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
@@ -923,7 +923,7 @@ class TestBedrockMantleResponsesSigV4:
         )
         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"}'
@@ -962,7 +962,7 @@ class TestBedrockMantleResponsesSigV4:
         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.
@@ -1012,7 +1012,7 @@ class TestBedrockMantleResponsesSigV4:
             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
@@ -1046,7 +1046,7 @@ class TestBedrockMantleResponsesSigV4:
             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
@@ -1084,7 +1084,7 @@ class TestBedrockMantleResponsesSigV4:
             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 275fb460b9..dd39089f2f 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 @@ class TestBedrockMantleChatAuth:
 
         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 @@ class TestBedrockMantleChatAuth:
             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 @@ class TestBedrockMantleChatAuth:
             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 @@ class TestBedrockMantleChatAuth:
         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")
 
 

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests (157 mantle tests pass; ruff strict gate passes)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review and received a Confidence Score of at least 4/5

Screenshots / Proof of Fix

All bedrock-mantle unit tests pass and the ruff strict gate is green:

tests/test_litellm/llms/bedrock/test_mantle.py ......................... 24
tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py 47
tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py 86
157 passed

scripts/ruff_strict_gate.py -> OK: every strict rule is within its codebase ceiling

@CLAassistant

CLAassistant commented Jun 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.66667% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/bedrock/base_aws_llm.py 73.46% 13 Missing ⚠️
litellm/llms/bedrock_mantle/chat/transformation.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@laiweihwa

Copy link
Copy Markdown
Author

@greptileai review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@laiweihwa
laiweihwa marked this pull request as ready for review June 26, 2026 22:54

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@laiweihwa

Copy link
Copy Markdown
Author

Re: Greptile review requirement

I requested a Greptile review (see @greptileai comment above), but BerriAI's Greptile integration has exhausted its free trial -- the bot responded with "Your free trial has ended" instead of producing a Confidence Score.

Per the repo's own triage logic, a missing Greptile score is not a merge blocker (the automation treats None as passing). The PR is ready for human review whenever a maintainer is available.

@laiweihwa

Copy link
Copy Markdown
Author

I think should go with @mateo-berri 's work in #30714

@laiweihwa

Copy link
Copy Markdown
Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR aligns the SigV4 signing service name for all Bedrock Mantle request paths from the legacy "bedrock" string to the canonical "bedrock-mantle" string, matching the service's IAM namespace and the 401 error message the endpoint itself emits. Per the PR's own evidence, the AWS authorizer accepts both names (only "mantle" is rejected), so this is a forwards-correctness alignment rather than a bug fix.

  • Core change: BedrockMantleAuthMixin.sign_request in common_utils.py switches service_name from "bedrock" to "bedrock-mantle", shared by both the chat and responses backends. New sign_request overrides in AmazonMantleConfig and AmazonMantleMessagesConfig apply the same alignment to the bedrock/mantle/ routing path.
  • Type system: "bedrock-mantle" is added to the Literal constraint on BaseAWSLLM._sign_request; the docstring return type is corrected from Optional[str] to Optional[bytes].
  • Tests: Existing credential-scope assertions updated from /bedrock/aws4_request to /bedrock-mantle/aws4_request; three new tests added including one real botocore SigV4 path using offline fake credentials for strong regression coverage.

Confidence Score: 5/5

Safe to merge — the AWS endpoint accepts both the old and new service names, so no existing users are broken, and the new name is the one the endpoint itself declares as canonical.

All four signing paths are updated in lock-step, the Literal type guard prevents typos at static-analysis time, and the tests include a real botocore SigV4 path (offline, no network) that would catch any regression in the credential-scope string. The large formatting diff in base_aws_llm.py is cosmetic and carries no logic risk.

No files require special attention.

Important Files Changed

Filename Overview
litellm/llms/bedrock/base_aws_llm.py Adds "bedrock-mantle" to the Literal type for _sign_request's service_name parameter; corrects the docstring return type from Optional[str] to Optional[bytes]. Remaining diff is pure formatting with no logic changes.
litellm/llms/bedrock/chat/mantle/transformation.py Adds sign_request override to AmazonMantleConfig that hardwires service_name="bedrock-mantle" when delegating to _sign_request. Change is correct and properly narrows the signing scope from the parent's default.
litellm/llms/bedrock/messages/mantle_transformation.py Mirror of the chat transformation change — adds sign_request override to AmazonMantleMessagesConfig with service_name="bedrock-mantle". Symmetric and correct.
litellm/llms/bedrock_mantle/common_utils.py Core signing change: BedrockMantleAuthMixin.sign_request switches service_name from "bedrock" to "bedrock-mantle". This is the shared auth path used by both the chat and responses backends.
litellm/llms/bedrock_mantle/chat/transformation.py Docstring updated from service "bedrock" to "bedrock-mantle"; minor formatting cleanup. No logic changes.
litellm/llms/bedrock_mantle/responses/transformation.py Docstring updated from service "bedrock" to "bedrock-mantle"; minor formatting cleanup. No logic changes.
tests/test_litellm/llms/bedrock/test_mantle.py Existing credential-scope assertions updated from /bedrock/ to /bedrock-mantle/. Three new tests added including one real botocore SigV4 test using offline fake credentials. Test additions strengthen coverage without introducing real network calls.
tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py Five credential-scope assertions updated from /bedrock/ to /bedrock-mantle/. Remaining diff is formatting cleanup. Assertion updates are consistent with the production change and do not weaken coverage.
tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_transformation.py Five credential-scope assertions updated from /bedrock/ to /bedrock-mantle/. Remaining diff is formatting cleanup. Changes correctly track the production change.

Reviews (2): Last reviewed commit: "style: apply ruff format to fix lint che..." | Re-trigger Greptile

@6matt

6matt commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@laiweihwa thanks for digging into this. Before this lands I wanted to verify the premise against the live endpoint, because I don't think the current bedrock signing is actually broken.

I ran the full SigV4 service-name matrix against bedrock-mantle.us-east-1.api.aws with real IAM credentials over SSO and no bearer token, so the SigV4 path is genuinely exercised rather than falling back to Bearer auth. Every 200 below returned a real, billed completion.

Signed credential scope /openai/v1/responses /v1/chat/completions
.../us-east-1/bedrock/aws4_request 200 200
.../us-east-1/bedrock-mantle/aws4_request 200 200
.../us-east-1/mantle/aws4_request 401 401

The 401 in the description was produced by signing with service="mantle", which is a name nothing in the codebase ever uses. The shipped code signs with service="bedrock", and that authenticates fine on both surfaces, including the exact /v1/chat/completions path. The endpoint's credential-scope authorizer accepts a set of names that includes the legacy bedrock (the same way the rest of the Bedrock family signs the bedrock-runtime host as bedrock), and the "Credential should be scoped to correct service: 'bedrock-mantle'" message just reports the canonical name rather than meaning it is the only one accepted.

So this reads as a safe naming cleanup rather than a fix for a broken path. Signing as bedrock-mantle also returns 200, so the change won't break callers, but signing as bedrock was not failing in the first place. I'd suggest softening the "this breaks IAM roles scoped to bedrock-mantle:* only" framing, since the live behavior doesn't back the idea that the current signing is rejected. Aligning the signing name with the canonical service and the managed-policy namespace is defensible on its own, just not as an outage fix.

One scenario these tests don't cover: a role scoped strictly to bedrock-mantle:*. The matrix confirms the endpoint accepts a bedrock-scoped signature, but not whether such a narrowly scoped role would be IAM-denied when signing as bedrock. Mechanically that seems unlikely, since the credential-scope service name drives signature verification and region binding, not the IAM action namespace the service authorizes the operation against. A request from a strictly bedrock-mantle:*-scoped role, signed as bedrock, returning a 200 would settle it; that is the one piece of evidence that would actually distinguish "broken" from "cosmetic" here

…ical "bedrock-mantle"

The bedrock-mantle endpoint declares "bedrock-mantle" as its canonical SigV4
service name (signing with "mantle" returns HTTP 401 "Credential should be
scoped to correct service: 'bedrock-mantle'"). However, the endpoint also
accepts "bedrock" in the credential scope (both return HTTP 200 for real
inference), so the previous signing was not broken.

This change aligns the SigV4 credential scope with the canonical service name
and the bedrock-mantle IAM namespace (bedrock-mantle:*, per the AWS-managed
AmazonBedrockMantle*Access policies). Verified with a role scoped to only
AmazonBedrockMantleInferenceAccess: both service="bedrock" and
service="bedrock-mantle" produce HTTP 200 completions, confirming this is a
naming alignment rather than a fix for broken auth.

Changes:
- Add "bedrock-mantle" to the allowed service Literal in BaseAWSLLM._sign_request.
- Sign with service_name="bedrock-mantle" on all three signing surfaces:
  - AmazonMantleConfig.sign_request (bedrock/mantle/ chat route)
  - AmazonMantleMessagesConfig.sign_request (bedrock/mantle/ messages route)
  - BedrockMantleAuthMixin.sign_request (bedrock_mantle/ standalone provider)
- Correct stale docstrings that still named the SigV4 service "bedrock"
  (bedrock_mantle/common_utils.py, bedrock_mantle/chat/transformation.py) and
  fix the _sign_request docstring return type to Tuple[dict, Optional[bytes]].
- Use the builtin tuple return annotation on the mantle sign_request overrides
  to satisfy the strict ruff UP006 gate.
- Update existing test assertions to /bedrock-mantle/aws4_request and add a
  real-signature SigV4 test (no _sign_request mock) asserting both bedrock/mantle/
  configs scope the credential to bedrock-mantle.

Related: BerriAI#31475, BerriAI#31113, BerriAI#31196, BerriAI#30714
@laiweihwa
laiweihwa force-pushed the fix/bedrock-mantle-sigv4-service-name branch from d1520db to 038fbd3 Compare July 3, 2026 14:52
@laiweihwa laiweihwa changed the title fix(bedrock-mantle): use correct SigV4 signing service name "bedrock-mantle" refactor(bedrock-mantle): align SigV4 signing service name with canonical "bedrock-mantle" Jul 3, 2026
@laiweihwa

laiweihwa commented Jul 3, 2026

Copy link
Copy Markdown
Author

Thanks for the review, @6matt .You're right -- I ran the decisive test you proposed and it confirms your assessment.

I created a role with only AmazonBedrockMantleInferenceAccess attached (bedrock-mantle:* actions, nothing else), assumed it, and sent real inference requests:

POST https://bedrock-mantle.us-east-1.api.aws/v1/chat/completions
  (role: only bedrock-mantle:* via AmazonBedrockMantleInferenceAccess)

  service="bedrock-mantle"  ->  HTTP 200  (real completion)
  service="bedrock"         ->  HTTP 200  (real completion)
  service="mantle"          ->  HTTP 401  "Credential should be scoped to correct service: 'bedrock-mantle'."

Both bedrock and bedrock-mantle authenticate and authorize successfully, even with the narrowest possible permission set. The credential-scope service name affects signature routing, not IAM action authorization. The old code was not broken.

I've reframed the PR as a naming alignment (commit prefix changed from fix to refactor, description updated, Fixes #31475 changed to Related). The rationale is forward-correctness: bedrock-mantle is the canonical name declared by the endpoint, matches the IAM/ARN namespace, and follows the bedrock-agentcore pattern. If AWS ever tightens the authorizer to strict-match, this code is already correct.

The earlier "bug fix" framing was based on a flawed probe methodology -- we tested only empty-body requests (to avoid billing) and over-interpreted the 401 error message as exclusionary rather than canonical. Your full-inference test exposed the gap. Thanks for the diligence.

@laiweihwa

Copy link
Copy Markdown
Author

should be good to go for a re-review when you have a moment, @6matt

@laiweihwa

Copy link
Copy Markdown
Author

@greptileai review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants