diff --git a/src/anthropic/lib/aws/_client.py b/src/anthropic/lib/aws/_client.py index 76f42eb80..105ee402a 100644 --- a/src/anthropic/lib/aws/_client.py +++ b/src/anthropic/lib/aws/_client.py @@ -9,6 +9,7 @@ from ..._utils import asyncify from ..._client import Anthropic, AsyncAnthropic from ._credentials import ( + AuthMode, resolve_region, resolve_api_key, resolve_base_url, @@ -36,6 +37,7 @@ def __init__( self, *, api_key: str | None = None, + auth_mode: AuthMode = "auto", aws_access_key: str | None = None, aws_secret_key: str | None = None, aws_region: str | None = None, @@ -56,6 +58,7 @@ def __init__( webhook_key: str | None = None, ) -> None: self._skip_auth = skip_auth + self.auth_mode = auth_mode validate_credentials(aws_access_key=aws_access_key, aws_secret_key=aws_secret_key) @@ -64,6 +67,7 @@ def __init__( resolved_api_key = None else: self._use_sigv4 = resolve_auth_mode( + auth_mode=auth_mode, api_key=api_key, aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, @@ -170,6 +174,7 @@ def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodO self, *, api_key: str | None = None, + auth_mode: AuthMode = "auto", aws_access_key: str | None = None, aws_secret_key: str | None = None, aws_region: str | None = None, @@ -244,6 +249,7 @@ def __init__( self, *, api_key: str | None = None, + auth_mode: AuthMode = "auto", aws_access_key: str | None = None, aws_secret_key: str | None = None, aws_region: str | None = None, @@ -264,6 +270,7 @@ def __init__( webhook_key: str | None = None, ) -> None: self._skip_auth = skip_auth + self.auth_mode = auth_mode validate_credentials(aws_access_key=aws_access_key, aws_secret_key=aws_secret_key) @@ -272,6 +279,7 @@ def __init__( resolved_api_key = None else: self._use_sigv4 = resolve_auth_mode( + auth_mode=auth_mode, api_key=api_key, aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, @@ -378,6 +386,7 @@ def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodO self, *, api_key: str | None = None, + auth_mode: AuthMode = "auto", aws_access_key: str | None = None, aws_secret_key: str | None = None, aws_region: str | None = None, diff --git a/src/anthropic/lib/aws/_credentials.py b/src/anthropic/lib/aws/_credentials.py index d6f591dda..1e10ce8c4 100644 --- a/src/anthropic/lib/aws/_credentials.py +++ b/src/anthropic/lib/aws/_credentials.py @@ -2,6 +2,9 @@ import os from typing import Sequence +from typing_extensions import Literal + +AuthMode = Literal["auto", "api_key", "sigv4"] def validate_credentials( @@ -30,12 +33,17 @@ def _read_env(*env_vars: str) -> str | None: def resolve_auth_mode( *, + auth_mode: AuthMode | None = "auto", api_key: str | None, aws_access_key: str | None, aws_secret_key: str | None, aws_profile: str | None, api_key_env_vars: Sequence[str] = ("ANTHROPIC_AWS_API_KEY",), ) -> bool: + if auth_mode == "sigv4": + return True + if auth_mode == "api_key": + return False """Determine whether to use SigV4 auth. Returns True for SigV4, False for API key. Auth precedence: diff --git a/src/anthropic/lib/bedrock/_mantle.py b/src/anthropic/lib/bedrock/_mantle.py index 8c8c0f32c..493a149d1 100644 --- a/src/anthropic/lib/bedrock/_mantle.py +++ b/src/anthropic/lib/bedrock/_mantle.py @@ -25,6 +25,7 @@ merge_headers, ) from ..aws._credentials import ( + AuthMode, resolve_region, resolve_api_key, resolve_auth_mode, @@ -111,6 +112,7 @@ def _resolve_mantle_config( aws_region: str | None, aws_profile: str | None, skip_auth: bool, + auth_mode: AuthMode = "auto", base_url: str | httpx2.URL | None, default_headers: Mapping[str, str] | None, ) -> tuple[str | None, str | httpx2.URL, bool, dict[str, str]]: @@ -125,6 +127,7 @@ def _resolve_mantle_config( validate_credentials(aws_access_key=aws_access_key, aws_secret_key=aws_secret_key) use_sigv4 = resolve_auth_mode( + auth_mode=auth_mode, api_key=api_key, aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, @@ -171,6 +174,7 @@ class AnthropicBedrockMantle(BaseMantleClient[httpx2.Client, Stream[Any]], SyncA aws_session_token: str | None aws_profile: str | None skip_auth: bool + auth_mode: AuthMode _use_sigv4: bool @@ -183,6 +187,7 @@ def __init__( aws_region: str | None = None, aws_profile: str | None = None, api_key: str | None = None, + auth_mode: AuthMode = "auto", skip_auth: bool = False, base_url: str | httpx2.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -195,6 +200,7 @@ def __init__( ) -> None: resolved_api_key, resolved_base_url, use_sigv4, merged_headers = _resolve_mantle_config( api_key=api_key, + auth_mode=auth_mode, aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, aws_region=aws_region, @@ -225,6 +231,7 @@ def __init__( self.aws_session_token = aws_session_token self.aws_profile = aws_profile self.skip_auth = skip_auth + self.auth_mode = auth_mode self._use_sigv4 = use_sigv4 self.messages = Messages(self) @@ -289,6 +296,7 @@ def copy( aws_session_token: str | None = None, aws_region: str | None = None, aws_profile: str | None = None, + auth_mode: AuthMode | None = None, skip_auth: bool | None = None, base_url: str | httpx2.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -329,6 +337,7 @@ def copy( aws_session_token=aws_session_token or self.aws_session_token, aws_region=aws_region or self.aws_region, aws_profile=aws_profile or self.aws_profile, + auth_mode=self.auth_mode if auth_mode is None else auth_mode, skip_auth=skip_auth if skip_auth is not None else self.skip_auth, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, @@ -367,6 +376,7 @@ class AsyncAnthropicBedrockMantle(BaseMantleClient[httpx2.AsyncClient, AsyncStre aws_session_token: str | None aws_profile: str | None skip_auth: bool + auth_mode: AuthMode _use_sigv4: bool @@ -379,6 +389,7 @@ def __init__( aws_region: str | None = None, aws_profile: str | None = None, api_key: str | None = None, + auth_mode: AuthMode = "auto", skip_auth: bool = False, base_url: str | httpx2.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -391,6 +402,7 @@ def __init__( ) -> None: resolved_api_key, resolved_base_url, use_sigv4, merged_headers = _resolve_mantle_config( api_key=api_key, + auth_mode=auth_mode, aws_access_key=aws_access_key, aws_secret_key=aws_secret_key, aws_region=aws_region, @@ -421,6 +433,7 @@ def __init__( self.aws_session_token = aws_session_token self.aws_profile = aws_profile self.skip_auth = skip_auth + self.auth_mode = auth_mode self._use_sigv4 = use_sigv4 self.messages = AsyncMessages(self) @@ -485,6 +498,7 @@ def copy( aws_session_token: str | None = None, aws_region: str | None = None, aws_profile: str | None = None, + auth_mode: AuthMode | None = None, skip_auth: bool | None = None, base_url: str | httpx2.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, @@ -525,6 +539,7 @@ def copy( aws_session_token=aws_session_token or self.aws_session_token, aws_region=aws_region or self.aws_region, aws_profile=aws_profile or self.aws_profile, + auth_mode=self.auth_mode if auth_mode is None else auth_mode, skip_auth=skip_auth if skip_auth is not None else self.skip_auth, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, diff --git a/tests/lib/test_aws_auth.py b/tests/lib/test_aws_auth.py index e0e95fcf7..4751db032 100644 --- a/tests/lib/test_aws_auth.py +++ b/tests/lib/test_aws_auth.py @@ -38,6 +38,30 @@ def test_secret_key_only_raises(self) -> None: class TestResolveAuthMode: + def test_explicit_auth_mode_sigv4_overrides_env_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("ANTHROPIC_AWS_API_KEY", "env-key") + assert ( + resolve_auth_mode( + auth_mode="sigv4", + api_key=None, + aws_access_key=None, + aws_secret_key=None, + aws_profile=None, + ) + is True + ) + + def test_explicit_auth_mode_api_key_overrides_creds(self) -> None: + assert ( + resolve_auth_mode( + auth_mode="api_key", + api_key="my-key", + aws_access_key="AKID", + aws_secret_key="secret", + aws_profile=None, + ) + is False + ) def test_api_key_arg_returns_false(self) -> None: assert ( resolve_auth_mode( diff --git a/tests/lib/test_bedrock_mantle.py b/tests/lib/test_bedrock_mantle.py index e8a6695a4..e990293d7 100644 --- a/tests/lib/test_bedrock_mantle.py +++ b/tests/lib/test_bedrock_mantle.py @@ -163,6 +163,37 @@ def test_beta_skills_not_available(self) -> None: class TestAuthPrecedence: + + def test_explicit_auth_mode_sigv4_forces_sigv4(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token") + client = AnthropicBedrockMantle( + auth_mode="sigv4", + aws_region="us-east-1", + ) + assert client._use_sigv4 is True + assert client.auth_headers == {} + + def test_explicit_auth_mode_api_key_forces_api_key(self) -> None: + client = AnthropicBedrockMantle( + auth_mode="api_key", + api_key="my-bearer-key", + aws_access_key="AKID", + aws_secret_key="secret", + aws_region="us-east-1", + ) + assert client._use_sigv4 is False + assert client.api_key == "my-bearer-key" + assert client.auth_headers == {"Authorization": "Bearer my-bearer-key"} + + def test_copy_preserves_auth_mode(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "env-bearer-token") + client = AnthropicBedrockMantle( + auth_mode="sigv4", + aws_region="us-east-1", + ) + copied = client.copy() + assert copied.auth_mode == "sigv4" + assert copied._use_sigv4 is True def test_api_key_arg_uses_api_key_mode(self) -> None: client = AnthropicBedrockMantle( api_key="my-key",