From b90c2f113d1091896bb80988c790898320678f00 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:39:18 -0700 Subject: [PATCH] fix(bedrock): serve region-path and GovCloud gpt-oss ids on native Chat Completions The cost-map parity tests require every regional variant of a flagged id to carry the same supports_ flags, so the six us-gov gpt-oss entries now carry the native-route flags too. A region path in the model name (bedrock/us-gov-west-1/openai.gpt-oss-20b-1:0) is routing, not a different model: the route is looked up on the id after the path, the path's region picks the endpoint and the SigV4 scope, an explicit aws_region_name still wins, and the body carries the bare id AWS expects --- .../chat/chat_completions/transformation.py | 18 ++++++--- litellm/llms/bedrock/common_utils.py | 18 ++++++++- ...odel_prices_and_context_window_backup.json | 12 ++++++ model_prices_and_context_window.json | 12 ++++++ ...bedrock_chat_completions_transformation.py | 38 ++++++++++++++++++- 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/litellm/llms/bedrock/chat/chat_completions/transformation.py b/litellm/llms/bedrock/chat/chat_completions/transformation.py index 18ba06fee0b..a7926fb252e 100644 --- a/litellm/llms/bedrock/chat/chat_completions/transformation.py +++ b/litellm/llms/bedrock/chat/chat_completions/transformation.py @@ -24,7 +24,7 @@ from typing_extensions import assert_never import litellm from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM -from litellm.llms.bedrock.common_utils import BedrockError, strip_bedrock_routing_prefix +from litellm.llms.bedrock.common_utils import BedrockError, split_bedrock_region_path from litellm.llms.openai.chat.gpt_transformation import OpenAIChatCompletionStreamingHandler from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig from litellm.types.llms.openai import AllMessageValues @@ -196,7 +196,7 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig): if api_base is not None and "chat/completions" in api_base: return api_base.rstrip("/") aws_region_name: Final = self._aws_signer._get_aws_region_name( # pyright: ignore[reportPrivateUsage] # BaseAWSLLM has no public region resolver - optional_params=optional_params, model=model + optional_params=self._params_with_region_from_path(optional_params, model), model=model ) endpoint_url, _ = self._aws_signer.get_runtime_endpoint( api_base=api_base, @@ -210,6 +210,14 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig): return f"{base}/chat/completions" return f"{base}/openai/v1/chat/completions" + def _params_with_region_from_path( + self, optional_params: dict, model: str | None + ) -> dict: # mutable-ok: BaseAWSLLM's region resolver and signer take a plain dict + region_from_path, _ = split_bedrock_region_path(model or "") + if region_from_path is None or optional_params.get("aws_region_name") is not None: + return optional_params + return {**optional_params, "aws_region_name": region_from_path} # mutable-ok: BaseAWSLLM takes a plain dict + def sign_request( self, headers: dict, # mutable-ok: BaseConfig signature @@ -224,7 +232,7 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig): return self._aws_signer._sign_request( # pyright: ignore[reportPrivateUsage] # BaseAWSLLM has no public signer service_name="bedrock", headers=headers, - optional_params=optional_params, + optional_params=self._params_with_region_from_path(optional_params, model), request_data=request_data, api_base=api_base, api_key=api_key, @@ -268,7 +276,7 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig): headers: dict, # mutable-ok: BaseConfig signature ) -> dict: # mutable-ok: BaseConfig signature return super().transform_request( - model=strip_bedrock_routing_prefix(model), + model=split_bedrock_region_path(model)[1], messages=messages, optional_params=self._inference_params(optional_params), litellm_params=litellm_params, @@ -284,7 +292,7 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig): headers: dict, # mutable-ok: BaseConfig signature ) -> dict: # mutable-ok: BaseConfig signature return await super().async_transform_request( - model=strip_bedrock_routing_prefix(model), + model=split_bedrock_region_path(model)[1], messages=messages, optional_params=self._inference_params(optional_params), litellm_params=litellm_params, diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index df30eddd856..3b624b4ab02 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -795,8 +795,24 @@ def strip_bedrock_routing_prefix(model: str) -> str: return model +def split_bedrock_region_path(model: str) -> tuple[str | None, str]: + """Split a ``/`` routing path into the region and the id AWS receives. + + ``bedrock/us-gov-west-1/openai.gpt-oss-20b-1:0`` -> ``("us-gov-west-1", "openai.gpt-oss-20b-1:0")``; + a model without a region path comes back as ``(None, )``. + """ + stripped: Final = strip_bedrock_routing_prefix(model) + region, separator, model_id = stripped.partition("/") + if separator and region in _get_all_bedrock_regions(): + return region, model_id + return None, stripped + + def _bedrock_price_map_flag(model: str, flag: str) -> bool: - entries: Final = (litellm.model_cost.get(key) for key in (model, strip_bedrock_routing_prefix(model))) + entries: Final = ( + litellm.model_cost.get(key) + for key in (model, strip_bedrock_routing_prefix(model), split_bedrock_region_path(model)[1]) + ) return any(entry is not None and entry.get(flag) is True for entry in entries) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 6dbf246fd70..db0e3a4dc18 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -47214,6 +47214,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3.6e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -47227,6 +47229,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 7.2e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64459,6 +64463,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3.6e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64472,6 +64478,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 7.2e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64665,6 +64673,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3.6e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64678,6 +64688,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 7.2e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 6dbf246fd70..db0e3a4dc18 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -47214,6 +47214,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3.6e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -47227,6 +47229,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 7.2e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64459,6 +64463,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3.6e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64472,6 +64478,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 7.2e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64665,6 +64673,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3.6e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -64678,6 +64688,8 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 7.2e-07, + "supports_bedrock_runtime_chat_completions": true, + "supports_bedrock_runtime_chat_completions_tools_with_reasoning": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, diff --git a/tests/test_litellm/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py b/tests/test_litellm/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py index ce2e5b7e8f7..83fe7628e0a 100644 --- a/tests/test_litellm/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/chat_completions/test_bedrock_chat_completions_transformation.py @@ -163,6 +163,33 @@ def test_completion_posts_runtime_chat_completions(local_cost_map, fake_aws_env) assert "inferenceConfig" not in body +def test_region_path_sends_the_bare_model_id_to_the_path_region(local_cost_map, fake_aws_env): + requests, client = _recording_client(json=_chat_completion_json("ok", "openai.gpt-oss-20b-1:0")) + litellm.completion( + model="bedrock/us-gov-west-1/openai.gpt-oss-20b-1:0", + messages=[{"role": "user", "content": "hello"}], + client=client, + ) + + assert str(requests[0].url) == "https://bedrock-runtime.us-gov-west-1.amazonaws.com/openai/v1/chat/completions" + assert json.loads(requests[0].content)["model"] == "openai.gpt-oss-20b-1:0" + assert "/us-gov-west-1/bedrock/aws4_request" in requests[0].headers["Authorization"] + + +def test_explicit_aws_region_name_wins_over_the_region_path(local_cost_map, fake_aws_env): + requests, client = _recording_client(json=_chat_completion_json("ok", "openai.gpt-oss-20b-1:0")) + litellm.completion( + model="bedrock/us-gov-west-1/openai.gpt-oss-20b-1:0", + messages=[{"role": "user", "content": "hello"}], + aws_region_name="us-gov-east-1", + client=client, + ) + + assert str(requests[0].url) == "https://bedrock-runtime.us-gov-east-1.amazonaws.com/openai/v1/chat/completions" + assert json.loads(requests[0].content)["model"] == "openai.gpt-oss-20b-1:0" + assert "/us-gov-east-1/bedrock/aws4_request" in requests[0].headers["Authorization"] + + OPENAI_RUNTIME_MODELS = ( "openai.gpt-oss-20b-1:0", "openai.gpt-oss-120b-1:0", @@ -182,7 +209,16 @@ GET_WEATHER_TOOL = { } -@pytest.mark.parametrize("model", [*OPENAI_RUNTIME_MODELS, "bedrock/openai.gpt-oss-20b-1:0"]) +@pytest.mark.parametrize( + "model", + [ + *OPENAI_RUNTIME_MODELS, + "bedrock/openai.gpt-oss-20b-1:0", + "us-gov.openai.gpt-oss-20b-1:0", + "bedrock/us-gov-west-1/openai.gpt-oss-20b-1:0", + "us-gov-east-1/openai.gpt-oss-120b-1:0", + ], +) def test_openai_runtime_models_use_chat_completions_route(local_cost_map, model): assert uses_bedrock_runtime_chat_completions(model) is True assert BedrockModelInfo.get_bedrock_route(model) == "chat_completions"