fix(bedrock): serve region-path and GovCloud gpt-oss ids on native Chat Completions
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

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
This commit is contained in:
mateo-berri 2026-09-19 20:39:18 -07:00
parent a1c089f107
commit b90c2f113d
5 changed files with 91 additions and 7 deletions

View file

@ -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,

View file

@ -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 ``<region>/<model-id>`` 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, <routing-prefix-stripped id>)``.
"""
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)

View file

@ -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,

View file

@ -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,

View file

@ -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"