Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
03425af
1
Parent(s): be749ea
feat: classify gemini client rate limits
Browse files- src/lilith_agent/models.py +57 -28
- tests/test_models.py +31 -0
src/lilith_agent/models.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
from typing import Any, Sequence
|
| 4 |
|
| 5 |
from langchain_anthropic import ChatAnthropic
|
|
@@ -11,43 +14,69 @@ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
|
| 11 |
from langchain_ollama import ChatOllama
|
| 12 |
from langchain_openai import ChatOpenAI
|
| 13 |
|
| 14 |
-
import
|
| 15 |
-
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type, Retrying, AsyncRetrying
|
| 16 |
from lilith_agent.config import Config
|
| 17 |
|
| 18 |
-
# Identify retryable exceptions (429s) across providers
|
| 19 |
-
RETRY_EXCEPTIONS = []
|
| 20 |
try:
|
| 21 |
from google.api_core.exceptions import ResourceExhausted
|
| 22 |
-
RETRY_EXCEPTIONS.append(ResourceExhausted)
|
| 23 |
except ImportError:
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
try:
|
| 27 |
from anthropic import RateLimitError as AnthropicRateLimitError
|
| 28 |
-
RETRY_EXCEPTIONS.append(AnthropicRateLimitError)
|
| 29 |
except ImportError:
|
| 30 |
-
|
| 31 |
|
| 32 |
try:
|
| 33 |
from openai import RateLimitError as OpenAIRateLimitError
|
| 34 |
-
RETRY_EXCEPTIONS.append(OpenAIRateLimitError)
|
| 35 |
except ImportError:
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
try:
|
| 53 |
from langchain_community.cache import SQLiteCache
|
|
@@ -147,12 +176,12 @@ class _RetryWrapper(BaseChatModel):
|
|
| 147 |
return f"retry-{self.inner._llm_type}"
|
| 148 |
|
| 149 |
def _generate(self, *args, **kwargs):
|
| 150 |
-
for attempt in Retrying(**
|
| 151 |
with attempt:
|
| 152 |
return self.inner._generate(*args, **kwargs)
|
| 153 |
|
| 154 |
async def _agenerate(self, *args, **kwargs):
|
| 155 |
-
async for attempt in AsyncRetrying(**
|
| 156 |
with attempt:
|
| 157 |
return await self.inner._agenerate(*args, **kwargs)
|
| 158 |
|
|
@@ -169,12 +198,12 @@ class _BoundRetryWrapper(Runnable):
|
|
| 169 |
self._bound = bound
|
| 170 |
|
| 171 |
def invoke(self, input, config=None, **kwargs):
|
| 172 |
-
for attempt in Retrying(**
|
| 173 |
with attempt:
|
| 174 |
return self._bound.invoke(input, config=config, **kwargs)
|
| 175 |
|
| 176 |
async def ainvoke(self, input, config=None, **kwargs):
|
| 177 |
-
async for attempt in AsyncRetrying(**
|
| 178 |
with attempt:
|
| 179 |
return await self._bound.ainvoke(input, config=config, **kwargs)
|
| 180 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
from typing import Any, Sequence
|
| 7 |
|
| 8 |
from langchain_anthropic import ChatAnthropic
|
|
|
|
| 14 |
from langchain_ollama import ChatOllama
|
| 15 |
from langchain_openai import ChatOpenAI
|
| 16 |
|
| 17 |
+
from tenacity import AsyncRetrying, Retrying, retry_if_exception, stop_after_attempt, wait_exponential
|
|
|
|
| 18 |
from lilith_agent.config import Config
|
| 19 |
|
|
|
|
|
|
|
| 20 |
try:
|
| 21 |
from google.api_core.exceptions import ResourceExhausted
|
|
|
|
| 22 |
except ImportError:
|
| 23 |
+
ResourceExhausted = None
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
from google.genai.errors import ClientError as GenAIClientError
|
| 27 |
+
except ImportError:
|
| 28 |
+
GenAIClientError = None
|
| 29 |
|
| 30 |
try:
|
| 31 |
from anthropic import RateLimitError as AnthropicRateLimitError
|
|
|
|
| 32 |
except ImportError:
|
| 33 |
+
AnthropicRateLimitError = None
|
| 34 |
|
| 35 |
try:
|
| 36 |
from openai import RateLimitError as OpenAIRateLimitError
|
|
|
|
| 37 |
except ImportError:
|
| 38 |
+
OpenAIRateLimitError = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def is_retryable_rate_limit(exc: BaseException) -> bool:
|
| 42 |
+
if ResourceExhausted is not None and isinstance(exc, ResourceExhausted):
|
| 43 |
+
return True
|
| 44 |
+
if GenAIClientError is not None and isinstance(exc, GenAIClientError):
|
| 45 |
+
return getattr(exc, "code", None) == 429
|
| 46 |
+
if AnthropicRateLimitError is not None and isinstance(exc, AnthropicRateLimitError):
|
| 47 |
+
return True
|
| 48 |
+
if OpenAIRateLimitError is not None and isinstance(exc, OpenAIRateLimitError):
|
| 49 |
+
return True
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _tenacity_sleep(seconds: float) -> None:
|
| 54 |
+
time.sleep(seconds)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
async def _async_tenacity_sleep(seconds: float) -> None:
|
| 58 |
+
await asyncio.sleep(seconds)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _base_retry_params() -> dict[str, Any]:
|
| 62 |
+
return dict(
|
| 63 |
+
retry=retry_if_exception(is_retryable_rate_limit),
|
| 64 |
+
wait=wait_exponential(multiplier=2, min=4, max=60),
|
| 65 |
+
stop=stop_after_attempt(5),
|
| 66 |
+
before_sleep=lambda retry_state: log.warning(
|
| 67 |
+
f"LLM Rate Limit (429) hit. Retrying in {retry_state.next_action.sleep}s... "
|
| 68 |
+
f"(Attempt {retry_state.attempt_number}/5)"
|
| 69 |
+
),
|
| 70 |
+
reraise=True,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _sync_retry_params() -> dict[str, Any]:
|
| 75 |
+
return {**_base_retry_params(), "sleep": _tenacity_sleep}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _async_retry_params() -> dict[str, Any]:
|
| 79 |
+
return {**_base_retry_params(), "sleep": _async_tenacity_sleep}
|
| 80 |
|
| 81 |
try:
|
| 82 |
from langchain_community.cache import SQLiteCache
|
|
|
|
| 176 |
return f"retry-{self.inner._llm_type}"
|
| 177 |
|
| 178 |
def _generate(self, *args, **kwargs):
|
| 179 |
+
for attempt in Retrying(**_sync_retry_params()):
|
| 180 |
with attempt:
|
| 181 |
return self.inner._generate(*args, **kwargs)
|
| 182 |
|
| 183 |
async def _agenerate(self, *args, **kwargs):
|
| 184 |
+
async for attempt in AsyncRetrying(**_async_retry_params()):
|
| 185 |
with attempt:
|
| 186 |
return await self.inner._agenerate(*args, **kwargs)
|
| 187 |
|
|
|
|
| 198 |
self._bound = bound
|
| 199 |
|
| 200 |
def invoke(self, input, config=None, **kwargs):
|
| 201 |
+
for attempt in Retrying(**_sync_retry_params()):
|
| 202 |
with attempt:
|
| 203 |
return self._bound.invoke(input, config=config, **kwargs)
|
| 204 |
|
| 205 |
async def ainvoke(self, input, config=None, **kwargs):
|
| 206 |
+
async for attempt in AsyncRetrying(**_async_retry_params()):
|
| 207 |
with attempt:
|
| 208 |
return await self._bound.ainvoke(input, config=config, **kwargs)
|
| 209 |
|
tests/test_models.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
from langchain_core.runnables import Runnable, RunnableLambda
|
| 2 |
from langchain_core.messages import AIMessage
|
| 3 |
|
|
@@ -6,6 +8,7 @@ from lilith_agent.models import (
|
|
| 6 |
_NoThinkWrapper,
|
| 7 |
_BoundRetryWrapper,
|
| 8 |
_BoundNoThinkWrapper,
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
|
|
@@ -53,3 +56,31 @@ def test_no_think_wrapper_bind_tools_returns_runnable():
|
|
| 53 |
|
| 54 |
assert isinstance(bound, _BoundNoThinkWrapper)
|
| 55 |
assert isinstance(bound, Runnable)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
from langchain_core.runnables import Runnable, RunnableLambda
|
| 4 |
from langchain_core.messages import AIMessage
|
| 5 |
|
|
|
|
| 8 |
_NoThinkWrapper,
|
| 9 |
_BoundRetryWrapper,
|
| 10 |
_BoundNoThinkWrapper,
|
| 11 |
+
is_retryable_rate_limit,
|
| 12 |
)
|
| 13 |
|
| 14 |
|
|
|
|
| 56 |
|
| 57 |
assert isinstance(bound, _BoundNoThinkWrapper)
|
| 58 |
assert isinstance(bound, Runnable)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _make_genai_client_error(code: int):
|
| 62 |
+
pytest.importorskip("google.genai.errors")
|
| 63 |
+
from google.genai.errors import ClientError
|
| 64 |
+
|
| 65 |
+
return ClientError(
|
| 66 |
+
code,
|
| 67 |
+
{
|
| 68 |
+
"error": {
|
| 69 |
+
"code": code,
|
| 70 |
+
"status": "RESOURCE_EXHAUSTED" if code == 429 else "INVALID_ARGUMENT",
|
| 71 |
+
"message": "test error",
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_genai_client_error_429_is_retryable():
|
| 78 |
+
exc = _make_genai_client_error(429)
|
| 79 |
+
|
| 80 |
+
assert is_retryable_rate_limit(exc) is True
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def test_genai_client_error_400_is_not_retryable():
|
| 84 |
+
exc = _make_genai_client_error(400)
|
| 85 |
+
|
| 86 |
+
assert is_retryable_rate_limit(exc) is False
|