mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(anthropic): only treat the upstream error body as a usage limit
Locally raised validation errors echo the caller's messages, so matching "api usage limit" anywhere in the error string let request content turn a 400 into a 429 and cool down shared deployments. Parse Anthropic's structured error body and check error.message instead, and drop the substring check from the provider-agnostic rate-limit helper.
This commit is contained in:
parent
c2b5b09080
commit
73609df1bf
2 changed files with 70 additions and 11 deletions
|
|
@ -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
|
||||
|
|
@ -38,11 +39,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:
|
||||
"""
|
||||
|
|
@ -78,14 +114,6 @@ class ExceptionCheckers:
|
|||
if "service tier capacity exceeded" in _error_str_lower:
|
||||
return True
|
||||
|
||||
#######################################
|
||||
# Anthropic returns spend/usage-limit exhaustion typed as an
|
||||
# invalid_request_error (HTTP 400), e.g. "You have reached your specified
|
||||
# API usage limits." It is a usage limit and should map to 429. (#37599)
|
||||
#######################################
|
||||
if "api usage limit" in _error_str_lower:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -601,7 +629,7 @@ def _map_anthropic_exception(
|
|||
# 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_error_str_rate_limit(error_str, getattr(original_exception, "status_code", None)):
|
||||
if ExceptionCheckers.is_anthropic_usage_limit_error(error_str):
|
||||
raise RateLimitError(
|
||||
message=f"AnthropicError - {error_str}",
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -216,7 +216,38 @@ class TestExceptionCheckers:
|
|||
'"message":"You have reached your specified API usage limits. '
|
||||
'You will regain access on 2026-09-01 at 00:00 UTC."}}'
|
||||
)
|
||||
assert ExceptionCheckers.is_error_str_rate_limit(error_str, status_code=400) is True
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue