From 43bd32befaf35d861b37be0b5f72e0f2076f3ace Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 5 Jul 2026 14:29:07 +0000 Subject: [PATCH] feat(llm): support Bedrock API-key auth via AWS_BEARER_TOKEN_BEDROCK - pass the token to ChatBedrockConverse as api_key so langchain-aws prefers bearer auth and an ambient AWS_PROFILE can't override it; no AWS access keys required #1103 --- tests/test_bedrock_provider.py | 36 ++++++++++++++++++++- tradingagents/llm_clients/bedrock_client.py | 17 +++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/tests/test_bedrock_provider.py b/tests/test_bedrock_provider.py index c868e9806..12a3c952d 100644 --- a/tests/test_bedrock_provider.py +++ b/tests/test_bedrock_provider.py @@ -35,12 +35,46 @@ def test_helpful_error_when_langchain_aws_absent(monkeypatch): create_llm_client("bedrock", "m").get_llm() +def _capture_kwargs(monkeypatch): + """Stub _bedrock_class so the constructor kwargs are testable without the + optional langchain-aws extra installed.""" + import tradingagents.llm_clients.bedrock_client as bc + captured = {} + + class _FakeChat: + def __init__(self, **kwargs): + captured.update(kwargs) + + monkeypatch.setattr(bc, "_bedrock_class", lambda: _FakeChat) + return captured + + +@pytest.mark.unit +def test_bearer_token_passed_as_api_key(monkeypatch): + # #1103: a Bedrock API key authenticates without AWS access keys. + captured = _capture_kwargs(monkeypatch) + monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "bt-secret") + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + create_llm_client("bedrock", "us.anthropic.claude-opus-4-8-v1:0").get_llm() + assert captured["api_key"] == "bt-secret" + assert captured["region_name"] == "us-east-1" + + +@pytest.mark.unit +def test_no_bearer_token_omits_api_key(monkeypatch): + # Without a token, fall back to the AWS credential chain (no api_key kwarg). + captured = _capture_kwargs(monkeypatch) + monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) + create_llm_client("bedrock", "us.anthropic.claude-opus-4-8-v1:0").get_llm() + assert "api_key" not in captured + + @pytest.mark.unit def test_construction_when_extra_installed(monkeypatch): pytest.importorskip("langchain_aws") import tradingagents.llm_clients.bedrock_client as bc monkeypatch.setattr(bc, "_BEDROCK_CLASS", None) monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1") - llm = create_llm_client("bedrock", "us.anthropic.claude-sonnet-4-6-v1:0").get_llm() + llm = create_llm_client("bedrock", "us.anthropic.claude-sonnet-5").get_llm() assert type(llm).__name__ == "NormalizedChatBedrockConverse" assert llm.region_name == "eu-west-1" diff --git a/tradingagents/llm_clients/bedrock_client.py b/tradingagents/llm_clients/bedrock_client.py index 2ddb48dd3..406c4ff08 100644 --- a/tradingagents/llm_clients/bedrock_client.py +++ b/tradingagents/llm_clients/bedrock_client.py @@ -41,11 +41,12 @@ def _bedrock_class(): class BedrockClient(BaseLLMClient): """Client for Amazon Bedrock via the Converse API (langchain-aws). - Authentication uses the standard AWS credential chain (env vars, - ``~/.aws/credentials``, or an IAM role); set ``AWS_REGION`` / - ``AWS_DEFAULT_REGION`` and optionally ``AWS_PROFILE``. The model name is a - Bedrock model ID or cross-region inference profile ID, e.g. - ``us.anthropic.claude-opus-4-8-v1:0``. + Authentication is either a Bedrock API key (bearer token) via + ``AWS_BEARER_TOKEN_BEDROCK`` — no AWS access keys required — or the standard + AWS credential chain (env vars, ``~/.aws/credentials``, or an IAM role) with + optional ``AWS_PROFILE``. Set ``AWS_REGION`` / ``AWS_DEFAULT_REGION`` either + way (the token carries no region). The model name is a Bedrock model ID or + cross-region inference profile ID, e.g. ``us.anthropic.claude-opus-4-8-v1:0``. """ def get_llm(self) -> Any: @@ -59,6 +60,12 @@ class BedrockClient(BaseLLMClient): or _DEFAULT_REGION ) llm_kwargs = {"model": self.model, "region_name": region} + # A Bedrock API key authenticates without AWS access keys. Passing it as + # api_key makes langchain-aws prefer bearer auth, so an ambient + # AWS_PROFILE / SigV4 credentials can't override it (#1103). + bearer_token = os.environ.get("AWS_BEARER_TOKEN_BEDROCK") + if bearer_token: + llm_kwargs["api_key"] = bearer_token for key in ("temperature", "max_tokens", "max_retries", "callbacks"): if key in self.kwargs: llm_kwargs[key] = self.kwargs[key]