yc1838 commited on
Commit
d256c19
·
1 Parent(s): 69842d5

feat: apply gemini cooldown to async model calls

Browse files
Files changed (2) hide show
  1. src/lilith_agent/models.py +48 -6
  2. tests/test_models.py +33 -0
src/lilith_agent/models.py CHANGED
@@ -224,6 +224,14 @@ def _sleep_active_cooldown(lane: tuple[str, str] | None) -> None:
224
  time.sleep(remaining)
225
 
226
 
 
 
 
 
 
 
 
 
227
  def _record_success(lane: tuple[str, str] | None) -> None:
228
  if lane is None:
229
  return
@@ -370,9 +378,26 @@ class _RetryWrapper(BaseChatModel):
370
  raise
371
 
372
  async def _agenerate(self, *args, **kwargs):
373
- async for attempt in AsyncRetrying(**_async_retry_params()):
374
- with attempt:
375
- return await self.inner._agenerate(*args, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
  def bind_tools(self, tools: Any, **kwargs: Any):
378
  bound = self.inner.bind_tools(tools, **kwargs)
@@ -410,9 +435,26 @@ class _BoundRetryWrapper(Runnable):
410
  raise
411
 
412
  async def ainvoke(self, input, config=None, **kwargs):
413
- async for attempt in AsyncRetrying(**_async_retry_params()):
414
- with attempt:
415
- return await self._bound.ainvoke(input, config=config, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
  def __getattr__(self, name):
418
  return getattr(self._bound, name)
 
224
  time.sleep(remaining)
225
 
226
 
227
+ async def _sleep_active_cooldown_async(lane: tuple[str, str] | None) -> None:
228
+ if lane is None:
229
+ return
230
+ remaining = _cooldown_until.get(lane, 0.0) - time.monotonic()
231
+ if remaining > 0:
232
+ await asyncio.sleep(remaining)
233
+
234
+
235
  def _record_success(lane: tuple[str, str] | None) -> None:
236
  if lane is None:
237
  return
 
378
  raise
379
 
380
  async def _agenerate(self, *args, **kwargs):
381
+ lane = _gemini_lane(self.provider, self.model_name)
382
+ await _sleep_active_cooldown_async(lane)
383
+ try:
384
+ async for attempt in AsyncRetrying(**_async_retry_params()):
385
+ with attempt:
386
+ try:
387
+ result = await self.inner._agenerate(*args, **kwargs)
388
+ except Exception as observed:
389
+ record_rate_limit_observation(observed)
390
+ raise
391
+ record_rate_limit_success()
392
+ _record_success(lane)
393
+ return result
394
+ except Exception as exc:
395
+ if lane is not None and is_retryable_rate_limit(exc):
396
+ reason = _batch_abort_reason(exc)
397
+ if reason is not None:
398
+ raise BatchAbortRateLimitError(reason=reason, original_error=str(exc)) from exc
399
+ raise _record_exhausted_rate_limit(lane, exc) from exc
400
+ raise
401
 
402
  def bind_tools(self, tools: Any, **kwargs: Any):
403
  bound = self.inner.bind_tools(tools, **kwargs)
 
435
  raise
436
 
437
  async def ainvoke(self, input, config=None, **kwargs):
438
+ lane = _gemini_lane(self._provider, self._model_name)
439
+ await _sleep_active_cooldown_async(lane)
440
+ try:
441
+ async for attempt in AsyncRetrying(**_async_retry_params()):
442
+ with attempt:
443
+ try:
444
+ result = await self._bound.ainvoke(input, config=config, **kwargs)
445
+ except Exception as observed:
446
+ record_rate_limit_observation(observed)
447
+ raise
448
+ record_rate_limit_success()
449
+ _record_success(lane)
450
+ return result
451
+ except Exception as exc:
452
+ if lane is not None and is_retryable_rate_limit(exc):
453
+ reason = _batch_abort_reason(exc)
454
+ if reason is not None:
455
+ raise BatchAbortRateLimitError(reason=reason, original_error=str(exc)) from exc
456
+ raise _record_exhausted_rate_limit(lane, exc) from exc
457
+ raise
458
 
459
  def __getattr__(self, name):
460
  return getattr(self._bound, name)
tests/test_models.py CHANGED
@@ -1,4 +1,5 @@
1
  import pytest
 
2
 
3
  from langchain_core.runnables import Runnable, RunnableLambda
4
  from langchain_core.messages import AIMessage
@@ -390,3 +391,35 @@ def test_bound_retry_wrapper_raises_cooldown_for_gemini_lane(monkeypatch):
390
  bound.invoke([("user", "hi")])
391
 
392
  assert raised.value.model == "gemini-3.1-pro"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import pytest
2
+ from unittest.mock import AsyncMock
3
 
4
  from langchain_core.runnables import Runnable, RunnableLambda
5
  from langchain_core.messages import AIMessage
 
391
  bound.invoke([("user", "hi")])
392
 
393
  assert raised.value.model == "gemini-3.1-pro"
394
+
395
+
396
+ @pytest.mark.asyncio
397
+ async def test_async_retry_wrapper_raises_cooldown_for_gemini_lane(monkeypatch):
398
+ _reset_rate_limit_state_for_tests()
399
+ exc = _make_genai_client_error(429)
400
+ monkeypatch.setattr("lilith_agent.models.asyncio.sleep", AsyncMock())
401
+ wrapper = _RetryWrapper.model_construct(
402
+ inner=_FailingGenerateModel(exc), provider="google", model_name="gemini-3.1-pro"
403
+ )
404
+
405
+ with pytest.raises(RateLimitCooldownError) as raised:
406
+ await wrapper._agenerate([])
407
+
408
+ assert raised.value.model == "gemini-3.1-pro"
409
+
410
+
411
+ @pytest.mark.asyncio
412
+ async def test_async_bound_retry_wrapper_raises_cooldown_for_gemini_lane(monkeypatch):
413
+ _reset_rate_limit_state_for_tests()
414
+ exc = _make_genai_client_error(429)
415
+ monkeypatch.setattr("lilith_agent.models.asyncio.sleep", AsyncMock())
416
+ wrapper = _RetryWrapper.model_construct(
417
+ inner=_FailingGenerateModel(exc), provider="google", model_name="gemini-3.1-pro"
418
+ )
419
+
420
+ bound = wrapper.bind_tools([])
421
+
422
+ with pytest.raises(RateLimitCooldownError) as raised:
423
+ await bound.ainvoke([("user", "hi")])
424
+
425
+ assert raised.value.model == "gemini-3.1-pro"