from __future__ import annotations import os import re import inspect from typing import Literal, Callable, Awaitable, cast from dataclasses import field, dataclass import httpx from .._types import NOT_GIVEN, NotGiven from .._utils import asyncify from .._models import FinalRequestOptions from .._provider import _Provider, _create_provider, _ProviderRuntime from .._exceptions import OpenAIError from ..lib._bedrock_auth import ( BedrockAwsAuth, BedrockAwsAuthConfig, AwsCredentialsProvider, ) BedrockTokenProvider = Callable[[], "str | Awaitable[str]"] _AWS_SIGNING_HEADERS = ("authorization", "x-amz-content-sha256", "x-amz-date", "x-amz-security-token") _CANONICAL_BEDROCK_HOST = re.compile(r"^bedrock-mantle\.([a-z0-9-]+)\.api\.aws$", re.IGNORECASE) def _normalize_optional_string(value: str | None) -> str | None: if value is None: return None normalized = value.strip() return normalized or None def _normalize_base_url(base_url: str | httpx.URL) -> httpx.URL: url = httpx.URL(base_url) path = url.path.rstrip("/") responses_match = re.search(r"/responses(?:/.*)?$", path) if responses_match is not None: path = path[: responses_match.start()] return url.copy_with(path=path or "/") def _same_origin(left: httpx.URL, right: httpx.URL) -> bool: return (left.scheme, left.host, left.port) == (right.scheme, right.host, right.port) def _body_for_signing(request: httpx.Request) -> bytes: try: return request.content except httpx.RequestNotRead as exc: raise OpenAIError( "Bedrock SigV4 authentication requires a replayable request body. " "Buffer the body before sending or use bearer authentication." ) from exc def _assert_provider_owns_authorization(request: httpx.Request) -> None: if "Authorization" in request.headers: raise OpenAIError("Bedrock provider authentication cannot be combined with a custom `Authorization` header.") def _without_redirects(options: FinalRequestOptions) -> FinalRequestOptions: if options.follow_redirects: raise OpenAIError( "Bedrock SigV4 authentication does not support automatic redirects. " "Send a new request to the redirect target so it can be signed again." ) options.follow_redirects = False return options class _BedrockBearerAuth: def __init__(self, token_provider: BedrockTokenProvider, *, base_url: httpx.URL) -> None: self._token_provider = token_provider self._base_url = base_url def _validate_request(self, request: httpx.Request) -> None: _assert_provider_owns_authorization(request) if not _same_origin(request.url, self._base_url): raise OpenAIError( "Refusing to authenticate a Bedrock request for an origin other than the configured provider URL." ) def _resolve_token(self) -> str: try: token = cast(object, self._token_provider()) except OpenAIError: raise except Exception as exc: raise OpenAIError("Failed to resolve a bearer credential for Bedrock.") from exc if inspect.isawaitable(token): close = getattr(token, "close", None) if callable(close): close() raise OpenAIError("An async Bedrock token provider requires `AsyncOpenAI`.") if not isinstance(token, str) or not token.strip(): raise OpenAIError("The Bedrock bearer credential provider must return a non-empty string.") return token async def _resolve_token_async(self) -> str: try: token = cast(object, self._token_provider()) if inspect.isawaitable(token): token = await token except OpenAIError: raise except Exception as exc: raise OpenAIError("Failed to resolve a bearer credential for Bedrock.") from exc if not isinstance(token, str) or not token.strip(): raise OpenAIError("The Bedrock bearer credential provider must return a non-empty string.") return token def prepare_request(self, request: httpx.Request) -> None: self._validate_request(request) request.headers["Authorization"] = f"Bearer {self._resolve_token()}" async def prepare_async_request(self, request: httpx.Request) -> None: self._validate_request(request) request.headers["Authorization"] = f"Bearer {await self._resolve_token_async()}" class _BedrockSigV4Auth: def __init__( self, *, config: BedrockAwsAuthConfig, base_url: httpx.URL, auth: BedrockAwsAuth | None = None, ) -> None: self._config = config self._base_url = base_url self._auth = auth def _validate_request(self, request: httpx.Request) -> bytes: _assert_provider_owns_authorization(request) if not _same_origin(request.url, self._base_url): raise OpenAIError( "Refusing to sign a Bedrock request for an origin other than the configured provider URL." ) endpoint_region_match = _CANONICAL_BEDROCK_HOST.fullmatch(request.url.host) if endpoint_region_match is not None and endpoint_region_match.group(1) != self._config.region: raise OpenAIError( f"The Bedrock endpoint region `{endpoint_region_match.group(1)}` does not match the " f"SigV4 region `{self._config.region}`." ) return _body_for_signing(request) def _sign(self, request: httpx.Request, *, auth: BedrockAwsAuth, body: bytes) -> None: for header in _AWS_SIGNING_HEADERS: request.headers.pop(header, None) signed_headers = auth.sign( method=request.method, url=str(request.url), headers=dict(request.headers), body=body, ) request.headers.clear() request.headers.update(signed_headers) def prepare_request(self, request: httpx.Request) -> None: body = self._validate_request(request) if self._auth is None: self._auth = BedrockAwsAuth(self._config) self._sign(request, auth=self._auth, body=body) async def prepare_async_request(self, request: httpx.Request) -> None: body = self._validate_request(request) if self._auth is None: self._auth = await asyncify(BedrockAwsAuth)(self._config) signed_headers = await asyncify(self._auth.sign)( method=request.method, url=str(request.url), headers={ name: value for name, value in request.headers.items() if name.lower() not in _AWS_SIGNING_HEADERS }, body=body, ) request.headers.clear() request.headers.update(signed_headers) @dataclass class _BedrockProviderRuntime(_ProviderRuntime): region: str | None = None @dataclass(frozen=True) class _BedrockProviderDefinition: configured_region: str | None region_source: Literal["explicit", "environment"] | None configured_base_url: httpx.URL | None api_key: str | None = field(default=None, repr=False) token_provider: BedrockTokenProvider | None = field(default=None, repr=False, compare=False) use_environment_bearer: bool = False profile: str | None = None access_key_id: str | None = field(default=None, repr=False) secret_access_key: str | None = field(default=None, repr=False) session_token: str | None = field(default=None, repr=False) credential_provider: AwsCredentialsProvider | None = field(default=None, repr=False, compare=False) name: str = field(default="bedrock", init=False) def _aws_source(self) -> Literal["static", "profile", "provider", "default"]: if self.access_key_id is not None: return "static" if self.profile is not None: return "profile" if self.credential_provider is not None: return "provider" return "default" def _resolve_aws_auth(self) -> tuple[BedrockAwsAuthConfig, BedrockAwsAuth | None]: if self.configured_region is not None: return ( BedrockAwsAuthConfig( region=self.configured_region, source=self._aws_source(), region_source=self.region_source or "explicit", profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, session_token=self.session_token, credentials_provider=self.credential_provider, ), None, ) auth = BedrockAwsAuth.resolve( region=None, profile=self.profile, access_key_id=self.access_key_id, secret_access_key=self.secret_access_key, session_token=self.session_token, credentials_provider=self.credential_provider, ) return auth.config, auth def configure(self) -> _ProviderRuntime: def environment_token() -> str: token = os.environ.get("AWS_BEARER_TOKEN_BEDROCK") if not token: raise OpenAIError( "Could not find credentials for Bedrock. Pass a bearer credential or AWS credentials to " "`bedrock(...)`, set `AWS_BEARER_TOKEN_BEDROCK`, or configure the default AWS credential chain." ) return token auth: _BedrockBearerAuth | _BedrockSigV4Auth | None = None bearer_provider: BedrockTokenProvider | None = None if self.api_key is not None: bearer_provider = lambda: self.api_key or "" region = self.configured_region elif self.token_provider is not None: bearer_provider = self.token_provider region = self.configured_region elif self.use_environment_bearer: bearer_provider = environment_token region = self.configured_region else: aws_config, aws_auth = self._resolve_aws_auth() region = aws_config.region base_url = self.configured_base_url or _normalize_base_url( f"https://bedrock-mantle.{region}.api.aws/openai/v1" ) auth = _BedrockSigV4Auth(config=aws_config, base_url=base_url, auth=aws_auth) if self.configured_base_url is not None: base_url = self.configured_base_url elif region is not None: base_url = _normalize_base_url(f"https://bedrock-mantle.{region}.api.aws/openai/v1") else: raise OpenAIError( "Bedrock requires an AWS region. Pass `region` to `bedrock(...)`, or set `AWS_REGION` or " "`AWS_DEFAULT_REGION`." ) if bearer_provider is not None: auth = _BedrockBearerAuth(bearer_provider, base_url=base_url) assert auth is not None if isinstance(auth, _BedrockSigV4Auth): return _BedrockProviderRuntime( name=self.name, base_url=base_url, region=region, transform_request=_without_redirects, prepare_request=auth.prepare_request, prepare_async_request=auth.prepare_async_request, ) return _BedrockProviderRuntime( name=self.name, base_url=base_url, region=region, prepare_request=auth.prepare_request, prepare_async_request=auth.prepare_async_request, ) def bedrock( *, region: str | None = None, base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, api_key: str | None | NotGiven = NOT_GIVEN, token_provider: BedrockTokenProvider | None = None, access_key_id: str | None = None, secret_access_key: str | None = None, session_token: str | None = None, profile: str | None = None, credential_provider: AwsCredentialsProvider | None = None, ) -> _Provider: """Configure the standard OpenAI client for Amazon Bedrock Mantle.""" normalized_region = _normalize_optional_string(region) if region is not None and normalized_region is None: raise OpenAIError("The Bedrock AWS `region` must not be empty.") region_source: Literal["explicit", "environment"] | None = None if normalized_region is not None: region_source = "explicit" else: normalized_region = _normalize_optional_string( os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") ) if normalized_region is not None: region_source = "environment" configured_base_url: httpx.URL | None if isinstance(base_url, NotGiven): environment_base_url = _normalize_optional_string(os.environ.get("AWS_BEDROCK_BASE_URL")) configured_base_url = _normalize_base_url(environment_base_url) if environment_base_url else None elif base_url is None: configured_base_url = None else: if isinstance(base_url, str) and not base_url.strip(): raise OpenAIError("The Bedrock `base_url` must not be empty.") configured_base_url = _normalize_base_url(base_url) normalized_profile = _normalize_optional_string(profile) if profile is not None and normalized_profile is None: raise OpenAIError("The Bedrock AWS `profile` must not be empty.") if (access_key_id is None) != (secret_access_key is None) or (session_token is not None and access_key_id is None): raise OpenAIError( "Static AWS credentials require both `access_key_id` and `secret_access_key`. " "A `session_token` may only be used with both." ) if access_key_id is not None and (not access_key_id.strip() or not cast(str, secret_access_key).strip()): raise OpenAIError("Static AWS credentials require non-empty `access_key_id` and `secret_access_key` values.") if session_token is not None and not session_token.strip(): raise OpenAIError("A static AWS `session_token` must not be empty when provided.") explicit_api_key = not isinstance(api_key, NotGiven) and api_key is not None if explicit_api_key and (not isinstance(api_key, str) or not api_key.strip()): raise OpenAIError("The Bedrock bearer credential must not be empty.") if explicit_api_key and token_provider is not None: raise OpenAIError("The `api_key` and `token_provider` options are mutually exclusive. Configure only one.") explicit_bearer = explicit_api_key or token_provider is not None aws_modes = sum( ( access_key_id is not None, normalized_profile is not None, credential_provider is not None, ) ) if aws_modes > 1: raise OpenAIError( "Bedrock authentication is ambiguous. Configure exactly one explicit AWS mode: static credentials, " "profile, or credential provider." ) if explicit_bearer and aws_modes: raise OpenAIError( "Bedrock authentication is ambiguous. Configure exactly one explicit mode: bearer credential, " "static AWS credentials, profile, or credential provider." ) skip_environment_bearer = not isinstance(api_key, NotGiven) and api_key is None use_environment_bearer = ( not explicit_bearer and not aws_modes and not skip_environment_bearer and bool(os.environ.get("AWS_BEARER_TOKEN_BEDROCK")) ) return _create_provider( _BedrockProviderDefinition( configured_region=normalized_region, region_source=region_source, configured_base_url=configured_base_url, api_key=cast("str | None", api_key) if explicit_api_key else None, token_provider=token_provider, use_environment_bearer=use_environment_bearer, profile=normalized_profile, access_key_id=access_key_id, secret_access_key=secret_access_key, session_token=session_token, credential_provider=credential_provider, ) ) __all__ = ["bedrock", "BedrockTokenProvider", "AwsCredentialsProvider"]