Skip to content

Commit cde301b

Browse files
MukundaKattaGWeale
authored andcommitted
fix(models): wrap Anthropic rate limit errors
Merge #5401 ## Summary - wrap Anthropic RateLimitError in a dedicated ADK exception with mitigation guidance - apply the wrapper consistently for both streaming and non-streaming Claude requests - add regression tests for both code paths ## Testing - python3 -m py_compile src/google/adk/models/anthropic_llm.py tests/unittests/models/test_anthropic_llm.py - python3 -m pytest tests/unittests/models/test_anthropic_llm.py -k "wraps_anthropic_rate_limit_error" *(fails in this environment because the pytest interpreter is missing the package during collection)* Co-authored-by: George Weale <gweale@google.com> COPYBARA_INTEGRATE_REVIEW=#5401 from MukundaKatta:codex/adk-anthropic-rate-limit e08c51b PiperOrigin-RevId: 956692885
1 parent d58caa6 commit cde301b

2 files changed

Lines changed: 92 additions & 11 deletions

File tree

src/google/adk/models/anthropic_llm.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from anthropic import AsyncAnthropicVertex
3838
from anthropic import NOT_GIVEN
3939
from anthropic import NotGiven
40+
from anthropic import RateLimitError
4041
from anthropic import types as anthropic_types
4142
from google.genai import types
4243
from pydantic import BaseModel
@@ -57,6 +58,28 @@
5758
logger = logging.getLogger("google_adk." + __name__)
5859

5960

61+
_RATE_LIMIT_POSSIBLE_FIX_MESSAGE = (
62+
"On how to mitigate this issue, please refer to:\n\n"
63+
"https://docs.anthropic.com/en/api/errors#http-errors"
64+
)
65+
66+
67+
# anthropic is an optional dependency, so mypy resolves the base class to Any.
68+
class _AnthropicRateLimitError(RateLimitError): # type: ignore[misc]
69+
"""Represents a rate limit error received from Anthropic."""
70+
71+
def __init__(self, rate_limit_error: RateLimitError):
72+
super().__init__(
73+
str(rate_limit_error),
74+
response=rate_limit_error.response,
75+
body=getattr(rate_limit_error, "body", None),
76+
)
77+
78+
def __str__(self) -> str:
79+
base_message = super().__str__()
80+
return f"{_RATE_LIMIT_POSSIBLE_FIX_MESSAGE}\n\n{base_message}"
81+
82+
6083
@dataclasses.dataclass
6184
class _ToolUseAccumulator:
6285
"""Accumulates streamed tool_use content block data."""
@@ -744,17 +767,20 @@ async def generate_content_async(
744767
)
745768
thinking = _build_anthropic_thinking_param(llm_request.config)
746769

747-
if not stream:
748-
kwargs = self._build_anthropic_kwargs(
749-
llm_request, messages, tools, tool_choice, thinking
750-
)
751-
message = await self._anthropic_client.messages.create(**kwargs)
752-
yield message_to_generate_content_response(message)
753-
else:
754-
async for response in self._generate_content_streaming(
755-
llm_request, messages, tools, tool_choice, thinking
756-
):
757-
yield response
770+
try:
771+
if not stream:
772+
kwargs = self._build_anthropic_kwargs(
773+
llm_request, messages, tools, tool_choice, thinking
774+
)
775+
message = await self._anthropic_client.messages.create(**kwargs)
776+
yield message_to_generate_content_response(message)
777+
else:
778+
async for response in self._generate_content_streaming(
779+
llm_request, messages, tools, tool_choice, thinking
780+
):
781+
yield response
782+
except RateLimitError as rate_limit_error:
783+
raise _AnthropicRateLimitError(rate_limit_error) from rate_limit_error
758784

759785
async def _generate_content_streaming(
760786
self,

tests/unittests/models/test_anthropic_llm.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
from unittest.mock import MagicMock
2323

2424
from anthropic import NOT_GIVEN
25+
from anthropic import RateLimitError
2526
from anthropic import types as anthropic_types
2627
from google.adk import version as adk_version
2728
from google.adk.models import anthropic_llm
2829
from google.adk.models import AnthropicGenerateContentConfig
30+
from google.adk.models.anthropic_llm import _AnthropicRateLimitError
2931
from google.adk.models.anthropic_llm import AnthropicLlm
3032
from google.adk.models.anthropic_llm import Claude
3133
from google.adk.models.anthropic_llm import content_to_message_param
@@ -39,6 +41,7 @@
3941
from google.genai import version as genai_version
4042
from google.genai.types import Content
4143
from google.genai.types import Part
44+
import httpx
4245
import pytest
4346

4447

@@ -2802,3 +2805,55 @@ async def test_streaming_sets_finish_reason():
28022805

28032806
final = responses[-1]
28042807
assert final.finish_reason == types.FinishReason.MAX_TOKENS
2808+
2809+
2810+
def _make_rate_limit_error() -> RateLimitError:
2811+
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
2812+
response = httpx.Response(429, request=request)
2813+
return RateLimitError(
2814+
"rate limited",
2815+
response=response,
2816+
body={"type": "error", "error": {"type": "rate_limit_error"}},
2817+
)
2818+
2819+
2820+
@pytest.mark.asyncio
2821+
async def test_non_streaming_wraps_anthropic_rate_limit_error():
2822+
llm = AnthropicLlm(model="claude-sonnet-4-20250514")
2823+
mock_client = MagicMock()
2824+
mock_client.messages.create = AsyncMock(side_effect=_make_rate_limit_error())
2825+
2826+
llm_request = LlmRequest(
2827+
model="claude-sonnet-4-20250514",
2828+
contents=[Content(role="user", parts=[Part.from_text(text="Hi")])],
2829+
config=types.GenerateContentConfig(system_instruction="Test"),
2830+
)
2831+
2832+
with mock.patch.object(llm, "_anthropic_client", mock_client):
2833+
with pytest.raises(_AnthropicRateLimitError) as excinfo:
2834+
_ = [r async for r in llm.generate_content_async(llm_request)]
2835+
2836+
assert "docs.anthropic.com/en/api/errors#http-errors" in str(excinfo.value)
2837+
assert "rate limited" in str(excinfo.value)
2838+
2839+
2840+
@pytest.mark.asyncio
2841+
async def test_streaming_wraps_anthropic_rate_limit_error():
2842+
llm = AnthropicLlm(model="claude-sonnet-4-20250514")
2843+
mock_client = MagicMock()
2844+
mock_client.messages.create = AsyncMock(side_effect=_make_rate_limit_error())
2845+
2846+
llm_request = LlmRequest(
2847+
model="claude-sonnet-4-20250514",
2848+
contents=[Content(role="user", parts=[Part.from_text(text="Hi")])],
2849+
config=types.GenerateContentConfig(system_instruction="Test"),
2850+
)
2851+
2852+
with mock.patch.object(llm, "_anthropic_client", mock_client):
2853+
with pytest.raises(_AnthropicRateLimitError) as excinfo:
2854+
_ = [
2855+
r async for r in llm.generate_content_async(llm_request, stream=True)
2856+
]
2857+
2858+
assert "docs.anthropic.com/en/api/errors#http-errors" in str(excinfo.value)
2859+
assert "rate limited" in str(excinfo.value)

0 commit comments

Comments
 (0)