From 2615722f71686ea26bb05233a8b0fecfde423bfa Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 10 Jun 2026 10:14:05 +0530 Subject: [PATCH 1/8] fix: respect aws region --- .../bedrock_mantle/chat/transformation.py | 6 +++++- ...bedrock_mantle_responses_transformation.py | 21 +++++++++++++++++++ .../test_bedrock_mantle_transformation.py | 20 ++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index ad37a1990d30..0e9cb5615714 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -34,10 +34,14 @@ def get_config(cls): return super().get_config() def _get_openai_compatible_provider_info( - self, api_base: Optional[str], api_key: Optional[str] + self, + api_base: Optional[str], + api_key: Optional[str], + aws_region_name: Optional[str] = None, ) -> Tuple[Optional[str], Optional[str]]: region = ( get_secret_str("BEDROCK_MANTLE_REGION") + or aws_region_name or get_secret_str("AWS_REGION") or BEDROCK_MANTLE_DEFAULT_REGION ) 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 c3de29bd9d58..08e3abd6856e 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 @@ -83,6 +83,27 @@ def test_url_region_fallback_to_aws_region(self, monkeypatch): url = cfg.get_complete_url(api_base=None, litellm_params={}) assert url == "https://bedrock-mantle.us-west-2.api.aws/openai/v1/responses" + def test_url_region_from_aws_region_name_litellm_params(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + monkeypatch.setenv("AWS_REGION", "us-west-2") + cfg = BedrockMantleResponsesAPIConfig() + url = cfg.get_complete_url( + api_base=None, + litellm_params={"aws_region_name": "us-east-2"}, + ) + assert url == "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses" + + def test_url_aws_region_name_overrides_env_region(self, monkeypatch): + monkeypatch.setenv("BEDROCK_MANTLE_REGION", "us-west-2") + cfg = BedrockMantleResponsesAPIConfig() + url = cfg.get_complete_url( + api_base=None, + litellm_params={"aws_region_name": "us-east-2"}, + ) + assert url == "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses" + def test_url_region_default_us_east_1(self, monkeypatch): monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) 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 deaa05379307..cb78ae659370 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 @@ -63,6 +63,26 @@ def test_default_api_base_uses_aws_region(self, monkeypatch): api_base, _ = cfg._get_openai_compatible_provider_info(None, None) assert api_base == "https://bedrock-mantle.ap-northeast-1.api.aws/v1" + def test_aws_region_name_param_overrides_env(self, monkeypatch): + monkeypatch.setenv("BEDROCK_MANTLE_REGION", "us-west-2") + cfg = BedrockMantleChatConfig() + api_base, _ = cfg._get_openai_compatible_provider_info( + None, None, aws_region_name="us-east-2" + ) + assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" + + def test_get_llm_provider_uses_aws_region_name_for_responses(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + _, provider, _, api_base = litellm.get_llm_provider( + model="openai.gpt-5.5", + custom_llm_provider="bedrock_mantle", + aws_region_name="us-east-2", + ) + assert provider == "bedrock_mantle" + assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" + def test_default_api_base_fallback_to_us_east_1(self, monkeypatch): monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) From 2d888fa980574075552871fb3a460f57dab4d2f6 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 10 Jun 2026 11:28:27 +0530 Subject: [PATCH 2/8] Fix chat completion to responses bridge --- .../litellm_core_utils/get_litellm_params.py | 7 ++- .../get_llm_provider_logic.py | 32 ++++++++----- litellm/main.py | 9 ++++ litellm/responses/main.py | 26 +++-------- ...t_responses_bridge_provider_propagation.py | 37 +++++++++++++++ ...bedrock_mantle_responses_transformation.py | 46 +++++++++++++++++++ 6 files changed, 124 insertions(+), 33 deletions(-) diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index 6e655b03fed2..fc3c25e0d957 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -4,7 +4,7 @@ # Pre-define optional kwargs keys as frozenset for O(1) lookups # These are extracted from kwargs only if present, avoiding unnecessary .get() calls -_OPTIONAL_KWARGS_KEYS = frozenset( +OPTIONAL_KWARGS_KEYS = frozenset( { "azure_ad_token", "tenant_id", @@ -39,6 +39,9 @@ } ) +# Backward-compatible alias for existing imports/tests. +_OPTIONAL_KWARGS_KEYS = OPTIONAL_KWARGS_KEYS + def _get_base_model_from_litellm_call_metadata( metadata: Optional[dict], @@ -166,7 +169,7 @@ def get_litellm_params( # Sparse extraction: only add kwargs keys that are actually present if kwargs: - for key in _OPTIONAL_KWARGS_KEYS: + for key in OPTIONAL_KWARGS_KEYS: if key in kwargs: litellm_params[key] = kwargs[key] diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index de65ed933121..a7f8c69cfb6d 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -1,5 +1,5 @@ import re -from typing import Optional, Tuple +from typing import Optional, Tuple, cast from urllib.parse import urlparse import litellm @@ -7,7 +7,7 @@ from litellm.llms.openai_like.json_loader import JSONProviderRegistry from litellm.secret_managers.main import get_secret, get_secret_str -from ..types.router import LiteLLM_Params +from ..types.router import GenericLiteLLMParams, LiteLLM_Params def _endpoint_matches_api_base(endpoint: str, api_base: str) -> bool: @@ -159,7 +159,7 @@ def get_llm_provider( # noqa: PLR0915 custom_llm_provider: Optional[str] = None, api_base: Optional[str] = None, api_key: Optional[str] = None, - litellm_params: Optional[LiteLLM_Params] = None, + litellm_params: Optional[GenericLiteLLMParams] = None, ) -> Tuple[str, str, Optional[str], Optional[str]]: """ Returns the provider for a given model name - e.g. 'azure/chatgpt-v-2' -> 'azure' @@ -178,7 +178,7 @@ def get_llm_provider( # noqa: PLR0915 ) if litellm.LiteLLMProxyChatConfig._should_use_litellm_proxy_by_default( - litellm_params=litellm_params + litellm_params=cast(Optional[LiteLLM_Params], litellm_params) ): return litellm.LiteLLMProxyChatConfig.litellm_proxy_get_custom_llm_provider_info( model=model, api_base=api_base, api_key=api_key @@ -186,12 +186,14 @@ def get_llm_provider( # noqa: PLR0915 ## IF LITELLM PARAMS GIVEN ## if litellm_params: - assert ( - custom_llm_provider is None and api_base is None and api_key is None - ), "Either pass in litellm_params or the custom_llm_provider/api_base/api_key. Otherwise, these values will be overriden." - custom_llm_provider = litellm_params.custom_llm_provider - api_base = litellm_params.api_base - api_key = litellm_params.api_key + if ( + custom_llm_provider is None + and api_base is None + and api_key is None + ): + custom_llm_provider = litellm_params.custom_llm_provider + api_base = litellm_params.api_base + api_key = litellm_params.api_key dynamic_api_key = None # check if llm provider provided @@ -235,6 +237,7 @@ def get_llm_provider( # noqa: PLR0915 api_base=api_base, api_key=api_key, dynamic_api_key=dynamic_api_key, + litellm_params=litellm_params, ) # check if llm provider part of model name @@ -250,6 +253,7 @@ def get_llm_provider( # noqa: PLR0915 api_base=api_base, api_key=api_key, dynamic_api_key=dynamic_api_key, + litellm_params=litellm_params, ) elif model.split("/", 1)[0] in litellm.provider_list: custom_llm_provider = model.split("/", 1)[0] @@ -570,6 +574,7 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 api_base: Optional[str], api_key: Optional[str], dynamic_api_key: Optional[str], + litellm_params: Optional[GenericLiteLLMParams] = None, ) -> Tuple[str, str, Optional[str], Optional[str]]: """ Returns: @@ -633,11 +638,16 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 api_base, api_key ) elif custom_llm_provider == "bedrock_mantle": + mantle_region = ( + getattr(litellm_params, "aws_region_name", None) + if litellm_params is not None + else None + ) ( api_base, dynamic_api_key, ) = litellm.BedrockMantleChatConfig()._get_openai_compatible_provider_info( - api_base, api_key + api_base, api_key, aws_region_name=mantle_region ) elif custom_llm_provider == "nvidia_nim": # nvidia_nim is openai compatible, we just need to set this to custom_openai and have the api_base be https://api.endpoints.anyscale.com/v1 diff --git a/litellm/main.py b/litellm/main.py index 02609217ddb4..18dcdfcd6beb 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -86,6 +86,7 @@ get_audio_file_for_health_check, ) from litellm.litellm_core_utils.completion_timeout import CompletionTimeout +from litellm.litellm_core_utils.get_litellm_params import OPTIONAL_KWARGS_KEYS from litellm.litellm_core_utils.dd_tracing import tracer from litellm.litellm_core_utils.get_provider_specific_headers import ( ProviderSpecificHeaderUtils, @@ -1407,11 +1408,19 @@ def completion( # type: ignore # noqa: PLR0915 if deployment_id is not None: # azure llms model = deployment_id custom_llm_provider = "azure" + _supplemental_provider_params = { + k: kwargs[k] for k in OPTIONAL_KWARGS_KEYS if k in kwargs + } model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider( model=model, custom_llm_provider=custom_llm_provider, api_base=api_base, api_key=api_key, + litellm_params=( + GenericLiteLLMParams(**_supplemental_provider_params) + if _supplemental_provider_params + else None + ), ) ## RESPONSES API BRIDGE LOGIC ## - check early and normalize model name diff --git a/litellm/responses/main.py b/litellm/responses/main.py index e4c713f67c0b..34c9cdd3d1ca 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -673,6 +673,8 @@ def _resolve_model_provider_for_responses( litellm_params: GenericLiteLLMParams, local_vars: Dict[str, Any], ) -> tuple[str, Optional[str]]: + if custom_llm_provider is not None and not litellm_params.custom_llm_provider: + litellm_params.custom_llm_provider = custom_llm_provider ( model, custom_llm_provider, @@ -680,9 +682,7 @@ def _resolve_model_provider_for_responses( dynamic_api_base, ) = litellm.get_llm_provider( model=model, - custom_llm_provider=custom_llm_provider, - api_base=litellm_params.api_base, - api_key=litellm_params.api_key, + litellm_params=litellm_params, ) local_vars["custom_llm_provider"] = custom_llm_provider if dynamic_api_key is not None: @@ -1972,27 +1972,13 @@ def compact_responses( # get llm provider logic litellm_params = GenericLiteLLMParams(**kwargs) - ( - model, - custom_llm_provider, - dynamic_api_key, - dynamic_api_base, - ) = litellm.get_llm_provider( + model, custom_llm_provider = _resolve_model_provider_for_responses( model=model, custom_llm_provider=custom_llm_provider, - api_base=litellm_params.api_base, - api_key=litellm_params.api_key, + litellm_params=litellm_params, + local_vars=local_vars, ) - # Update local_vars with detected provider (fixes #19782) - local_vars["custom_llm_provider"] = custom_llm_provider - - # Use dynamic credentials from get_llm_provider (e.g., when use_litellm_proxy=True) - if dynamic_api_key is not None: - litellm_params.api_key = dynamic_api_key - if dynamic_api_base is not None: - litellm_params.api_base = dynamic_api_base - if custom_llm_provider is None: raise ValueError("custom_llm_provider is required but passed as None") diff --git a/tests/test_litellm/completion_extras/test_responses_bridge_provider_propagation.py b/tests/test_litellm/completion_extras/test_responses_bridge_provider_propagation.py index b41dbd54b856..8036c72679e8 100644 --- a/tests/test_litellm/completion_extras/test_responses_bridge_provider_propagation.py +++ b/tests/test_litellm/completion_extras/test_responses_bridge_provider_propagation.py @@ -114,3 +114,40 @@ async def _fake_aresponses(**kwargs): "so the downstream get_llm_provider() call does not re-strip the " "provider prefix on a provider/provider/model deployment string" ) + + +@pytest.mark.asyncio +async def test_async_completion_forwards_aws_region_name(): + handler = ResponsesToCompletionBridgeHandler() + handler.transformation_handler = MagicMock() + handler.transformation_handler.transform_request.return_value = { + "model": "openai.gpt-5.5", + "input": [], + "aws_region_name": "us-east-2", + "api_base": "https://bedrock-mantle.us-east-1.api.aws/v1", + "custom_llm_provider": "bedrock_mantle", + } + + async def _fake_aresponses(**kwargs): + _fake_aresponses.kwargs = kwargs + return MagicMock(spec=[]) + + _fake_aresponses.kwargs = {} + + validated = _validated_kwargs() + validated["custom_llm_provider"] = "bedrock_mantle" + validated["litellm_params"] = { + "aws_region_name": "us-east-2", + "api_base": "https://bedrock-mantle.us-east-1.api.aws/v1", + "custom_llm_provider": "bedrock_mantle", + } + + with ( + patch.object(handler, "validate_input_kwargs", return_value=validated), + patch("litellm.aresponses", _fake_aresponses), + ): + try: + await handler.acompletion() + except Exception: + pass + assert _fake_aresponses.kwargs.get("aws_region_name") == "us-east-2" 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 08e3abd6856e..1c9c9e857ea0 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 @@ -148,6 +148,52 @@ def test_default_construction_keeps_openai_path(self, monkeypatch): url = cfg.get_complete_url(api_base=None, litellm_params={}) assert url == "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses" + def test_url_aws_region_name_overrides_stale_api_base(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + cfg = BedrockMantleResponsesAPIConfig() + url = cfg.get_complete_url( + api_base="https://bedrock-mantle.us-east-1.api.aws/v1", + litellm_params={"aws_region_name": "us-east-2"}, + ) + assert url == "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses" + + +class TestBedrockMantleGetLlmProviderRegion: + def test_get_llm_provider_uses_supplemental_litellm_params(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + from litellm.types.router import GenericLiteLLMParams + + _, provider, _, api_base = get_llm_provider( + model="bedrock_mantle/openai.gpt-5.5", + api_key="test-key", + litellm_params=GenericLiteLLMParams(aws_region_name="us-east-2"), + ) + assert provider == "bedrock_mantle" + assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" + + def test_get_llm_provider_uses_aws_region_from_litellm_params(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + from litellm.types.router import GenericLiteLLMParams + + params = GenericLiteLLMParams( + custom_llm_provider="bedrock_mantle", + aws_region_name="us-east-2", + ) + _, provider, _, api_base = get_llm_provider( + model="bedrock_mantle/openai.gpt-5.5", + litellm_params=params, + ) + assert provider == "bedrock_mantle" + assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" + class TestBedrockMantleResponsesAuth: def test_config_api_key_takes_priority(self, monkeypatch): From 801c23dcac83fe2c1fee87273a8239a8ead2d2a7 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 10 Jun 2026 12:27:40 +0530 Subject: [PATCH 3/8] Handle response streaming events --- .../responses/transformation.py | 59 ++++- litellm/responses/streaming_iterator.py | 104 ++++++++- ...bedrock_mantle_responses_transformation.py | 25 +++ .../test_responses_websocket_all_providers.py | 203 ++++++++++++++++++ 4 files changed, 388 insertions(+), 3 deletions(-) diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index 29248e1ca50e..baf2b1ad2cff 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -16,7 +16,7 @@ """ import re -from typing import Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from botocore.exceptions import ( CredentialRetrievalError, @@ -25,9 +25,11 @@ ProfileNotFound, ) +from litellm._logging import verbose_logger from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import LlmProviders @@ -48,6 +50,11 @@ r"^https?://bedrock-mantle\.([^/.]+)\.api\.aws", re.IGNORECASE ) +# Per Bedrock Mantle Responses API validation errors. +_BEDROCK_MANTLE_SUPPORTED_RESPONSE_TOOL_TYPES = frozenset( + {"function", "mcp", "custom", "namespace", "tool_search"} +) + class BedrockMantleResponsesAPIConfig(OpenAIResponsesAPIConfig): def __init__( @@ -125,6 +132,56 @@ def supports_native_file_search(self) -> bool: def supports_native_websocket(self) -> bool: return False + @staticmethod + def _filter_unsupported_tools(tools: List[Any]) -> List[Any]: + """Keep only tool types Mantle's Responses API accepts.""" + kept: List[Any] = [] + dropped_types: List[str] = [] + for tool in tools: + if not isinstance(tool, dict): + kept.append(tool) + continue + tool_type = tool.get("type") + if tool_type in _BEDROCK_MANTLE_SUPPORTED_RESPONSE_TOOL_TYPES: + kept.append(tool) + elif tool_type is not None: + dropped_types.append(str(tool_type)) + + if dropped_types: + verbose_logger.debug( + "Bedrock Mantle Responses API: dropping unsupported tool type(s) " + "%s (supported: %s).", + sorted(set(dropped_types)), + sorted(_BEDROCK_MANTLE_SUPPORTED_RESPONSE_TOOL_TYPES), + ) + + return kept + + def map_openai_params( + self, + response_api_optional_params: ResponsesAPIOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + params = super().map_openai_params( + response_api_optional_params=response_api_optional_params, + model=model, + drop_params=drop_params, + ) + + tools = params.get("tools") + if not tools: + return params + + tools_list = tools if isinstance(tools, list) else [tools] + filtered = self._filter_unsupported_tools(tools_list) + if filtered: + params["tools"] = filtered + else: + params.pop("tools", None) + + return params + def sign_request( self, headers: dict, diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index dfc43bc29b5a..2143beed3a57 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -4,6 +4,7 @@ import json import time import traceback +import uuid from datetime import datetime from functools import lru_cache from typing import Any, Dict, List, Literal, Optional @@ -1418,6 +1419,8 @@ async def bidirectional_forward(self) -> None: } ) +_WARMUP_RESPONSE_ID_PREFIX = "resp_warmup_" + class ManagedResponsesWebSocketHandler: """ @@ -1455,6 +1458,9 @@ def __init__( self.logging_obj = logging_obj self.user_api_key_dict = user_api_key_dict self.litellm_metadata: Dict[str, Any] = litellm_metadata or {} + self.model_group: Optional[str] = self.litellm_metadata.get( + "model_group" + ) or self.litellm_metadata.get("deployment_model_name") self.api_key = api_key self.api_base = api_base self.timeout = timeout @@ -1612,6 +1618,71 @@ async def _parse_message(self, raw_message: str) -> Optional[Dict[str, Any]]: return None return msg_obj + @staticmethod + def _is_warmup_frame(msg_obj: Dict[str, Any]) -> bool: + """Return True for a response.create whose generate flag is false.""" + nested = msg_obj.get("response") + source = nested if isinstance(nested, dict) and nested else msg_obj + return source.get("generate") is False + + @staticmethod + def _is_warmup_response_id(response_id: Optional[str]) -> bool: + """Return True for synthetic warmup IDs that only exist on this connection.""" + if not response_id: + return False + decoded = ResponsesAPIRequestUtils._decode_responses_api_response_id( + response_id + ) + raw_id = decoded.get("response_id", response_id) + return str(raw_id).startswith(_WARMUP_RESPONSE_ID_PREFIX) + + @staticmethod + def _warmup_source_params(msg_obj: Dict[str, Any]) -> Dict[str, Any]: + nested = msg_obj.get("response") + if isinstance(nested, dict) and nested: + return nested + return {k: v for k, v in msg_obj.items() if k != "type"} + + def _build_warmup_response(self, msg_obj: Dict[str, Any]) -> Dict[str, Any]: + """Build a minimal completed Responses API object for a warmup ack.""" + source = self._warmup_source_params(msg_obj) + wire_model = source.get("model") or self.model_group or self.model + return { + "id": f"resp_warmup_{uuid.uuid4().hex}", + "object": "response", + "created_at": int(time.time()), + "status": "completed", + "model": wire_model, + "output": [], + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + }, + } + + async def _send_warmup_ack(self, msg_obj: Dict[str, Any]) -> None: + """ + Acknowledge a generate=false prewarm without calling the provider. + + Codex blocks on the warmup turn until it receives response.created and + response.completed over the WebSocket. Managed HTTP providers cannot + honor an empty-input warmup, so we synthesize the completion locally. + """ + response = self._build_warmup_response(msg_obj) + for event_type, status in ( + ("response.created", "in_progress"), + ("response.completed", "completed"), + ): + event = { + "type": event_type, + "response": {**response, "status": status}, + } + serialized = self._serialize_chunk(event) + if serialized is None: + continue + await self.websocket.send_text(serialized) + @staticmethod def _build_base_call_kwargs(msg_obj: Dict[str, Any]) -> Dict[str, Any]: """ @@ -1641,6 +1712,12 @@ def _apply_history( """Prepend in-memory turn history, or fall back to DB-based reconstruction.""" if not previous_response_id: return + if self._is_warmup_response_id(previous_response_id): + verbose_logger.debug( + "ManagedResponsesWS: ignoring synthetic warmup previous_response_id=%s", + previous_response_id, + ) + return if prior_history: call_kwargs["input"] = prior_history + current_messages verbose_logger.debug( @@ -1807,10 +1884,31 @@ async def _process_response_create(self, raw_message: str) -> None: if msg_obj is None: return + # generate=false is a prompt-cache warmup hint (sent by codex prewarm). + # Native provider sockets handle it server-side, but there is no HTTP + # equivalent and the frame carries empty input. Managed providers must + # synthesize a completion so clients like Codex can proceed. + if self._is_warmup_frame(msg_obj): + try: + await self._send_warmup_ack(msg_obj) + except Exception as exc: + verbose_logger.debug( + "ManagedResponsesWS: error sending warmup ack: %s", exc + ) + return + call_kwargs = self._build_base_call_kwargs(msg_obj) call_kwargs["stream"] = True - model = call_kwargs.pop("model", None) or self.model + # A frame that repeats the connection's public alias (model_group) must + # reuse the router-resolved self.model; passing the alias raw to + # litellm.aresponses fails in get_llm_provider. A genuinely different + # provider-prefixed per-frame model is still honored. + requested_model = call_kwargs.pop("model", None) + if requested_model is None or requested_model == self.model_group: + model = self.model + else: + model = requested_model previous_response_id: Optional[str] = call_kwargs.pop( "previous_response_id", None @@ -1828,7 +1926,9 @@ async def _process_response_create(self, raw_message: str) -> None: call_kwargs, previous_response_id, current_messages, prior_history ) self._inject_credentials(call_kwargs, model=model) - self._update_proxy_request(call_kwargs, model) + self._update_proxy_request( + call_kwargs, requested_model or self.model_group or model + ) call_kwargs.update(self.extra_kwargs) try: 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 1c9c9e857ea0..372b4ab403fe 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 @@ -314,6 +314,31 @@ def test_standard_path_outbound_body_carries_bare_model(self): assert "input" in body +class TestBedrockMantleResponsesTools: + def test_map_openai_params_drops_unsupported_tools(self): + cfg = BedrockMantleResponsesAPIConfig() + params = cfg.map_openai_params( + response_api_optional_params={ + "tools": [ + {"type": "web_search"}, + {"type": "function", "name": "exec_command"}, + ] + }, + model="openai.gpt-5.5", + drop_params=False, + ) + assert params["tools"] == [{"type": "function", "name": "exec_command"}] + + def test_map_openai_params_removes_tools_when_all_unsupported(self): + cfg = BedrockMantleResponsesAPIConfig() + params = cfg.map_openai_params( + response_api_optional_params={"tools": [{"type": "web_search"}]}, + model="openai.gpt-5.5", + drop_params=False, + ) + assert "tools" not in params + + class TestBedrockMantleResponsesRegistry: def test_registry_returns_config_for_gpt_5_5(self): from litellm.utils import ProviderConfigManager diff --git a/tests/test_litellm/responses/test_responses_websocket_all_providers.py b/tests/test_litellm/responses/test_responses_websocket_all_providers.py index 1981651797d8..54f86e7b8546 100644 --- a/tests/test_litellm/responses/test_responses_websocket_all_providers.py +++ b/tests/test_litellm/responses/test_responses_websocket_all_providers.py @@ -165,6 +165,209 @@ async def test_managed_handler_instantiation(self): assert handler.timeout == 30.0 assert handler.custom_llm_provider == "test_provider" + @pytest.mark.asyncio + async def test_frame_alias_resolves_to_connection_model(self, monkeypatch): + """ + A response.create frame that repeats the public model alias must reach + litellm.aresponses with the router-resolved deployment model, not the + raw alias (which fails in get_llm_provider). Regression for codex + WebSocket sessions against managed providers like bedrock_mantle. + """ + import json + from unittest.mock import AsyncMock, MagicMock + + import litellm + from litellm.litellm_core_utils.litellm_logging import Logging + from litellm.responses.streaming_iterator import ( + ManagedResponsesWebSocketHandler, + ) + + captured: dict = {} + + async def fake_aresponses(*args, **kwargs): + captured["model"] = kwargs.get("model") + + async def _empty(): + return + yield + + return _empty() + + monkeypatch.setattr(litellm, "aresponses", fake_aresponses) + + mock_websocket = MagicMock() + mock_websocket.send_text = AsyncMock() + + handler = ManagedResponsesWebSocketHandler( + websocket=mock_websocket, + model="bedrock_mantle/openai.gpt-5.5", + logging_obj=Logging( + model="bedrock_mantle/openai.gpt-5.5", + messages=[], + stream=True, + call_type="aresponses", + start_time=0, + litellm_call_id="test-id", + function_id="test-func", + ), + litellm_metadata={"model_group": "gpt-5.5-mantle"}, + ) + + frame = json.dumps( + { + "type": "response.create", + "model": "gpt-5.5-mantle", + "input": [], + } + ) + await handler._process_response_create(frame) + + assert captured["model"] == "bedrock_mantle/openai.gpt-5.5" + + @pytest.mark.asyncio + async def test_warmup_frame_skips_provider_and_sends_synthetic_ack( + self, monkeypatch + ): + """ + A generate=false warmup frame (codex prewarm) carries empty input that + managed HTTP providers reject. It must not call the provider, and should + emit synthetic response.created/completed events so Codex can proceed. + """ + import json + from unittest.mock import AsyncMock, MagicMock + + import litellm + from litellm.litellm_core_utils.litellm_logging import Logging + from litellm.responses.streaming_iterator import ( + ManagedResponsesWebSocketHandler, + ) + + called = False + + async def fail_aresponses(*args, **kwargs): + nonlocal called + called = True + raise AssertionError("provider must not be called for a warmup frame") + + monkeypatch.setattr(litellm, "aresponses", fail_aresponses) + + mock_websocket = MagicMock() + mock_websocket.send_text = AsyncMock() + + handler = ManagedResponsesWebSocketHandler( + websocket=mock_websocket, + model="bedrock_mantle/openai.gpt-5.5", + logging_obj=Logging( + model="bedrock_mantle/openai.gpt-5.5", + messages=[], + stream=True, + call_type="aresponses", + start_time=0, + litellm_call_id="test-id", + function_id="test-func", + ), + litellm_metadata={"model_group": "gpt-5.5-mantle"}, + ) + + frame = json.dumps( + { + "type": "response.create", + "model": "gpt-5.5-mantle", + "generate": False, + "input": [], + } + ) + await handler._process_response_create(frame) + + assert called is False + assert mock_websocket.send_text.call_count == 2 + events = [ + json.loads(call.args[0]) for call in mock_websocket.send_text.call_args_list + ] + assert events[0]["type"] == "response.created" + assert events[0]["response"]["status"] == "in_progress" + assert events[1]["type"] == "response.completed" + assert events[1]["response"]["status"] == "completed" + assert events[1]["response"]["output"] == [] + assert events[1]["response"]["model"] == "gpt-5.5-mantle" + + @pytest.mark.asyncio + async def test_warmup_previous_response_id_not_forwarded_to_provider( + self, monkeypatch + ): + import json + from unittest.mock import AsyncMock, MagicMock + + import litellm + from litellm.litellm_core_utils.litellm_logging import Logging + from litellm.responses.streaming_iterator import ( + ManagedResponsesWebSocketHandler, + ) + + captured: dict = {} + + async def fake_aresponses(*args, **kwargs): + captured.update(kwargs) + + async def _empty(): + return + yield + + return _empty() + + monkeypatch.setattr(litellm, "aresponses", fake_aresponses) + + mock_websocket = MagicMock() + mock_websocket.send_text = AsyncMock() + + handler = ManagedResponsesWebSocketHandler( + websocket=mock_websocket, + model="bedrock_mantle/openai.gpt-5.5", + logging_obj=Logging( + model="bedrock_mantle/openai.gpt-5.5", + messages=[], + stream=True, + call_type="aresponses", + start_time=0, + litellm_call_id="test-id", + function_id="test-func", + ), + litellm_metadata={"model_group": "gpt-5.5-mantle"}, + ) + + await handler._process_response_create( + json.dumps( + { + "type": "response.create", + "model": "gpt-5.5-mantle", + "generate": False, + "input": [], + } + ) + ) + warmup_id = json.loads(mock_websocket.send_text.call_args_list[1].args[0])[ + "response" + ]["id"] + + await handler._process_response_create( + json.dumps( + { + "type": "response.create", + "model": "gpt-5.5-mantle", + "previous_response_id": warmup_id, + "input": [ + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Hi"}], + } + ], + } + ) + ) + + assert "previous_response_id" not in captured + class TestChunkTransformation: """Test chunk serialization and transformation for WebSocket streaming""" From 65c061a9dc9e2f7f05f4b96f107e62c37a0f1838 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 10 Jun 2026 16:46:52 +0530 Subject: [PATCH 4/8] Fix bedrock mantle region priority and CI test failures. aws_region_name now overrides BEDROCK_MANTLE_REGION env, and provider tests pass region via litellm_params. Co-authored-by: Cursor --- litellm/litellm_core_utils/get_llm_provider_logic.py | 6 +----- litellm/llms/bedrock_mantle/chat/transformation.py | 4 ++-- .../bedrock_mantle/test_bedrock_mantle_transformation.py | 5 ++++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index a7f8c69cfb6d..277ce4f2a6de 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -186,11 +186,7 @@ def get_llm_provider( # noqa: PLR0915 ## IF LITELLM PARAMS GIVEN ## if litellm_params: - if ( - custom_llm_provider is None - and api_base is None - and api_key is None - ): + if custom_llm_provider is None and api_base is None and api_key is None: custom_llm_provider = litellm_params.custom_llm_provider api_base = litellm_params.api_base api_key = litellm_params.api_key diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index 0e9cb5615714..77b70b244c1a 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -40,8 +40,8 @@ def _get_openai_compatible_provider_info( aws_region_name: Optional[str] = None, ) -> Tuple[Optional[str], Optional[str]]: region = ( - get_secret_str("BEDROCK_MANTLE_REGION") - or aws_region_name + aws_region_name + or get_secret_str("BEDROCK_MANTLE_REGION") or get_secret_str("AWS_REGION") or BEDROCK_MANTLE_DEFAULT_REGION ) 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 cb78ae659370..4b9cacdde86a 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 @@ -65,6 +65,7 @@ def test_default_api_base_uses_aws_region(self, monkeypatch): def test_aws_region_name_param_overrides_env(self, monkeypatch): monkeypatch.setenv("BEDROCK_MANTLE_REGION", "us-west-2") + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) cfg = BedrockMantleChatConfig() api_base, _ = cfg._get_openai_compatible_provider_info( None, None, aws_region_name="us-east-2" @@ -72,13 +73,15 @@ def test_aws_region_name_param_overrides_env(self, monkeypatch): assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" def test_get_llm_provider_uses_aws_region_name_for_responses(self, monkeypatch): + from litellm.types.router import GenericLiteLLMParams + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) monkeypatch.delenv("AWS_REGION", raising=False) _, provider, _, api_base = litellm.get_llm_provider( model="openai.gpt-5.5", custom_llm_provider="bedrock_mantle", - aws_region_name="us-east-2", + litellm_params=GenericLiteLLMParams(aws_region_name="us-east-2"), ) assert provider == "bedrock_mantle" assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" From aaa3825371748983f0f3e2cfbfddfdd9bb2e4440 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 12 Jun 2026 04:54:02 +0000 Subject: [PATCH 5/8] refactor(bedrock_mantle): use warmup id prefix constant and log untyped tool drops --- litellm/llms/bedrock_mantle/responses/transformation.py | 2 +- litellm/responses/streaming_iterator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index baf2b1ad2cff..2816601e62a0 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -144,7 +144,7 @@ def _filter_unsupported_tools(tools: List[Any]) -> List[Any]: tool_type = tool.get("type") if tool_type in _BEDROCK_MANTLE_SUPPORTED_RESPONSE_TOOL_TYPES: kept.append(tool) - elif tool_type is not None: + else: dropped_types.append(str(tool_type)) if dropped_types: diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 2143beed3a57..6b790aa383ba 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -1648,7 +1648,7 @@ def _build_warmup_response(self, msg_obj: Dict[str, Any]) -> Dict[str, Any]: source = self._warmup_source_params(msg_obj) wire_model = source.get("model") or self.model_group or self.model return { - "id": f"resp_warmup_{uuid.uuid4().hex}", + "id": f"{_WARMUP_RESPONSE_ID_PREFIX}{uuid.uuid4().hex}", "object": "response", "created_at": int(time.time()), "status": "completed", From eebbba8cb45c2cd6e6e8b7f6a05a857dd659884d Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:07:49 +0000 Subject: [PATCH 6/8] refactor(bedrock_mantle): keep aws_region_name extraction inside chat config --- litellm/litellm_core_utils/get_llm_provider_logic.py | 7 +------ litellm/llms/bedrock_mantle/chat/transformation.py | 5 +++-- .../bedrock_mantle/test_bedrock_mantle_transformation.py | 4 +++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 277ce4f2a6de..5dc3f5c68681 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -634,16 +634,11 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915 api_base, api_key ) elif custom_llm_provider == "bedrock_mantle": - mantle_region = ( - getattr(litellm_params, "aws_region_name", None) - if litellm_params is not None - else None - ) ( api_base, dynamic_api_key, ) = litellm.BedrockMantleChatConfig()._get_openai_compatible_provider_info( - api_base, api_key, aws_region_name=mantle_region + api_base, api_key, litellm_params=litellm_params ) elif custom_llm_provider == "nvidia_nim": # nvidia_nim is openai compatible, we just need to set this to custom_openai and have the api_base be https://api.endpoints.anyscale.com/v1 diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index 77b70b244c1a..cdf7045f3dc2 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -14,6 +14,7 @@ from litellm._logging import verbose_logger from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import AllMessageValues +from litellm.types.router import GenericLiteLLMParams from ...openai_like.chat.transformation import OpenAILikeChatConfig @@ -37,10 +38,10 @@ def _get_openai_compatible_provider_info( self, api_base: Optional[str], api_key: Optional[str], - aws_region_name: Optional[str] = None, + litellm_params: Optional[GenericLiteLLMParams] = None, ) -> Tuple[Optional[str], Optional[str]]: region = ( - aws_region_name + (litellm_params.aws_region_name if litellm_params else None) or get_secret_str("BEDROCK_MANTLE_REGION") or get_secret_str("AWS_REGION") or BEDROCK_MANTLE_DEFAULT_REGION 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 4b9cacdde86a..3ae626401d6d 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 @@ -64,11 +64,13 @@ def test_default_api_base_uses_aws_region(self, monkeypatch): assert api_base == "https://bedrock-mantle.ap-northeast-1.api.aws/v1" def test_aws_region_name_param_overrides_env(self, monkeypatch): + from litellm.types.router import GenericLiteLLMParams + monkeypatch.setenv("BEDROCK_MANTLE_REGION", "us-west-2") monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) cfg = BedrockMantleChatConfig() api_base, _ = cfg._get_openai_compatible_provider_info( - None, None, aws_region_name="us-east-2" + None, None, litellm_params=GenericLiteLLMParams(aws_region_name="us-east-2") ) assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" From be38a21e976fd76096bd141a336713ad63fd6442 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:17:08 +0000 Subject: [PATCH 7/8] fix(bedrock_mantle): validate aws_region_name before host interpolation The client-supplied aws_region_name flows unvalidated into the Bedrock Mantle host (https://bedrock-mantle.{region}.api.aws), so a value containing a slash could redirect the request, along with the configured bearer API key, to an arbitrary host. Validate the region against the AWS region format in both the chat and responses transformations before it is interpolated. --- .../bedrock_mantle/chat/transformation.py | 2 ++ .../responses/transformation.py | 1 + ...bedrock_mantle_responses_transformation.py | 13 ++++++++ .../test_bedrock_mantle_transformation.py | 31 +++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index cdf7045f3dc2..370906952602 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -12,6 +12,7 @@ import litellm from litellm._logging import verbose_logger +from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import AllMessageValues from litellm.types.router import GenericLiteLLMParams @@ -46,6 +47,7 @@ def _get_openai_compatible_provider_info( or get_secret_str("AWS_REGION") or BEDROCK_MANTLE_DEFAULT_REGION ) + BaseAWSLLM._validate_aws_region_name(region) api_base = ( api_base or get_secret_str("BEDROCK_MANTLE_API_BASE") diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index 2816601e62a0..21b20e146a6c 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -74,6 +74,7 @@ def custom_llm_provider(self) -> LlmProviders: def _resolve_region(params: dict) -> str: region = params.get("aws_region_name") if region: + BaseAWSLLM._validate_aws_region_name(region) return region base = params.get("api_base") or get_secret_str("BEDROCK_MANTLE_API_BASE") if base: 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 372b4ab403fe..9e9b7bc6d387 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 @@ -104,6 +104,19 @@ def test_url_aws_region_name_overrides_env_region(self, monkeypatch): ) assert url == "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses" + def test_url_rejects_malicious_aws_region_name(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + cfg = BedrockMantleResponsesAPIConfig() + with pytest.raises(ValueError): + cfg.get_complete_url( + api_base=None, + litellm_params={ + "aws_region_name": "us-east-1.api.aws.attacker.example/" + }, + ) + def test_url_region_default_us_east_1(self, monkeypatch): monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) 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 3ae626401d6d..428f536b0ec7 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 @@ -74,6 +74,37 @@ def test_aws_region_name_param_overrides_env(self, monkeypatch): ) assert api_base == "https://bedrock-mantle.us-east-2.api.aws/v1" + def test_malicious_aws_region_name_rejected(self, monkeypatch): + from litellm.types.router import GenericLiteLLMParams + + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + cfg = BedrockMantleChatConfig() + with pytest.raises(ValueError): + cfg._get_openai_compatible_provider_info( + None, + None, + litellm_params=GenericLiteLLMParams( + aws_region_name="us-east-1.api.aws.attacker.example/" + ), + ) + + def test_get_llm_provider_rejects_malicious_aws_region_name(self, monkeypatch): + from litellm.types.router import GenericLiteLLMParams + + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + with pytest.raises(litellm.exceptions.BadRequestError): + litellm.get_llm_provider( + model="openai.gpt-5.5", + custom_llm_provider="bedrock_mantle", + litellm_params=GenericLiteLLMParams( + aws_region_name="us-east-1.api.aws.attacker.example/" + ), + ) + def test_get_llm_provider_uses_aws_region_name_for_responses(self, monkeypatch): from litellm.types.router import GenericLiteLLMParams From 86a2965e6faee7a15b8a357f0b91e3b6ef103845 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:28:21 +0000 Subject: [PATCH 8/8] fix(bedrock_mantle): close AWS_REGION_NAME chat gap and surface dropped tools Chat region resolution now consults AWS_REGION_NAME, matching the responses path precedence. Unsupported Responses tools dropped by map_openai_params are logged at warning level so the loss is visible in production. --- .../llms/bedrock_mantle/chat/transformation.py | 1 + .../bedrock_mantle/responses/transformation.py | 2 +- ...est_bedrock_mantle_responses_transformation.py | 15 +++++++++++++++ .../test_bedrock_mantle_transformation.py | 9 +++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index 370906952602..18f051f85244 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -44,6 +44,7 @@ def _get_openai_compatible_provider_info( region = ( (litellm_params.aws_region_name if litellm_params else None) or get_secret_str("BEDROCK_MANTLE_REGION") + or get_secret_str("AWS_REGION_NAME") or get_secret_str("AWS_REGION") or BEDROCK_MANTLE_DEFAULT_REGION ) diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index 21b20e146a6c..b409666a967b 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -149,7 +149,7 @@ def _filter_unsupported_tools(tools: List[Any]) -> List[Any]: dropped_types.append(str(tool_type)) if dropped_types: - verbose_logger.debug( + verbose_logger.warning( "Bedrock Mantle Responses API: dropping unsupported tool type(s) " "%s (supported: %s).", sorted(set(dropped_types)), 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 9e9b7bc6d387..9f683bb15af2 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 @@ -351,6 +351,21 @@ def test_map_openai_params_removes_tools_when_all_unsupported(self): ) assert "tools" not in params + def test_dropped_tools_are_logged_at_warning_level(self): + from unittest.mock import patch + + cfg = BedrockMantleResponsesAPIConfig() + with patch( + "litellm.llms.bedrock_mantle.responses.transformation.verbose_logger.warning" + ) as mock_warning: + cfg.map_openai_params( + response_api_optional_params={"tools": [{"type": "web_search"}]}, + model="openai.gpt-5.5", + drop_params=False, + ) + assert mock_warning.call_count == 1 + assert "web_search" in str(mock_warning.call_args) + class TestBedrockMantleResponsesRegistry: def test_registry_returns_config_for_gpt_5_5(self): 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 428f536b0ec7..061d378f757e 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 @@ -63,6 +63,15 @@ def test_default_api_base_uses_aws_region(self, monkeypatch): api_base, _ = cfg._get_openai_compatible_provider_info(None, None) assert api_base == "https://bedrock-mantle.ap-northeast-1.api.aws/v1" + def test_default_api_base_uses_aws_region_name_env(self, monkeypatch): + monkeypatch.delenv("BEDROCK_MANTLE_REGION", raising=False) + monkeypatch.delenv("BEDROCK_MANTLE_API_BASE", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + monkeypatch.setenv("AWS_REGION_NAME", "ca-central-1") + cfg = BedrockMantleChatConfig() + api_base, _ = cfg._get_openai_compatible_provider_info(None, None) + assert api_base == "https://bedrock-mantle.ca-central-1.api.aws/v1" + def test_aws_region_name_param_overrides_env(self, monkeypatch): from litellm.types.router import GenericLiteLLMParams