mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
Adds litellm/litellm_core_utils/aws_partition.py mapping a region to its AWS partition (aws, aws-cn, aws-us-gov, and the iso partitions), its DNS suffix, and its ARN prefix, and uses it at every AWS host and ARN build site: bedrock (runtime, agent, agentcore, legacy client, batches, files, realtime), sagemaker, polly, secrets manager, s3 log uploads, bedrock passthrough routes, and rag ingestion. ARN detection now accepts arn:aws-cn: and arn:aws-us-gov: prefixes. STS region resolution now falls back to the configured aws_region_name after the aws_sts_endpoint host and the AWS_REGION/AWS_DEFAULT_REGION env vars, so cn and gov role assumption no longer silently signs against us-west-2. A partition sweep test walks every endpoint builder with cn regions and asserts no amazonaws.com host or arn:aws: prefix comes out, plus an AST guard that fails on any new f-string hardcoding either literal.
155 lines
6.6 KiB
Python
155 lines
6.6 KiB
Python
from collections.abc import Mapping
|
|
from typing import Final
|
|
from urllib.parse import parse_qs
|
|
|
|
import httpx
|
|
|
|
from litellm._logging import verbose_logger
|
|
from litellm.constants import PASS_THROUGH_HEADER_PREFIX
|
|
from litellm.litellm_core_utils.aws_partition import contains_aws_arn
|
|
|
|
# Headers that must not be overwritten via the x-pass- forwarding mechanism.
|
|
# Includes standard credential/auth headers and protocol-level headers that
|
|
# affect routing or message framing.
|
|
_PASS_THROUGH_PROTECTED_HEADERS: Final[frozenset] = frozenset(
|
|
{
|
|
"authorization",
|
|
"api-key",
|
|
"x-api-key",
|
|
"x-goog-api-key",
|
|
"host",
|
|
"content-length",
|
|
"accept-encoding",
|
|
}
|
|
)
|
|
|
|
# Header name prefix used to block AWS SigV4 signing headers from being overridden.
|
|
_PASS_THROUGH_PROTECTED_HEADER_PREFIXES: Final[tuple] = ("x-amz-",)
|
|
|
|
|
|
class BasePassthroughUtils:
|
|
@staticmethod
|
|
def get_merged_query_parameters(
|
|
existing_url: httpx.URL,
|
|
request_query_params: Mapping[str, str | list],
|
|
default_query_params: dict[str, str | list] | None = None,
|
|
) -> dict[str, str | list[str]]:
|
|
# Get the existing query params from the target URL
|
|
existing_query_string: Final = existing_url.query.decode("utf-8")
|
|
existing_query_params: Final = parse_qs(existing_query_string)
|
|
|
|
# parse_qs returns a dict where each value is a list, so let's flatten it
|
|
updated_existing_query_params: Final = {k: v[0] if len(v) == 1 else v for k, v in existing_query_params.items()}
|
|
|
|
# Start with default query params (lowest priority)
|
|
merged_params: Final = {}
|
|
if default_query_params:
|
|
merged_params.update(default_query_params)
|
|
|
|
# Override with existing URL query params (medium priority)
|
|
merged_params.update(updated_existing_query_params)
|
|
|
|
# Override with request query params (highest priority - client can override anything)
|
|
merged_params.update(request_query_params)
|
|
|
|
return merged_params
|
|
|
|
@staticmethod
|
|
def forward_headers_from_request(
|
|
request_headers: dict,
|
|
headers: dict,
|
|
forward_headers: bool | None = False,
|
|
):
|
|
"""
|
|
Helper to forward headers from original request.
|
|
|
|
Also handles 'x-pass-' prefixed headers which are always forwarded
|
|
with the prefix stripped, regardless of forward_headers setting.
|
|
e.g., 'x-pass-anthropic-beta: value' becomes 'anthropic-beta: value'
|
|
"""
|
|
if forward_headers is True:
|
|
# Header We Should NOT forward
|
|
request_headers.pop("content-length", None)
|
|
request_headers.pop("host", None)
|
|
# accept-encoding must stay client-negotiated: forwarding e.g. "br" when
|
|
# the brotli package is absent relays undecodable bytes to the caller
|
|
request_headers.pop("accept-encoding", None)
|
|
|
|
custom_header_names: Final = {header_name.lower() for header_name in headers}
|
|
for header_name in list(request_headers.keys()):
|
|
if header_name.lower() in custom_header_names:
|
|
request_headers.pop(header_name, None)
|
|
|
|
# Combine request headers with custom headers
|
|
headers = {**request_headers, **headers}
|
|
|
|
# Process x-pass- prefixed headers (strip prefix and forward)
|
|
# Credential and protocol-level headers are excluded from this mechanism.
|
|
for header_name, header_value in request_headers.items():
|
|
if header_name.lower().startswith(PASS_THROUGH_HEADER_PREFIX):
|
|
# Strip the 'x-pass-' prefix and normalize to lowercase
|
|
actual_header_name = header_name[len(PASS_THROUGH_HEADER_PREFIX) :].lower()
|
|
if actual_header_name in _PASS_THROUGH_PROTECTED_HEADERS or any(
|
|
actual_header_name.startswith(p) for p in _PASS_THROUGH_PROTECTED_HEADER_PREFIXES
|
|
):
|
|
verbose_logger.debug(
|
|
"x-pass- header %s maps to a protected header name; skipping",
|
|
header_name,
|
|
)
|
|
continue
|
|
headers[actual_header_name] = header_value
|
|
|
|
return headers
|
|
|
|
|
|
class CommonUtils:
|
|
@staticmethod
|
|
def encode_bedrock_runtime_modelid_arn(endpoint: str) -> str:
|
|
"""
|
|
Encodes any "/" found in the modelId of an AWS Bedrock Runtime Endpoint when arns are passed in.
|
|
- modelID value can be an ARN which contains slashes that SHOULD NOT be treated as path separators.
|
|
e.g endpoint: /model/<modelId>/invoke
|
|
<modelId> containing arns with slashes need to be encoded from
|
|
arn:aws:bedrock:ap-southeast-1:123456789012:application-inference-profile/abdefg12334 =>
|
|
arn:aws:bedrock:ap-southeast-1:123456789012:application-inference-profile%2Fabdefg12334
|
|
so that it is treated as one part of the path.
|
|
Otherwise, the encoded endpoint will return 500 error when passed to Bedrock endpoint.
|
|
|
|
See the apis in https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Amazon_Bedrock_Runtime.html
|
|
for more details on the regex patterns of modelId which we use in the regex logic below.
|
|
|
|
Args:
|
|
endpoint (str): The original endpoint string which may contain ARNs that contain slashes.
|
|
|
|
Returns:
|
|
str: The endpoint with properly encoded ARN slashes
|
|
"""
|
|
import re
|
|
|
|
# Early exit: if no ARN detected, return unchanged
|
|
if not contains_aws_arn(endpoint):
|
|
return endpoint
|
|
|
|
# Handle all patterns in one go - more efficient and cleaner
|
|
patterns: Final = [
|
|
# Custom model with 2 slashes (order matters - do this first)
|
|
(r"(custom-model)/([a-z0-9.-]+)/([a-z0-9]+)", r"\1%2F\2%2F\3"),
|
|
# All other resource types with 1 slash
|
|
(r"(:application-inference-profile)/", r"\1%2F"),
|
|
(r"(:inference-profile)/", r"\1%2F"),
|
|
(r"(:foundation-model)/", r"\1%2F"),
|
|
(r"(:imported-model)/", r"\1%2F"),
|
|
(r"(:provisioned-model)/", r"\1%2F"),
|
|
(r"(:prompt)/", r"\1%2F"),
|
|
(r"(:endpoint)/", r"\1%2F"),
|
|
(r"(:prompt-router)/", r"\1%2F"),
|
|
(r"(:default-prompt-router)/", r"\1%2F"),
|
|
]
|
|
|
|
for pattern, replacement in patterns:
|
|
# Check if pattern exists before applying regex (early exit optimization)
|
|
if re.search(pattern, endpoint):
|
|
endpoint = re.sub(pattern, replacement, endpoint)
|
|
break # Exit after first match since each ARN has only one resource type
|
|
|
|
return endpoint
|