Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/anthropic/lib/aws/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import httpx2

from ..._utils import lru_cache
from ..._exceptions import CredentialsError

if TYPE_CHECKING:
import boto3
Expand Down Expand Up @@ -62,7 +63,7 @@ def get_auth_headers(
request = AWSRequest(method=method.upper(), url=url, headers=new_headers, data=data)
credentials = session.get_credentials()
if not credentials:
raise RuntimeError("Could not resolve AWS credentials from session")
raise CredentialsError("Could not resolve AWS credentials from session")

signer = SigV4Auth(credentials, service_name, session.region_name)
signer.add_auth(request)
Expand Down
6 changes: 3 additions & 3 deletions src/anthropic/lib/vertex/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..._version import __version__
from ..._constants import ANTHROPIC_API_VERSION
from ..._streaming import Stream, AsyncStream
from ..._exceptions import AnthropicError
from ..._exceptions import AnthropicError, CredentialsError
from ..._middleware import MiddlewareInput
from ..._base_client import (
DEFAULT_MAX_RETRIES,
Expand Down Expand Up @@ -154,7 +154,7 @@ def _ensure_access_token(self) -> str:
refresh_auth(self.credentials)

if not self.credentials.token:
raise RuntimeError("Could not resolve API token from the environment")
raise CredentialsError("Could not resolve API token from the environment")

assert isinstance(self.credentials.token, str)
return self.credentials.token
Expand Down Expand Up @@ -318,7 +318,7 @@ async def _ensure_access_token(self) -> str:
await asyncify(refresh_auth)(self.credentials)

if not self.credentials.token:
raise RuntimeError("Could not resolve API token from the environment")
raise CredentialsError("Could not resolve API token from the environment")

assert isinstance(self.credentials.token, str)
return self.credentials.token
Expand Down
3 changes: 2 additions & 1 deletion tests/lib/test_aws_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import httpx2
import pytest

from anthropic import CredentialsError
from anthropic.lib.aws._auth import get_auth_headers
from anthropic.lib.aws._credentials import (
resolve_region,
Expand Down Expand Up @@ -595,7 +596,7 @@ def fake_get_session(**_kwargs: object) -> NoCredentialsSession:

monkeypatch.setattr("anthropic.lib.aws._auth._get_session", fake_get_session)

with pytest.raises(RuntimeError, match="Could not resolve AWS credentials"):
with pytest.raises(CredentialsError, match="Could not resolve AWS credentials"):
get_auth_headers(
method="POST",
url="https://gateway.us-east-1.api.aws/v1/messages",
Expand Down
50 changes: 50 additions & 0 deletions tests/lib/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
BadRequestError,
OverloadedError,
AnthropicBedrock,
CredentialsError,
InternalServerError,
AsyncAnthropicBedrock,
PreconditionFailedError,
Expand Down Expand Up @@ -117,6 +118,55 @@ def mock_aws_config(
yield


@pytest.fixture
def missing_bedrock_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
class NoCredentialsSession:
region_name = "us-east-1"

def get_credentials(self) -> None:
return None

def no_credentials_session(**_kwargs: object) -> NoCredentialsSession:
return NoCredentialsSession()

monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
monkeypatch.setattr("anthropic.lib.aws._auth._get_session", no_credentials_session)


@pytest.mark.respx()
@pytest.mark.parametrize("beta", [False, True])
@pytest.mark.usefixtures("missing_bedrock_credentials")
def test_missing_credentials(respx_mock: MockRouter, beta: bool) -> None:
with AnthropicBedrock(aws_region="us-east-1") as client:
messages = client.beta.messages if beta else client.messages
with pytest.raises(CredentialsError) as exc_info:
messages.create(
model="anthropic.claude-3-sonnet-20240229-v1:0",
max_tokens=16,
messages=[{"role": "user", "content": "Hello"}],
)

assert str(exc_info.value) == "Could not resolve AWS credentials from session"
assert not respx_mock.calls


@pytest.mark.respx()
@pytest.mark.parametrize("beta", [False, True])
@pytest.mark.usefixtures("missing_bedrock_credentials")
async def test_missing_credentials_async(respx_mock: MockRouter, beta: bool) -> None:
async with AsyncAnthropicBedrock(aws_region="us-east-1") as client:
messages = client.beta.messages if beta else client.messages
with pytest.raises(CredentialsError) as exc_info:
await messages.create(
model="anthropic.claude-3-sonnet-20240229-v1:0",
max_tokens=16,
messages=[{"role": "user", "content": "Hello"}],
)

assert str(exc_info.value) == "Could not resolve AWS credentials from session"
assert not respx_mock.calls


@pytest.mark.respx()
def test_messages_retries(respx_mock: MockRouter) -> None:
respx_mock.post(re.compile(r"https://bedrock-runtime\.us-east-1\.amazonaws\.com/model/.*/invoke")).mock(
Expand Down
57 changes: 57 additions & 0 deletions tests/lib/test_vertex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import json
from typing import Any, Dict, List, cast
from collections.abc import Iterator
from typing_extensions import Protocol

import httpx2
Expand All @@ -15,6 +16,7 @@
AnthropicVertex,
BadRequestError,
OverloadedError,
CredentialsError,
InternalServerError,
AsyncAnthropicVertex,
PreconditionFailedError,
Expand Down Expand Up @@ -122,9 +124,45 @@ def _assert_tool_runner_calls(calls: List[MockRequestCall]) -> None:
}


@pytest.fixture
def tokenless_credentials(monkeypatch: pytest.MonkeyPatch) -> Iterator[object]:
class TokenlessCredentials:
token = None
expired = False

credentials = TokenlessCredentials()
refreshed: list[object] = []

def refresh_without_token(value: object) -> None:
refreshed.append(value)

monkeypatch.setattr("anthropic.lib.vertex._client.refresh_auth", refresh_without_token)
yield credentials
assert refreshed == [credentials]


class TestAnthropicVertex:
client = AnthropicVertex(region="region", project_id="project", access_token="my-access-token")

@pytest.mark.respx()
@pytest.mark.parametrize("beta", [False, True])
def test_tokenless_credentials_after_refresh(
self, tokenless_credentials: object, respx_mock: MockRouter, beta: bool
) -> None:
with AnthropicVertex(
region="region", project_id="project", credentials=cast(Any, tokenless_credentials)
) as client:
messages = client.beta.messages if beta else client.messages
with pytest.raises(CredentialsError) as exc_info:
messages.create(
model="claude-3-sonnet@20240229",
max_tokens=16,
messages=[{"role": "user", "content": "Hello"}],
)

assert str(exc_info.value) == "Could not resolve API token from the environment"
assert not respx_mock.calls

@pytest.mark.respx()
def test_messages_retries(self, respx_mock: MockRouter) -> None:
request_url = "https://region-aiplatform.googleapis.com/v1/projects/project/locations/region/publishers/anthropic/models/claude-3-sonnet@20240229:rawPredict"
Expand Down Expand Up @@ -477,6 +515,25 @@ def refresh(self, _request: object) -> None:
class TestAsyncAnthropicVertex:
client = AsyncAnthropicVertex(region="region", project_id="project", access_token="my-access-token")

@pytest.mark.respx()
@pytest.mark.parametrize("beta", [False, True])
async def test_tokenless_credentials_after_refresh(
self, tokenless_credentials: object, respx_mock: MockRouter, beta: bool
) -> None:
async with AsyncAnthropicVertex(
region="region", project_id="project", credentials=cast(Any, tokenless_credentials)
) as client:
messages = client.beta.messages if beta else client.messages
with pytest.raises(CredentialsError) as exc_info:
await messages.create(
model="claude-3-sonnet@20240229",
max_tokens=16,
messages=[{"role": "user", "content": "Hello"}],
)

assert str(exc_info.value) == "Could not resolve API token from the environment"
assert not respx_mock.calls

@pytest.mark.respx()
@pytest.mark.asyncio()
async def test_messages_retries(self, respx_mock: MockRouter) -> None:
Expand Down