diff --git a/litellm/llms/bedrock_mantle/common_utils.py b/litellm/llms/bedrock_mantle/common_utils.py index 75e34cf3ae5..3f7540c2147 100644 --- a/litellm/llms/bedrock_mantle/common_utils.py +++ b/litellm/llms/bedrock_mantle/common_utils.py @@ -40,6 +40,37 @@ def split_mantle_region_prefix(model: str) -> tuple[str | None, str]: return None, model +# Cross-Region / geo inference-profile prefixes on Bedrock model IDs +# (us.openai.gpt-6.1-sol, eu.anthropic..., global....). Distinct from the +# AWS-region path prefix handled by split_mantle_region_prefix (us-east-1/...). +# Keep aligned with litellm.utils._BEDROCK_REGION_PREFIXES; cannot import that +# here without a circular import through utils -> this module. +_MANTLE_GEO_PROFILE_PREFIXES: Final = ( + "us.", + "eu.", + "apac.", + "jp.", + "au.", + "us-gov.", + "global.", + "ap-northeast-1.", +) + + +def mantle_cost_map_model_id(model: str) -> str: + """Bare model id for ``bedrock_mantle/`` cost-map lookups. + + Strips an optional AWS-region path prefix and a geo inference-profile + prefix so ``us.openai.gpt-6.1-sol`` resolves to the + ``openai.gpt-6.1-sol`` row. The model id sent to AWS is unchanged. + """ + bare: Final = split_mantle_region_prefix(model)[1] + for prefix in _MANTLE_GEO_PROFILE_PREFIXES: + if bare.startswith(prefix): + return bare[len(prefix) :] + return bare + + def resolve_mantle_region(params: Mapping[str, object]) -> str: region: Final = params.get("aws_region_name") if isinstance(region, str) and region: @@ -149,7 +180,7 @@ def mantle_supports_responses(model: str | None, model_cost: dict) -> bool: gpt-oss substring), so a substring gate would be wrong. A model absent from model_cost simply has no signal and returns False (chat-completions emulation). """ - entry: Final = model_cost.get(f"bedrock_mantle/{split_mantle_region_prefix(model)[1]}", {}) if model else {} + entry: Final = model_cost.get(f"bedrock_mantle/{mantle_cost_map_model_id(model)}", {}) if model else {} if "/v1/responses" in (entry.get("supported_endpoints") or []): return True return entry.get("mode") == "responses" @@ -166,5 +197,5 @@ def mantle_base_segment(model: str | None, model_cost: dict) -> str: the base for the model's whole OpenAI-compatible surface, so both the chat and responses configs derive from it -- there is no separate model-name rule. """ - entry: Final = model_cost.get(f"bedrock_mantle/{split_mantle_region_prefix(model)[1]}", {}) if model else {} + entry: Final = model_cost.get(f"bedrock_mantle/{mantle_cost_map_model_id(model)}", {}) if model else {} return "openai/v1" if entry.get("use_openai_responses_path") is True else "v1" diff --git a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py index a2289167ad4..921963c2b57 100644 --- a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py +++ b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py @@ -1352,6 +1352,22 @@ class TestMantleBaseSegment: ), ("openai.gpt-oss-120b", {}, "v1"), (None, {}, "v1"), + # Geo inference-profile prefixes must resolve to the bare cost-map row + ( + "us.openai.gpt-5.5", + {"bedrock_mantle/openai.gpt-5.5": {"use_openai_responses_path": True}}, + "openai/v1", + ), + ( + "eu.openai.gpt-oss-120b", + {"bedrock_mantle/openai.gpt-oss-120b": {}}, + "v1", + ), + ( + "us-east-1/us.openai.gpt-5.5", + {"bedrock_mantle/openai.gpt-5.5": {"use_openai_responses_path": True}}, + "openai/v1", + ), ], ) def test_base_segment(self, model, model_cost, expected): @@ -1365,6 +1381,43 @@ class TestMantleSupportsResponses: model-name match: per-model, so gpt-oss-120b is supported but the safeguard variant is not despite the shared substring.""" + @pytest.mark.parametrize( + "model", + [ + "openai.gpt-6.1-sol", + "us.openai.gpt-6.1-sol", + "eu.openai.gpt-6.1-sol", + "global.openai.gpt-6.1-sol", + "us-east-1/us.openai.gpt-6.1-sol", + ], + ) + def test_geo_inference_profile_resolves_cost_map_row(self, model): + from litellm.llms.bedrock_mantle.common_utils import ( + mantle_base_segment, + mantle_cost_map_model_id, + mantle_supports_responses, + ) + + model_cost = { + "bedrock_mantle/openai.gpt-6.1-sol": { + "mode": "responses", + "supported_endpoints": ["/v1/chat/completions", "/v1/responses"], + "use_openai_responses_path": True, + } + } + assert mantle_cost_map_model_id(model) == "openai.gpt-6.1-sol" + assert mantle_supports_responses(model, model_cost) is True + assert mantle_base_segment(model, model_cost) == "openai/v1" + + def test_unknown_geo_prefixed_model_stays_unsupported(self): + from litellm.llms.bedrock_mantle.common_utils import ( + mantle_base_segment, + mantle_supports_responses, + ) + + assert mantle_supports_responses("us.openai.unknown-model", {}) is False + assert mantle_base_segment("us.openai.unknown-model", {}) == "v1" + class TestBedrockMantlePerModelResponsesURL: """End-to-end: the registry-selected config must build the correct wire URL