This commit is contained in:
devin-ai-integration[bot] 2026-11-04 00:50:46 +00:00 • committed by GitHub
commit d18b86d2f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 196 additions and 41 deletions

View file

@ -215,9 +215,25 @@ class BearerRequestTarget(BedrockRequestTarget):
credentials: None = None
def bedrock_bearer_token(api_key: str | None) -> str | None:
token: Final = api_key if api_key is not None else get_secret_str("AWS_BEARER_TOKEN_BEDROCK")
return token or None
def _has_explicit_sigv4_params(auth_params: AwsAuthParams | None) -> bool:
return auth_params is not None and any(
(
auth_params.aws_access_key_id,
auth_params.aws_secret_access_key,
auth_params.aws_session_token,
auth_params.aws_role_name,
auth_params.aws_profile_name,
auth_params.aws_web_identity_token,
)
)
def bedrock_bearer_token(api_key: str | None, auth_params: AwsAuthParams | None = None) -> str | None:
if api_key is not None:
return api_key or None
if _has_explicit_sigv4_params(auth_params):
return None
return get_secret_str("AWS_BEARER_TOKEN_BEDROCK") or None
class _WebIdentityTokenClaims(LiteLLMBaseModel):
@ -1635,7 +1651,9 @@ class BaseAWSLLM(SignsRequestsWithAWS):
api_key: str | None = None,
supports_bearer_token: bool = True,
) -> AWSPreparedRequest | BearerPreparedRequest:
aws_bearer_token: Final = bedrock_bearer_token(api_key) if supports_bearer_token else None
aws_bearer_token: Final = (
bedrock_bearer_token(api_key) if supports_bearer_token and (api_key or credentials is None) else None
)
if aws_bearer_token is not None:
headers["Authorization"] = f"Bearer {aws_bearer_token}"
@ -1737,7 +1755,8 @@ class BaseAWSLLM(SignsRequestsWithAWS):
Returns:
Tuple[dict, Optional[str]]: A tuple containing the headers and the json str body of the request
"""
aws_bearer_token: Final = bedrock_bearer_token(api_key)
auth_params: Final = AwsAuthParams.model_validate(optional_params)
aws_bearer_token: Final = bedrock_bearer_token(api_key, auth_params)
if aws_bearer_token is not None:
headers = headers or {}
@ -1751,7 +1770,6 @@ class BaseAWSLLM(SignsRequestsWithAWS):
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
auth_params: Final = AwsAuthParams.model_validate(optional_params)
aws_region_name: Final = self._get_aws_region_name(optional_params=optional_params, model=model)
credentials: Final[Credentials] = self.resolve_credentials(auth_params, aws_region_name)

View file

@ -47,6 +47,7 @@ from litellm.llms.bedrock.common_utils import (
)
from litellm.llms.openai.chat.gpt_transformation import OpenAIChatCompletionStreamingHandler
from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig
from litellm.types.llms.bedrock import AwsAuthParams
from litellm.types.llms.openai import AllMessageValues
from litellm.types.utils import Choices, ModelResponse, ModelResponseStream
@ -355,7 +356,7 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig):
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
api_key=bedrock_bearer_token(api_key),
api_key=bedrock_bearer_token(api_key, AwsAuthParams.model_validate(optional_params)),
api_base=api_base,
)

View file

@ -346,15 +346,14 @@ class BedrockConverseLLM(BaseAWSLLM):
)
auth_params: Final = pop_aws_auth_params(optional_params)
aws_bearer_token: Final = bedrock_bearer_token(api_key, auth_params)
aws_bedrock_runtime_endpoint: Final = optional_params.pop("aws_bedrock_runtime_endpoint", None)
optional_params.pop("aws_region_name", None)
litellm_params["aws_region_name"] = aws_region_name # [DO NOT DELETE] important for async calls
credentials: Final[Credentials | None] = (
None
if bedrock_bearer_token(api_key) is not None
else self.resolve_credentials(auth_params, aws_region_name)
None if aws_bearer_token is not None else self.resolve_credentials(auth_params, aws_region_name)
)
### SET RUNTIME ENDPOINT ###

View file

@ -25,6 +25,7 @@ from litellm.llms.custom_httpx.http_handler import (
from litellm.secret_managers.main import get_secret
from litellm.types.llms.bedrock import (
AmazonEmbeddingRequest,
AwsAuthParams,
CohereEmbeddingRequest,
TwelveLabsAsyncInvokeStatusResponse,
)
@ -439,7 +440,7 @@ class BedrockEmbedding(BaseAWSLLM):
bedrock_model: Final = _without_region_path(model_without_async_route)
credentials, aws_region_name = self._load_credentials(
optional_params,
bearer_token=bedrock_bearer_token(api_key),
bearer_token=bedrock_bearer_token(api_key, AwsAuthParams.model_validate(optional_params)),
region_from_model=self._get_aws_region_from_model_arn(model_id_param) or path_region,
)

View file

@ -29,7 +29,7 @@ from litellm.llms.custom_httpx.http_handler import (
get_httpx_client,
)
from litellm.types.llms.base import LiteLLMBaseModel
from litellm.types.llms.bedrock import BearerPreparedRequest
from litellm.types.llms.bedrock import AwsAuthParams, BearerPreparedRequest
from litellm.types.utils import ImageResponse
from ..base_aws_llm import BaseAWSLLM, bedrock_bearer_token
@ -214,7 +214,9 @@ class BedrockImageEdit(BaseAWSLLM):
BedrockImageEditPreparedRequest: The prepared request object
"""
boto3_credentials_info: Final = self._get_boto_credentials_from_optional_params(
optional_params, model, bearer_token=bedrock_bearer_token(api_key)
optional_params,
model,
bearer_token=bedrock_bearer_token(api_key, AwsAuthParams.model_validate(optional_params)),
)
# Use the existing ARN-aware provider detection method

View file

@ -28,7 +28,7 @@ from litellm.llms.custom_httpx.http_handler import (
get_httpx_client,
)
from litellm.types.llms.base import LiteLLMBaseModel
from litellm.types.llms.bedrock import BearerPreparedRequest
from litellm.types.llms.bedrock import AwsAuthParams, BearerPreparedRequest
from litellm.types.utils import ImageResponse
from ..base_aws_llm import BaseAWSLLM, bedrock_bearer_token
@ -233,7 +233,9 @@ class BedrockImageGeneration(BaseAWSLLM):
body (bytes): The request body
"""
boto3_credentials_info: Final = self._get_boto_credentials_from_optional_params(
optional_params, model, bearer_token=bedrock_bearer_token(api_key)
optional_params,
model,
bearer_token=bedrock_bearer_token(api_key, AwsAuthParams.model_validate(optional_params)),
)
# Use the existing ARN-aware provider detection method

View file

@ -70,6 +70,7 @@ from litellm.types.guardrails import (
GuardrailEventHooks,
LitellmParams,
)
from litellm.types.llms.bedrock import AwsAuthParams
from litellm.types.llms.openai import AllMessageValues, ChatCompletionUserMessage
from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import (
BedrockChecksMessage,
@ -329,6 +330,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
# store kwargs as optional_params
self.optional_params = kwargs
self.aws_auth_params: Final = AwsAuthParams.model_validate(kwargs)
self.disable_exception_on_block: bool = disable_exception_on_block or False
"""
@ -830,7 +832,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
credentials: Final[Credentials | None] = (
None
if bedrock_bearer_token(api_key) is not None
if bedrock_bearer_token(api_key, self.aws_auth_params) is not None
else self.get_credentials(
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
@ -874,7 +876,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
proxy_endpoint_url = f"{proxy_endpoint_url}{request_path}"
encoded_data: Final = json.dumps(data).encode("utf-8")
aws_bearer_token: Final = bedrock_bearer_token(api_key)
aws_bearer_token: Final = bedrock_bearer_token(api_key, self.aws_auth_params)
if aws_bearer_token is not None:
try:

View file

@ -550,6 +550,36 @@ def test_bearer_api_key_is_sent_as_the_authorization_header(monkeypatch):
assert headers["Authorization"] == "Bearer bedrock-api-key"
@pytest.mark.parametrize(
"auth_kwargs, authorization_prefix",
[
(
{
"aws_access_key_id": SIGV4_PARAMS["aws_access_key_id"],
"aws_secret_access_key": SIGV4_PARAMS["aws_secret_access_key"],
},
f"AWS4-HMAC-SHA256 Credential={SIGV4_PARAMS['aws_access_key_id']}/",
),
({}, "Bearer env-bedrock-token"),
],
ids=["explicit-keys-sign-sigv4", "no-keys-use-env-token"],
)
def test_explicit_aws_keys_win_over_the_env_bearer_token(
local_cost_map, fake_aws_env, monkeypatch, auth_kwargs, authorization_prefix
):
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bedrock-token")
requests, client = _recording_client(json=_chat_completion_json("ok", "us.xai.grok-4.6"))
litellm.completion(
model="bedrock/chat_completions/us.xai.grok-4.6",
messages=[{"role": "user", "content": "hello"}],
client=client,
**auth_kwargs,
)
assert len(requests) == 1
assert requests[0].headers["Authorization"].startswith(authorization_prefix), requests[0].headers
@pytest.mark.parametrize(
"request_params, expected_route",
[

View file

@ -218,9 +218,10 @@ def test_bearer_token_auth_serves_when_boto3_resolves_no_sigv4_credentials(monke
@pytest.mark.parametrize("configured_through", ["env_var", "api_key"])
def test_bearer_token_auth_never_runs_the_sigv4_credential_chain(monkeypatch, configured_through):
"""The deployment's AWS profile does not exist, so resolving SigV4 credentials
raises; a bearer-token deployment must still serve the request, since the
bearer token alone signs it."""
"""The process's default AWS profile does not exist, so resolving SigV4
credentials raises; a bearer-token deployment must still serve the request,
since the bearer token alone signs it."""
monkeypatch.setenv("AWS_PROFILE", "litellm-no-such-aws-profile")
if configured_through == "env_var":
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "bedrock-bearer-token")
else:
@ -229,7 +230,7 @@ def test_bearer_token_auth_never_runs_the_sigv4_credential_chain(monkeypatch, co
response = BedrockConverseLLM().completion(
**_completion_kwargs(
optional_params={"maxTokens": 16, "aws_profile_name": "litellm-no-such-aws-profile"},
optional_params={"maxTokens": 16},
litellm_params={},
client=client,
api_key="bedrock-bearer-token" if configured_through == "api_key" else None,
@ -240,6 +241,26 @@ def test_bearer_token_auth_never_runs_the_sigv4_credential_chain(monkeypatch, co
assert client.post.call_args.kwargs["headers"]["Authorization"] == "Bearer bedrock-bearer-token"
def test_explicit_deployment_keys_sign_over_the_env_bearer_token(monkeypatch):
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "bedrock-bearer-token")
client = _sync_client_returning_converse_response()
response = BedrockConverseLLM().completion(
**_completion_kwargs(
optional_params={
"maxTokens": 16,
"aws_access_key_id": "AKIAEXPLICITDEPLOYMENT",
"aws_secret_access_key": "explicit-secret",
},
litellm_params={},
client=client,
)
)
assert response.choices[0].message.content == "hi"
assert "Credential=AKIAEXPLICITDEPLOYMENT/" in client.post.call_args.kwargs["headers"]["Authorization"]
def test_session_tags_sign_the_request_and_stay_out_of_the_body(monkeypatch):
"""The tagged STS session signs the Converse call and the tags never reach the request body (#34069)."""
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)

View file

@ -182,6 +182,37 @@ class TestBedrockAsyncInvokeEmbedding:
request_url = mock_post.call_args.kwargs.get("url", "")
assert "/async-invoke" in request_url
def test_async_invoke_signs_with_explicit_deployment_keys_over_env_bearer_token(self, monkeypatch):
"""A deployment configured with its own IAM keys must sign StartAsyncInvoke
with SigV4 even when AWS_BEARER_TOKEN_BEDROCK is set in the process: a
Bedrock API key cannot write the S3 output, so Bedrock rejects the bearer
request with "Invalid S3 credentials"."""
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
client = HTTPHandler()
with patch.object(client, "post") as mock_post:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = json.dumps(async_invoke_response)
mock_response.json = lambda: json.loads(mock_response.text)
mock_post.return_value = mock_response
response = litellm.embedding(
model="bedrock/async_invoke/twelvelabs.marengo-embed-3-0-v1:0",
input=test_input,
client=client,
aws_region_name="us-east-1",
aws_access_key_id="AKIAEXPLICITDEPLOYMENT",
aws_secret_access_key="explicit-deployment-secret",
input_type="text",
output_s3_uri="s3://test-bucket/async-invoke-output/",
)
authorization = mock_post.call_args.kwargs["headers"]["Authorization"]
assert response._hidden_params._invocation_arn == async_invoke_response["invocationArn"]
assert authorization.startswith("AWS4-HMAC-SHA256 Credential=AKIAEXPLICITDEPLOYMENT/")
assert "Bearer" not in authorization
def test_async_invoke_marengo_3_wraps_the_nested_payload_with_the_base_model_id(self):
client = HTTPHandler()

View file

@ -1449,10 +1449,11 @@ def test_embedding_session_tags_sign_the_request_and_stay_out_of_the_body(monkey
def test_bedrock_embedding_bearer_token_never_runs_the_sigv4_credential_chain(monkeypatch):
"""The deployment's AWS profile does not exist, so resolving SigV4 credentials
raises; a bearer-token deployment must still serve the request, since the
bearer token alone signs it."""
"""The process's default AWS profile does not exist, so resolving SigV4
credentials raises; a bearer-token deployment must still serve the request,
since the bearer token alone signs it."""
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
monkeypatch.setenv("AWS_PROFILE", "litellm-no-such-aws-profile")
client = HTTPHandler()
with patch.object(client, "post") as mock_post:
@ -1467,7 +1468,6 @@ def test_bedrock_embedding_bearer_token_never_runs_the_sigv4_credential_chain(mo
input=test_input,
client=client,
aws_region_name="us-west-2",
aws_profile_name="litellm-no-such-aws-profile",
)
assert response.data[0]["embedding"] == titan_embedding_response["embedding"]

View file

@ -1,17 +1,18 @@
from unittest.mock import Mock
def test_image_generation_bearer_token_never_runs_the_sigv4_credential_chain(monkeypatch):
"""The deployment's AWS profile does not exist, so resolving SigV4 credentials
raises; a bearer-token deployment must still sign the request with the
bearer token alone."""
"""The process's default AWS profile does not exist, so resolving SigV4
credentials raises; a bearer-token deployment must still sign the request
with the bearer token alone."""
from litellm.llms.bedrock.image_generation.image_handler import BedrockImageGeneration
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
monkeypatch.setenv("AWS_PROFILE", "litellm-no-such-aws-profile")
request = BedrockImageGeneration()._prepare_request(
model="amazon.nova-canvas-v1:0",
prompt="A cute baby sea otter",
optional_params={"aws_region_name": "us-west-2", "aws_profile_name": "litellm-no-such-aws-profile"},
optional_params={"aws_region_name": "us-west-2"},
api_base=None,
extra_headers=None,
api_key=None,

View file

@ -610,16 +610,17 @@ def test_transform_response_empty_images_without_error_raises():
def test_prepare_request_bearer_token_never_runs_the_sigv4_credential_chain(monkeypatch):
"""The deployment's AWS profile does not exist, so resolving SigV4 credentials
raises; a bearer-token deployment must still sign the request with the
bearer token alone."""
"""The process's default AWS profile does not exist, so resolving SigV4
credentials raises; a bearer-token deployment must still sign the request
with the bearer token alone."""
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
monkeypatch.setenv("AWS_PROFILE", "litellm-no-such-aws-profile")
request = BedrockImageEdit()._prepare_request(
model="amazon.nova-canvas-v1:0",
image=[io.BytesIO(b"fake-png")],
prompt="make it warmer",
optional_params={"aws_region_name": "us-west-2", "aws_profile_name": "litellm-no-such-aws-profile"},
optional_params={"aws_region_name": "us-west-2"},
api_base=None,
extra_headers=None,
logging_obj=Mock(),

View file

@ -768,6 +768,52 @@ def test_get_request_headers_preserves_bearer_payload(from_environment):
assert result.method == "POST"
@pytest.mark.parametrize(
("api_key", "optional_params", "expected"),
[
(None, {}, "env-bearer-token-12345"),
(None, {"aws_region_name": "us-west-2", "aws_access_key_id": None}, "env-bearer-token-12345"),
(None, {"aws_access_key_id": "AKIAEXPLICIT", "aws_secret_access_key": "secret"}, None),
(None, {"aws_session_token": "session"}, None),
(None, {"aws_profile_name": "deployment-profile"}, None),
(None, {"aws_role_name": "arn:aws:iam::123456789012:role/deployment"}, None),
(None, {"aws_web_identity_token": "oidc-token"}, None),
("explicit-bearer", {"aws_access_key_id": "AKIAEXPLICIT"}, "explicit-bearer"),
("", {}, None),
("", {"aws_access_key_id": "AKIAEXPLICIT"}, None),
],
)
def test_bedrock_bearer_token_env_token_yields_to_explicit_sigv4_params(monkeypatch, api_key, optional_params, expected):
"""The env bearer token is the ambient fallback: explicit SigV4 params on the
deployment turn it off, an explicit api_key still wins over both, and an
explicit empty api_key keeps meaning "no bearer, sign with SigV4"."""
from litellm.llms.bedrock.base_aws_llm import bedrock_bearer_token
from litellm.types.llms.bedrock import AwsAuthParams
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
assert bedrock_bearer_token(api_key, AwsAuthParams.model_validate(optional_params)) == expected
def test_get_request_headers_signs_with_resolved_credentials_despite_env_bearer_token(monkeypatch):
"""Credentials resolved from the deployment's explicit SigV4 params must sign
the request even when the process carries AWS_BEARER_TOKEN_BEDROCK, which a
Bedrock API key cannot do on the caller's behalf for S3-backed calls."""
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
prepared = BaseAWSLLM().get_request_headers(
credentials=Credentials("test_key", "test_secret", "test_token"),
aws_region_name="us-west-2",
extra_headers=None,
endpoint_url="https://bedrock-runtime.us-west-2.amazonaws.com/async-invoke",
data='{"prompt": "test"}',
headers={"Content-Type": "application/json"},
)
assert prepared.headers["Authorization"].startswith("AWS4-HMAC-SHA256 Credential=test_key/")
assert "Bearer" not in prepared.headers["Authorization"]
def test_get_request_headers_with_sigv4():
# Setup
llm = BaseAWSLLM()

View file

@ -6156,14 +6156,14 @@ async def test_apply_guardrail_debug_log_masks_signed_request_headers():
@pytest.mark.asyncio
async def test_bearer_token_never_runs_the_sigv4_credential_chain(monkeypatch):
"""The guardrail's AWS profile does not exist, so resolving SigV4 credentials
raises; with a bearer token configured the guardrail must still run, since
the bearer token alone signs the request."""
"""The process's default AWS profile does not exist, so resolving SigV4
credentials raises; with a bearer token configured the guardrail must still
run, since the bearer token alone signs the request."""
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
monkeypatch.setenv("AWS_PROFILE", "litellm-no-such-aws-profile")
guardrail = BedrockGuardrail(
guardrailIdentifier="test-guardrail",
guardrailVersion="DRAFT",
aws_profile_name="litellm-no-such-aws-profile",
)
mock_response = MagicMock()
mock_response.status_code = 200

View file

@ -840,14 +840,14 @@ async def test_many_blocks_scanned_at_request_level_and_can_block():
@pytest.mark.asyncio
async def test_checks_bearer_token_never_runs_the_sigv4_credential_chain(monkeypatch):
"""Same bearer-token rule as ApplyGuardrail: the guardrail's AWS profile does
not exist, yet the InvokeGuardrailChecks call still goes out on the bearer
token and its verdict is enforced."""
"""Same bearer-token rule as ApplyGuardrail: the process's default AWS
profile does not exist, yet the InvokeGuardrailChecks call still goes out on
the bearer token and its verdict is enforced."""
monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token-12345")
monkeypatch.setenv("AWS_PROFILE", "litellm-no-such-aws-profile")
g = BedrockGuardrail(
checks=CONTENT_FILTER_CHECKS,
content_filter_threshold=0.5,
aws_profile_name="litellm-no-such-aws-profile",
)
payload = {"results": {"contentFilter": {"results": [{"category": "VIOLENCE", "severityScore": 0.8}]}}}
post = AsyncMock(return_value=_mock_http_response(200, payload))