This commit is contained in:
Syed Ahnaf Raza 2026-11-07 04:21:05 +00:00 • committed by GitHub
commit 8a20203d8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 114 additions and 1 deletions

View file

@ -3,8 +3,9 @@ import json
import re
import traceback
from collections.abc import Mapping
from dataclasses import dataclass
from types import MappingProxyType
from typing import Final, Protocol, cast
from typing import Final, Literal, Protocol, cast
import httpx
from pydantic import TypeAdapter, ValidationError
@ -43,11 +44,46 @@ from ..exceptions import (
)
@dataclass(frozen=True, slots=True)
class _AnthropicErrorDetail:
message: str
@dataclass(frozen=True, slots=True)
class _AnthropicErrorBody:
type: Literal["error"]
error: _AnthropicErrorDetail
_ANTHROPIC_ERROR_BODY: Final = TypeAdapter(_AnthropicErrorBody)
class ExceptionCheckers:
"""
Helper class for checking various error conditions in exception strings.
"""
@staticmethod
def is_anthropic_usage_limit_error(error_str: str) -> bool:
"""
Anthropic reports spend/usage-limit exhaustion as an invalid_request_error
(HTTP 400), e.g. "You have reached your specified API usage limits." (#37599)
Only the structured upstream error body is inspected, never the raw string:
locally raised validation errors echo the caller's messages, and matching on
those would let request content turn a 400 into a 429 and cool down shared
deployments.
"""
start: Final = error_str.find("{")
if start == -1:
return False
try:
end: Final = json.JSONDecoder().raw_decode(error_str, start)[1]
body: Final = _ANTHROPIC_ERROR_BODY.validate_json(error_str[start:end])
except ValueError: # includes json.JSONDecodeError and pydantic.ValidationError
return False
return "api usage limit" in body.error.message.lower()
@staticmethod
def is_error_str_rate_limit(error_str: str, status_code: int | None = None) -> bool:
"""
@ -650,6 +686,15 @@ def _map_anthropic_exception(
model=model,
llm_provider="anthropic",
)
# Anthropic returns usage/spend-limit exhaustion typed as invalid_request_error (400),
# but per the exception-mapping docs it should surface as a RateLimitError (429). Check
# before the generic 400 branches below, which would otherwise map it to BadRequestError.
if ExceptionCheckers.is_anthropic_usage_limit_error(error_str):
raise RateLimitError(
message=f"AnthropicError - {error_str}",
model=model,
llm_provider="anthropic",
)
if "Client error '400 Bad Request'" in error_str:
raise BadRequestError(
message=f"AnthropicError - {error_str}",

View file

@ -320,6 +320,74 @@ class TestExceptionCheckers:
is True
)
def test_anthropic_usage_limit_is_treated_as_rate_limit(self):
"""Anthropic reports spend/usage-limit exhaustion as an invalid_request_error
(HTTP 400), but it is a usage limit and should map to 429, not 400 (#37599).
"""
error_str = (
"litellm.BadRequestError: AnthropicException - "
'{"type":"error","error":{"type":"invalid_request_error",'
'"message":"You have reached your specified API usage limits. '
'You will regain access on 2026-09-01 at 00:00 UTC."}}'
)
assert ExceptionCheckers.is_anthropic_usage_limit_error(error_str) is True
def test_echoed_request_content_is_not_an_anthropic_usage_limit(self):
"""A local validation failure echoes the caller's messages. Usage-limit text in
that echoed content must not be mistaken for the upstream error and promoted to
a 429, which would cool down shared deployments.
"""
error_str = (
"AnthropicException - Unsupported file content\n"
"Received Messages=[{'role': 'user', 'content': ["
"{'type': 'text', 'text': 'api usage limit'}, {'type': 'file', 'file': {}}]}]"
)
assert ExceptionCheckers.is_anthropic_usage_limit_error(error_str) is False
assert ExceptionCheckers.is_error_str_rate_limit(error_str, status_code=400) is False
def test_echoed_request_content_still_maps_to_bad_request(self):
error_str = (
"Unsupported file content\n"
"Received Messages=[{'role': 'user', 'content': '"
'{"type":"error","error":{"message":"api usage limit"}}'
"'}]"
)
from litellm.llms.anthropic.common_utils import AnthropicError
original_exception = AnthropicError(status_code=400, message=error_str)
with pytest.raises(litellm.BadRequestError):
exception_type(
model="claude-haiku-4-5-20251001",
original_exception=original_exception,
custom_llm_provider="anthropic",
)
def test_anthropic_usage_limit_maps_to_rate_limit_error_end_to_end(self):
"""Production path: an Anthropic usage-limit error is typed invalid_request_error
(HTTP 400). Driving it through exception_type must surface a RateLimitError (429),
not a BadRequestError (#37599) — the helper alone is not on the Anthropic mapper.
"""
message = (
"AnthropicException - "
'{"type":"error","error":{"type":"invalid_request_error",'
'"message":"You have reached your specified API usage limits. '
'You will regain access on 2026-09-01 at 00:00 UTC."},'
'"request_id":"req_011CeDfeyYs7N6neZrLwFxPJ"}'
)
original_exception = httpx.HTTPStatusError(
message=message,
request=httpx.Request("POST", "https://api.anthropic.com/v1/messages"),
response=httpx.Response(400),
)
with pytest.raises(litellm.RateLimitError):
exception_type(
model="claude-haiku-4-5-20251001",
original_exception=original_exception,
custom_llm_provider="anthropic",
)
def test_is_azure_content_policy_violation_error_with_policy_violation_text(self):
"""Test detection of Azure content policy violation with explicit policy violation text"""