Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1bbf9fb
feat(bedrock): route OpenAI models to bedrock-runtime's native Chat C…
6matt Sep 26, 2026
5678141
fix(bedrock): bridge function-tools + reasoning requests to native Re…
6matt Sep 26, 2026
4b67e19
style(bedrock): add __init__ return annotation for ruff strict gate (…
6matt Sep 26, 2026
25d0ab3
refactor(bedrock): drive tools+reasoning bridge from a price-map flag…
6matt Sep 26, 2026
59e1bf6
fix(bedrock): stop callers forging Bedrock request-attribution metada…
6matt Sep 26, 2026
55f819c
test(bedrock): cover native-chat dispatch/params; type __init__; regi…
6matt Sep 26, 2026
582fe71
fix(bedrock): stop extra_body from overriding the authorized model on…
6matt Sep 26, 2026
33c4c47
Merge remote-tracking branch 'upstream/main' into feat/bedrock-native…
6matt Sep 26, 2026
4466a96
test(bedrock): cover native-chat config to 100%; justify model-pin mu…
6matt Sep 26, 2026
cab8ae0
style(bedrock): keep the model-pin line single-line for ruff format
6matt Sep 26, 2026
bdcc4ae
style(bedrock): ruff-format the native-chat test file
6matt Sep 26, 2026
0d9ceab
test(bedrock): assert SigV4-signed body carries the pinned model
6matt Sep 26, 2026
c7d2829
test(bedrock): assert exact model field in signed body; drop redundan…
6matt Sep 26, 2026
b6959df
Merge remote-tracking branch 'upstream/main' into feat/bedrock-native…
6matt Sep 26, 2026
7719625
fix(bedrock): revert tools+reasoning price-map flag to a version check
6matt Sep 26, 2026
ad1f2a4
fix(cost-map): dedupe perceptron-mk1.5, keep newer capability flags
6matt Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions litellm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,9 @@ def set_global_gitlab_config(config: Dict[str, Any]) -> None:
from .llms.bedrock.chat.invoke_transformations.amazon_openai_transformation import (
AmazonBedrockOpenAIConfig as AmazonBedrockOpenAIConfig,
)
from .llms.bedrock.chat.openai_native.transformation import (
BedrockOpenAIChatConfig as BedrockOpenAIChatConfig,
)
from .llms.bedrock.image_generation.amazon_stability1_transformation import (
AmazonStabilityConfig as AmazonStabilityConfig,
)
Expand Down
5 changes: 5 additions & 0 deletions litellm/_lazy_imports_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@
"DatabricksResponsesAPIConfig",
"OpenRouterResponsesAPIConfig",
"BedrockOpenAIResponsesConfig",
"BedrockOpenAIChatConfig",
"BedrockMantleResponsesAPIConfig",
"GoogleAIStudioInteractionsConfig",
"VertexAIInteractionsConfig",
Expand Down Expand Up @@ -925,6 +926,10 @@
".llms.bedrock.responses.transformation",
"BedrockOpenAIResponsesConfig",
),
"BedrockOpenAIChatConfig": (
".llms.bedrock.chat.openai_native.transformation",
"BedrockOpenAIChatConfig",
),
"BedrockMantleChatConfig": (
".llms.bedrock_mantle.chat.transformation",
"BedrockMantleChatConfig",
Expand Down
Empty file.
191 changes: 191 additions & 0 deletions litellm/llms/bedrock/chat/openai_native/transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""Amazon Bedrock Runtime - native OpenAI Chat Completions API.

AWS serves the newer OpenAI models on ``bedrock-runtime`` through an
OpenAI-compatible surface at
``https://bedrock-runtime.{region}.{dns_suffix}/openai/v1/chat/completions``,
alongside Converse. Without this config the ``bedrock`` provider translates every
chat request into Converse, which is lossy for OpenAI-family models (verbosity,
reasoning items/tokens, ``prompt_cache_key``, OpenAI tool shapes).

This is the Chat Completions sibling of ``BedrockOpenAIResponsesConfig``
(litellm/llms/bedrock/responses/transformation.py). Payloads and SSE follow the
OpenAI Chat spec, so it inherits ``OpenAIGPT5Config`` -- the same param mapper the
``openai`` provider uses for the gpt-5/gpt-6 reasoning series -- and overrides only
the endpoint URL, authentication, and AWS SigV4 signing. Inheriting GPT5Config is
what makes the config reject-safe: bedrock-runtime enforces the reasoning-model
rules verbatim (only ``temperature=1``, no ``top_p``/``logprobs`` while reasoning),
and GPT5Config already drops or validates exactly those. A model that is not a
reasoning-series model falls back to the plain OpenAIGPT param mapping.

Auth: Bearer token (litellm_params.api_key or the standard AWS_BEARER_TOKEN_BEDROCK)
when present; otherwise AWS SigV4 (service "bedrock") over the standard credential
chain, signed via BaseAWSLLM._sign_request once the body is final.

Model IDs: bedrock-runtime serves these models only through a cross-Region
inference profile, so the body carries ``us.openai.gpt-6-luna`` /
``global.openai.gpt-6-luna`` verbatim; the bare ``openai.gpt-6-luna`` is rejected
("on-demand throughput isn't supported ... Retry ... with the ID or ARN of an
inference profile").
"""

from typing import Final

import httpx

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
from litellm.llms.bedrock.request_metadata import (
bedrock_request_metadata_headers,
merge_bedrock_invoke_headers,
)
from litellm.llms.bedrock.responses.transformation import resolve_bedrock_bearer_token
from litellm.llms.openai.chat.gpt_5_transformation import (
OpenAIGPT5Config,
is_gpt_reasoning_series_name,
)
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues

BEDROCK_RUNTIME_OPENAI_CHAT_PATH: Final = "/openai/v1/chat/completions"
# The same base suffixes the Responses config strips, plus the chat path itself,
# so an explicit api_base carrying any of them collapses to the runtime host.
BEDROCK_RUNTIME_OPENAI_BASE_SUFFIXES: Final = (
"/openai/v1/chat/completions",
"/openai/v1/responses",
"/v1/chat/completions",
"/openai/v1",
"/v1",
)


class BedrockOpenAIChatConfig(OpenAIGPT5Config, BaseAWSLLM):
"""Chat Completions config for the OpenAI models on the bedrock-runtime endpoint."""

def __init__(self) -> None:
OpenAIGPT5Config.__init__(self)
BaseAWSLLM.__init__(self)

@property
def custom_llm_provider(self) -> str | None:
return "bedrock"

def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException:
# bedrock-runtime returns x-amzn-RequestId; BedrockError preserves it.
return BedrockError(status_code=status_code, message=error_message, headers=headers)

def map_openai_params(
self,
non_default_params: dict,
optional_params: dict,
model: str,
drop_params: bool,
) -> dict:
# The reasoning-series mapper (temperature/top_p/logprobs guards) is correct only
# for gpt-5/gpt-6; anything else on this surface takes the plain OpenAI mapping.
if is_gpt_reasoning_series_name(model):
return super().map_openai_params(non_default_params, optional_params, model, drop_params)
return OpenAIGPTConfig().map_openai_params(non_default_params, optional_params, model, drop_params)

def get_supported_openai_params(self, model: str) -> list:
if is_gpt_reasoning_series_name(model):
return super().get_supported_openai_params(model)
return OpenAIGPTConfig().get_supported_openai_params(model)

def transform_request(
self,
model: str,
messages: list[AllMessageValues],
optional_params: dict,
litellm_params: dict,
headers: dict,
) -> dict:
# AWS auth params ride in optional_params for signing; they must not reach the
# request body (the endpoint rejects "Unknown parameter: 'aws_region_name'").
inference_params: Final = {
key: value for key, value in optional_params.items() if key not in self.aws_authentication_params
}
return super().transform_request(
model=model,
messages=messages,
optional_params=inference_params,
litellm_params=litellm_params,
headers=headers,
)

def get_complete_url(
self,
api_base: str | None,
api_key: str | None,
model: str,
optional_params: dict,
litellm_params: dict,
stream: bool | None = None,
) -> str:
region: Final = self._get_aws_region_name(optional_params=optional_params, model=model)
override: Final = (
api_base
or optional_params.get("aws_bedrock_runtime_endpoint")
or get_secret_str("AWS_BEDROCK_RUNTIME_ENDPOINT")
)
# Partition-aware: bedrock-runtime is amazonaws.com.cn in China and other
# suffixes in GovCloud/ISO, so defer to the shared endpoint builder.
host: Final = (
override or self._select_default_endpoint_url(endpoint_type="runtime", aws_region_name=region)
).rstrip("/")
base: Final = next(
(host[: -len(suffix)] for suffix in BEDROCK_RUNTIME_OPENAI_BASE_SUFFIXES if host.endswith(suffix)),
host,
)
return f"{base}{BEDROCK_RUNTIME_OPENAI_CHAT_PATH}"

def validate_environment(
self,
headers: dict,
model: str,
messages: list[AllMessageValues],
optional_params: dict,
litellm_params: dict,
api_key: str | None = None,
api_base: str | None = None,
) -> dict:
headers.setdefault("Content-Type", "application/json")
bearer: Final = resolve_bedrock_bearer_token(api_key)
if bearer:
headers["Authorization"] = f"Bearer {bearer}"
# The X-Amzn-Bedrock-Request-Metadata header lands in AWS billing/CloudTrail records,
# so the proxy owns it: caller-supplied copies are dropped and only the trusted pairs
# derived from litellm_params are signed, exactly as the Invoke OpenAI path does.
owned_names, metadata_headers = bedrock_request_metadata_headers(litellm_params)
return merge_bedrock_invoke_headers(headers, (), metadata_headers, owned_names)

def sign_request(
self,
headers: dict,
optional_params: dict,
request_data: dict,
api_base: str,
api_key: str | None = None,
model: str | None = None,
stream: bool | None = None,
fake_stream: bool | None = None,
) -> tuple[dict, bytes | None]:
# The model rides in the body on this surface (not the URL), and extra_body is merged
# over the body before signing, so extra_body={"model": ...} would otherwise invoke a
# model the key isn't authorized for. Pin the authorized model back before signing.
if model is not None:
request_data["model"] = model # rebind-ok: request_data is the body the handler signs
if resolve_bedrock_bearer_token(api_key):
# Bedrock API keys are Bearer credentials; SigV4 on top would be wrong.
return headers, None
return self._sign_request(
service_name="bedrock",
headers=headers,
optional_params=optional_params,
request_data=request_data,
api_base=api_base,
model=model,
stream=stream,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
fake_stream=fake_stream,
)
64 changes: 64 additions & 0 deletions litellm/llms/bedrock/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,60 @@ def bedrock_supports_openai_responses(model: str | None, model_cost: Mapping[str
)


def bedrock_supports_openai_chat(model: str | None, model_cost: Mapping[str, object]) -> bool:
"""Whether a Bedrock model is served by bedrock-runtime's OpenAI Chat Completions surface.

Same shape as ``bedrock_supports_openai_responses``: purely data-driven from the
model's ``supported_endpoints`` (``/v1/chat/completions``), overridable via
``register_model`` / proxy ``model_info``, with no model-name match. A model
absent from ``model_cost`` returns False, leaving the Converse route in place.
"""
if not model:
return False
candidates: Final = (model_cost.get(key) for key in (model, f"bedrock/{model}"))
return any(
isinstance(entry, Mapping) and "/v1/chat/completions" in (entry.get("supported_endpoints") or ())
for entry in candidates
)


def bedrock_chat_rejects_function_tools_while_reasoning(model: str) -> bool:
"""Whether bedrock-runtime's native chat surface rejects function tools while reasoning is active.

Measured against bedrock-runtime: gpt-5.6 and gpt-6 reject function tools whenever reasoning
is on (the default) -- "Function tools with reasoning_effort are not supported ... use
/v1/responses or set reasoning_effort to 'none'" -- while gpt-5.4/5.5 serve tools with
reasoning natively. Callers bridge the rejected requests to /v1/responses. This mirrors the
version-gated openai/azure arms in the same bridge (``is_model_gpt_5_4_plus_model``,
``foundry_chat_rejects_function_tools_while_reasoning``); a price-map flag was ruled out
because the cost-map guard runs the base branch's schema generator, which can't classify a
key a PR introduces. The Bedrock ``[region.]openai.<model>`` id is normalised to its
trailing OpenAI name so the gpt-version helper matches.
"""
if not model:
return False
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config

openai_name: Final = model.split("openai.")[-1]
return OpenAIGPT5Config.is_model_gpt_5_6_plus_model(openai_name)


def bedrock_uses_native_openai_chat(model: str) -> bool:
"""Whether ``model`` should use bedrock-runtime's native OpenAI Chat Completions surface.

True only when the model would otherwise default to Converse, wasn't explicitly
pinned to it (``bedrock/converse/<model>`` stays an escape hatch), and advertises
``/v1/chat/completions`` in ``supported_endpoints``. Shared by the chat-config
selector and the completion dispatcher so both agree on the route.
"""
if BedrockModelInfo.get_bedrock_route(model) != "converse":
return False
pinned_to_converse: Final = any(
model.startswith(prefix) or f"/{prefix}" in model for prefix in ("converse/", "converse_like/")
)
return not pinned_to_converse and bedrock_supports_openai_chat(model, litellm.model_cost)


def build_mantle_messages_url(
api_base: str | None,
aws_bedrock_runtime_endpoint: str | None,
Expand Down Expand Up @@ -1385,6 +1439,16 @@ def get_bedrock_chat_config(model: str):
bedrock_invoke_provider: Final = BaseAWSLLM.get_bedrock_invoke_provider(model=model)
base_model: Final = BedrockModelInfo.get_base_model(model)

# Native OpenAI Chat Completions surface on bedrock-runtime (data-driven, no
# model-name match). The dispatcher in main.py uses the same predicate to send
# these to base_llm_http_handler instead of the Converse handler.
if bedrock_uses_native_openai_chat(model):
from litellm.llms.bedrock.chat.openai_native.transformation import (
BedrockOpenAIChatConfig,
)

return BedrockOpenAIChatConfig()

# Handle explicit routes first
if bedrock_route == "claude_platform":
return litellm.BedrockClaudePlatformConfig()
Expand Down
35 changes: 33 additions & 2 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@
from litellm.llms.base_llm.base_model_iterator import (
convert_model_response_to_streaming,
)
from litellm.llms.bedrock.common_utils import BedrockModelInfo
from litellm.llms.bedrock.common_utils import (
BedrockModelInfo,
bedrock_chat_rejects_function_tools_while_reasoning,
bedrock_supports_openai_responses,
bedrock_uses_native_openai_chat,
)
from litellm.llms.cohere.common_utils import CohereModelInfo
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler, http2_enabled
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
Expand Down Expand Up @@ -1143,6 +1148,12 @@ def responses_api_bridge_check(
on_foundry_openai_endpoint: Final = custom_llm_provider == "azure_ai" and azure_ai_supports_native_responses(
model, api_base
)
# bedrock-runtime serves the OpenAI models on a native /v1/responses surface, so the
# function-tools-with-reasoning requests its chat surface rejects can bridge there
# rather than falling back to the lossy Converse translation.
on_bedrock_native_responses: Final = custom_llm_provider == "bedrock" and bedrock_supports_openai_responses(
model, litellm.model_cost
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
on_constraint_enforcing_endpoint: Final = (
custom_llm_provider == "azure" or resolved_api_base == "" or _is_openai_backed_api_base(resolved_api_base)
)
Expand All @@ -1152,14 +1163,16 @@ def responses_api_bridge_check(
and (
foundry_chat_rejects_function_tools_while_reasoning(model, reasoning_effort)
if on_foundry_openai_endpoint
else bedrock_chat_rejects_function_tools_while_reasoning(model)
if on_bedrock_native_responses
else (
OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model)
and (reasoning_effort is not None or on_constraint_enforcing_endpoint)
)
)
)
if (
(custom_llm_provider in ("openai", "azure") or on_foundry_openai_endpoint)
(custom_llm_provider in ("openai", "azure") or on_foundry_openai_endpoint or on_bedrock_native_responses)
and model_info.get("mode") != "responses"
and OpenAIGPT5Config.is_model_gpt_5_model(model)
and not OpenAIGPT5Config.is_model_gpt_5_search_model(model)
Expand Down Expand Up @@ -4212,6 +4225,24 @@ def _complete_bedrock(ctx: _CompletionDispatchContext) -> _CompletionDispatchRes
client=client,
provider_config=provider_config,
)
elif bedrock_uses_native_openai_chat(model):
Comment thread
veria-ai[bot] marked this conversation as resolved.
response = base_llm_http_handler.completion(
model=model,
stream=stream,
messages=messages,
acompletion=acompletion,
api_base=api_base,
model_response=model_response,
optional_params=optional_params,
litellm_params=litellm_params,
custom_llm_provider="bedrock",
timeout=timeout,
headers=headers,
encoding=_get_encoding(),
api_key=api_key,
logging_obj=logging,
client=client,
)
elif bedrock_route == "converse":
model = model.replace("converse/", "")
response = bedrock_converse_chat_completion.completion(
Expand Down
Loading
Loading