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