diff --git a/litellm/llms/bedrock_mantle/common_utils.py b/litellm/llms/bedrock_mantle/common_utils.py index d877fbb4e09..850738bc320 100644 --- a/litellm/llms/bedrock_mantle/common_utils.py +++ b/litellm/llms/bedrock_mantle/common_utils.py @@ -29,7 +29,7 @@ from litellm.secret_managers.main import get_secret_str BEDROCK_MANTLE_DEFAULT_REGION: Final = "us-east-1" # Standard Mantle host: https://bedrock-mantle..api.aws (group 1 = region). -MANTLE_HOST_RE: Final = re.compile(r"^https?://bedrock-mantle\.([^/.]+)\.api\.aws", re.IGNORECASE) +MANTLE_HOST_RE: Final = re.compile(r"^https?://bedrock-mantle\.([^/.]+)\.api\.aws(?=/|$)", re.IGNORECASE) def resolve_mantle_bearer_token(api_key: str | None) -> str | None: diff --git a/tests/test_litellm/llms/bedrock_mantle/passthrough/test_bedrock_mantle_passthrough_transformation.py b/tests/test_litellm/llms/bedrock_mantle/passthrough/test_bedrock_mantle_passthrough_transformation.py index 8c6eda605ca..090de0a9d3e 100644 --- a/tests/test_litellm/llms/bedrock_mantle/passthrough/test_bedrock_mantle_passthrough_transformation.py +++ b/tests/test_litellm/llms/bedrock_mantle/passthrough/test_bedrock_mantle_passthrough_transformation.py @@ -68,6 +68,27 @@ def test_explicit_region_and_non_mantle_api_base_are_kept(no_ambient_aws): assert base_url == vpc_endpoint +@pytest.mark.parametrize( + "lookalike_host", + [ + "https://bedrock-mantle.us-east-1.api.aws.internal.example.com", + "https://bedrock-mantle.us-gov-west-1.api.aws-int.example.com", + "https://bedrock-mantle.us-east-1.api.aws:8443", + ], +) +def test_lookalike_mantle_host_api_base_is_kept(no_ambient_aws, lookalike_host): + url, base_url = BedrockMantlePassthroughConfig().get_complete_url( + api_base=lookalike_host, + api_key=None, + model="us.openai.gpt-5.6-sol", + endpoint=INVOKE_ENDPOINT, + request_query_params=None, + litellm_params={"api_base": lookalike_host}, + ) + assert str(url) == f"{lookalike_host}/{INVOKE_ENDPOINT}" + assert base_url == lookalike_host + + def test_region_falls_back_to_the_mantle_default_without_any_hint(no_ambient_aws): url, _ = BedrockMantlePassthroughConfig().get_complete_url( api_base=None, 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 e8496eca4e5..1f23d39c631 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 @@ -26,6 +26,12 @@ from litellm.llms.bedrock_mantle.responses.transformation import ( from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders +LOOKALIKE_MANTLE_HOSTS = ( + "https://bedrock-mantle.us-east-1.api.aws.internal.example.com", + "https://bedrock-mantle.us-gov-west-1.api.aws-int.example.com", + "https://bedrock-mantle.us-east-1.api.aws:8443", +) + class TestBedrockMantleResponsesURL: def test_url_uses_region_from_env(self, monkeypatch): @@ -1642,6 +1648,23 @@ class TestBedrockMantleResponsesSigV4: ) assert url == "https://mantle-proxy.internal.example/openai/v1/responses" + @pytest.mark.parametrize("lookalike_host", LOOKALIKE_MANTLE_HOSTS) + def test_lookalike_mantle_host_from_api_base_is_preserved(self, monkeypatch, lookalike_host): + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + cfg = BedrockMantleResponsesAPIConfig() + url = cfg.get_complete_url( + api_base=f"{lookalike_host}/openai/v1", + litellm_params={"aws_region_name": "us-east-2"}, + ) + assert url == f"{lookalike_host}/openai/v1/responses" + + @pytest.mark.parametrize("lookalike_host", LOOKALIKE_MANTLE_HOSTS) + def test_lookalike_mantle_host_from_env_is_preserved(self, monkeypatch, lookalike_host): + monkeypatch.setenv("BEDROCK_MANTLE_API_BASE", lookalike_host) + cfg = BedrockMantleResponsesAPIConfig() + url = cfg.get_complete_url(api_base=None, litellm_params={}) + assert url == f"{lookalike_host}/openai/v1/responses" + def test_caller_authorization_does_not_override_sigv4(self, monkeypatch): """Adversarial-review regression: a caller-supplied Authorization header (e.g. from extra_headers, surviving the relaxed validate_environment) must not clobber 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 56b111f294e..b88c27e64b9 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 @@ -399,6 +399,46 @@ class TestBedrockMantleChatAuth: assert "/eu-west-1/bedrock/aws4_request" in headers["Authorization"] assert "/us-west-2/bedrock/aws4_request" not in headers["Authorization"] + @pytest.mark.parametrize( + ("region_params", "env", "expected_region"), + [ + ({"aws_region_name": "us-west-2"}, {}, "us-west-2"), + ({}, {"BEDROCK_MANTLE_REGION": "ap-southeast-2"}, "ap-southeast-2"), + ], + ) + def test_sigv4_scope_ignores_the_region_segment_of_a_lookalike_host( + self, monkeypatch, region_params, env, expected_region + ): + from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM + + for var in ( + "BEDROCK_MANTLE_API_KEY", + "AWS_BEARER_TOKEN_BEDROCK", + "BEDROCK_MANTLE_REGION", + "BEDROCK_MANTLE_API_BASE", + "AWS_REGION", + "AWS_REGION_NAME", + ): + monkeypatch.delenv(var, raising=False) + for var, value in env.items(): + monkeypatch.setenv(var, value) + + cfg = BedrockMantleChatConfig(aws_signer=BaseAWSLLM()) + headers, _ = cfg.sign_request( + headers={}, + optional_params={ + "aws_access_key_id": "AKIAEXAMPLE", + "aws_secret_access_key": "c2VjcmV0LXRlc3Qtc2VjcmV0LXRlc3Qtc2VjcmV0", + **region_params, + }, + request_data={"input": "hi"}, + api_base="https://bedrock-mantle.eu-west-1.api.aws.internal.example.com/openai/v1/chat/completions", + api_key=None, + ) + + assert f"/{expected_region}/bedrock/aws4_request" in headers["Authorization"] + assert "/eu-west-1/bedrock/aws4_request" not in headers["Authorization"] + def test_no_bearer_and_no_credentials_raises_value_error(self, monkeypatch): from unittest.mock import MagicMock