Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
7aa5b26
1
Parent(s): 9001c13
docs: design gemini rate limit cooldown
Browse files
docs/superpowers/specs/2026-05-06-gemini-rate-limit-cooldown-design.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Gemini Rate-Limit Cooldown Design
|
| 3 |
+
date: 2026-05-06
|
| 4 |
+
status: approved-for-planning
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Gemini Rate-Limit Cooldown Design
|
| 8 |
+
|
| 9 |
+
## Context
|
| 10 |
+
|
| 11 |
+
Lilith currently wraps chat model calls in `_RetryWrapper` with tenacity exponential backoff for provider rate-limit exceptions. This protects a single model call, but it does not coordinate across calls or GAIA tasks. If Gemini quota pressure persists after retries are exhausted, the runner can move to the next task and immediately repeat the same retry ladder against the same overloaded model lane.
|
| 12 |
+
|
| 13 |
+
For current usage, only two Google text models need first-class cooldown behavior:
|
| 14 |
+
|
| 15 |
+
| Model | RPM | TPM | RPD |
|
| 16 |
+
| --- | ---: | ---: | ---: |
|
| 17 |
+
| `gemini-3-flash-preview` | 2000 | 3,000,000 | 100,000 |
|
| 18 |
+
| `gemini-3.1-pro` | 1000 | 5,000,000 | 50,000 |
|
| 19 |
+
|
| 20 |
+
Gemma, embeddings, image, audio, video, and grounding quotas are out of scope for this patch.
|
| 21 |
+
|
| 22 |
+
## Goals
|
| 23 |
+
|
| 24 |
+
1. Avoid hammering Gemini after retry exhaustion.
|
| 25 |
+
2. Cool down the specific provider/model lane that is rate-limited.
|
| 26 |
+
3. Let GAIA batch runs wait, retry the same question once, then continue if still blocked.
|
| 27 |
+
4. Keep existing per-call exponential backoff behavior.
|
| 28 |
+
5. Preserve normal non-rate-limit error handling.
|
| 29 |
+
|
| 30 |
+
## Non-goals
|
| 31 |
+
|
| 32 |
+
- No memory embedding implementation.
|
| 33 |
+
- No embedding cooldown lane.
|
| 34 |
+
- No Gemma-specific token cap or quota profile.
|
| 35 |
+
- No persistent cross-process rate-limit database.
|
| 36 |
+
- No external queue, scheduler, or distributed throttler.
|
| 37 |
+
- No change to web-search Jaccard dedup.
|
| 38 |
+
|
| 39 |
+
## Proposed Architecture
|
| 40 |
+
|
| 41 |
+
### Model lanes
|
| 42 |
+
|
| 43 |
+
Represent each rate-limited Gemini model as a lane keyed by provider and model:
|
| 44 |
+
|
| 45 |
+
- `google:gemini-3-flash-preview`
|
| 46 |
+
- `google:gemini-3.1-pro`
|
| 47 |
+
|
| 48 |
+
Unknown models should continue to use the existing retry wrapper without profile-specific behavior. They may still share generic retry handling, but they should not get special cooldown assumptions from this design.
|
| 49 |
+
|
| 50 |
+
### In-process cooldown registry
|
| 51 |
+
|
| 52 |
+
Add a small module-level registry in `models.py`:
|
| 53 |
+
|
| 54 |
+
```text
|
| 55 |
+
(provider, model) -> cooldown_until_monotonic_seconds
|
| 56 |
+
(provider, model) -> consecutive_rate_limit_exhaustions
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Before invoking the wrapped model, `_RetryWrapper` checks whether its lane has an active cooldown. If active, it sleeps until the cooldown expires, then proceeds with the existing tenacity retry loop.
|
| 60 |
+
|
| 61 |
+
After a successful call, the lane's consecutive rate-limit exhaustion count resets to zero.
|
| 62 |
+
|
| 63 |
+
### Rate-limit exhaustion signal
|
| 64 |
+
|
| 65 |
+
Introduce a project-level exception such as `RateLimitCooldownError`. `_RetryWrapper` should raise it only when all retry attempts were exhausted due to a recognized retryable rate-limit exception.
|
| 66 |
+
|
| 67 |
+
The exception should carry enough information for callers and logs:
|
| 68 |
+
|
| 69 |
+
- provider
|
| 70 |
+
- model
|
| 71 |
+
- cooldown seconds
|
| 72 |
+
- original exception string
|
| 73 |
+
|
| 74 |
+
Non-rate-limit exceptions should keep their existing behavior and should not be rewritten as cooldown errors.
|
| 75 |
+
|
| 76 |
+
### Cooldown duration
|
| 77 |
+
|
| 78 |
+
Because both target models have high RPM, exhausted 429s are likely burst, token, or daily-quota pressure rather than ordinary request spacing. Use a conservative escalating cooldown for repeated failures on the same lane:
|
| 79 |
+
|
| 80 |
+
| Consecutive exhausted rate limits | Cooldown |
|
| 81 |
+
| ---: | ---: |
|
| 82 |
+
| 1 | 60 seconds |
|
| 83 |
+
| 2 | 120 seconds |
|
| 84 |
+
| 3+ | 300 seconds |
|
| 85 |
+
|
| 86 |
+
If the provider exception exposes an explicit retry delay later, a future patch can prefer that delay. This design does not require parsing provider-specific retry metadata.
|
| 87 |
+
|
| 88 |
+
### Runner behavior
|
| 89 |
+
|
| 90 |
+
`runner.py::run_agent_on_questions` should distinguish rate-limit cooldown failures from generic agent errors.
|
| 91 |
+
|
| 92 |
+
For each GAIA task:
|
| 93 |
+
|
| 94 |
+
1. Invoke the graph normally.
|
| 95 |
+
2. If a `RateLimitCooldownError` escapes:
|
| 96 |
+
- log the task, model, and cooldown duration,
|
| 97 |
+
- sleep for the indicated cooldown,
|
| 98 |
+
- retry the same task once from a fresh ephemeral memory context.
|
| 99 |
+
3. If the retry succeeds, format and checkpoint the answer normally.
|
| 100 |
+
4. If the retry still fails with rate-limit exhaustion, append an `AGENT ERROR: RATE LIMITED` answer for this run and continue to the next question.
|
| 101 |
+
5. Do not write a normal success checkpoint for rate-limited failures.
|
| 102 |
+
|
| 103 |
+
This keeps reruns possible after quota resets and avoids falsely treating a quota failure as a completed answer.
|
| 104 |
+
|
| 105 |
+
## Data Flow
|
| 106 |
+
|
| 107 |
+
```text
|
| 108 |
+
GAIA task
|
| 109 |
+
-> runner invokes graph
|
| 110 |
+
-> model node calls Gemini lane
|
| 111 |
+
-> _RetryWrapper checks lane cooldown
|
| 112 |
+
-> tenacity retries per call
|
| 113 |
+
-> exhausted 429 records lane cooldown
|
| 114 |
+
-> RateLimitCooldownError bubbles up
|
| 115 |
+
-> runner waits cooldown
|
| 116 |
+
-> runner retries same GAIA task once
|
| 117 |
+
-> success checkpoints answer OR failure continues without success checkpoint
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## Error Handling
|
| 121 |
+
|
| 122 |
+
- Rate-limit exhaustion should be logged at warning level with provider, model, and cooldown.
|
| 123 |
+
- Generic graph/model/tool errors should continue through the existing `AGENT ERROR` path.
|
| 124 |
+
- A failed retry after cooldown should not crash the whole batch.
|
| 125 |
+
- A successful call after cooldown should reset that model lane's consecutive rate-limit count.
|
| 126 |
+
|
| 127 |
+
## Testing Plan
|
| 128 |
+
|
| 129 |
+
Add focused unit tests without hitting external APIs:
|
| 130 |
+
|
| 131 |
+
1. `_RetryWrapper` sleeps when a lane cooldown is active.
|
| 132 |
+
2. `_RetryWrapper` records a 60-second cooldown after first exhausted retryable error.
|
| 133 |
+
3. Consecutive exhausted rate-limit errors escalate to 120 seconds and then 300 seconds.
|
| 134 |
+
4. Successful calls reset the consecutive failure counter.
|
| 135 |
+
5. `run_agent_on_questions` retries the same task once after `RateLimitCooldownError`.
|
| 136 |
+
6. `run_agent_on_questions` does not write a normal checkpoint when both attempts are rate-limited.
|
| 137 |
+
7. Unknown/non-Google models do not receive Gemini-specific cooldown profiles.
|
| 138 |
+
|
| 139 |
+
## Risks and Mitigations
|
| 140 |
+
|
| 141 |
+
- **Risk: sleeping in tests slows the suite.**
|
| 142 |
+
- Mitigation: inject or monkeypatch sleep/time functions in tests.
|
| 143 |
+
|
| 144 |
+
- **Risk: cooldown is only in-process.**
|
| 145 |
+
- Mitigation: acceptable for current local batch runs; cross-process persistence is a non-goal.
|
| 146 |
+
|
| 147 |
+
- **Risk: same-question retry duplicates some work.**
|
| 148 |
+
- Mitigation: retry only once and use a fresh ephemeral memory store, matching existing per-task isolation.
|
| 149 |
+
|
| 150 |
+
- **Risk: provider raises a different 429 exception shape.**
|
| 151 |
+
- Mitigation: reuse the existing `RETRY_EXCEPTIONS` list so cooldown behavior follows current retry classification.
|
| 152 |
+
|
| 153 |
+
## Implementation Boundaries
|
| 154 |
+
|
| 155 |
+
Expected files:
|
| 156 |
+
|
| 157 |
+
- `src/lilith_agent/models.py`
|
| 158 |
+
- `src/lilith_agent/runner.py`
|
| 159 |
+
- `tests/test_models.py`
|
| 160 |
+
- `tests/test_runner.py` or a new focused runner test file
|
| 161 |
+
|
| 162 |
+
Do not modify:
|
| 163 |
+
|
| 164 |
+
- `memory.py`
|
| 165 |
+
- `app.py` web-search dedup logic
|
| 166 |
+
- embedding dependencies or schema
|
| 167 |
+
- unrelated untracked files
|