yc1838 commited on
Commit
2fade2b
·
1 Parent(s): a16b876

feat: pause batch on high rate limit window

Browse files
Files changed (2) hide show
  1. src/lilith_agent/runner.py +14 -0
  2. tests/test_runner.py +30 -0
src/lilith_agent/runner.py CHANGED
@@ -164,6 +164,8 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
164
  BatchAbortRateLimitError,
165
  QuestionRateLimitStreakError,
166
  RateLimitCooldownError,
 
 
167
  get_cheap_model,
168
  rate_limit_question_scope,
169
  )
@@ -179,6 +181,14 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
179
  with ephemeral_memory():
180
  return graph.invoke(task_state, {"configurable": {"thread_id": task_id}})
181
 
 
 
 
 
 
 
 
 
182
  for idx, question in enumerate(questions, start=1):
183
  reset_vision_state()
184
  task_id = question.get("task_id")
@@ -236,10 +246,12 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
236
  except RateLimitCooldownError as exc:
237
  log_runner.warning("[runner] task=%s rate limited after retry: %s", task_id, exc)
238
  answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
 
239
  continue
240
  except QuestionRateLimitStreakError as exc:
241
  log_runner.warning("[runner] task=%s rate limit streak: %s", task_id, exc)
242
  answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
 
243
  continue
244
  except BatchAbortRateLimitError as exc:
245
  log_runner.warning("[runner] task=%s batch abort rate limit: %s", task_id, exc)
@@ -261,6 +273,7 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
261
  "submitted_answer": f"AGENT ERROR: {exc}",
262
  }
263
  )
 
264
  continue
265
 
266
  last_m = result["messages"][-1]
@@ -297,6 +310,7 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
297
  (submitted_answer[:160] + "…") if len(submitted_answer) > 160 else submitted_answer,
298
  )
299
  answers.append({"task_id": task_id, "submitted_answer": submitted_answer.strip()})
 
300
 
301
  return answers
302
 
 
164
  BatchAbortRateLimitError,
165
  QuestionRateLimitStreakError,
166
  RateLimitCooldownError,
167
+ batch_rate_limit_pause_seconds,
168
+ clear_batch_rate_limit_window,
169
  get_cheap_model,
170
  rate_limit_question_scope,
171
  )
 
181
  with ephemeral_memory():
182
  return graph.invoke(task_state, {"configurable": {"thread_id": task_id}})
183
 
184
+ def _maybe_pause_for_batch_rate_limit() -> None:
185
+ pause_seconds = batch_rate_limit_pause_seconds()
186
+ if pause_seconds is None:
187
+ return
188
+ log_runner.warning("[runner] pausing batch for %ss due to rate limit window", pause_seconds)
189
+ time.sleep(pause_seconds)
190
+ clear_batch_rate_limit_window()
191
+
192
  for idx, question in enumerate(questions, start=1):
193
  reset_vision_state()
194
  task_id = question.get("task_id")
 
246
  except RateLimitCooldownError as exc:
247
  log_runner.warning("[runner] task=%s rate limited after retry: %s", task_id, exc)
248
  answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
249
+ _maybe_pause_for_batch_rate_limit()
250
  continue
251
  except QuestionRateLimitStreakError as exc:
252
  log_runner.warning("[runner] task=%s rate limit streak: %s", task_id, exc)
253
  answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
254
+ _maybe_pause_for_batch_rate_limit()
255
  continue
256
  except BatchAbortRateLimitError as exc:
257
  log_runner.warning("[runner] task=%s batch abort rate limit: %s", task_id, exc)
 
273
  "submitted_answer": f"AGENT ERROR: {exc}",
274
  }
275
  )
276
+ _maybe_pause_for_batch_rate_limit()
277
  continue
278
 
279
  last_m = result["messages"][-1]
 
310
  (submitted_answer[:160] + "…") if len(submitted_answer) > 160 else submitted_answer,
311
  )
312
  answers.append({"task_id": task_id, "submitted_answer": submitted_answer.strip()})
313
+ _maybe_pause_for_batch_rate_limit()
314
 
315
  return answers
316
 
tests/test_runner.py CHANGED
@@ -227,3 +227,33 @@ def test_runner_stops_batch_and_writes_abort_marker_on_daily_quota(tmp_path: Pat
227
  assert data["reason"] == "daily quota exhausted"
228
  assert not (tmp_path / "task-abort.json").exists()
229
  assert not (tmp_path / "task-never.json").exists()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  assert data["reason"] == "daily quota exhausted"
228
  assert not (tmp_path / "task-abort.json").exists()
229
  assert not (tmp_path / "task-never.json").exists()
230
+
231
+
232
+ class _GraphAlwaysSucceeds:
233
+ def __init__(self):
234
+ self.calls = 0
235
+
236
+ def invoke(self, state, config):
237
+ self.calls += 1
238
+ return {"messages": [AIMessage(content=f"answer-{self.calls}")]}
239
+
240
+
241
+ def test_runner_pauses_batch_when_window_trips(monkeypatch, tmp_path: Path):
242
+ pauses = [300, None]
243
+ sleeps = []
244
+ monkeypatch.setattr("lilith_agent.models.batch_rate_limit_pause_seconds", lambda: pauses.pop(0))
245
+ monkeypatch.setattr("lilith_agent.models.clear_batch_rate_limit_window", lambda: None)
246
+ monkeypatch.setattr("lilith_agent.runner.time.sleep", sleeps.append)
247
+ monkeypatch.setattr("lilith_agent.runner._final_formatting_cleanup", lambda model, question, raw, llm_formatter_enabled=True: raw)
248
+
249
+ answers = run_agent_on_questions(
250
+ _GraphAlwaysSucceeds(),
251
+ [
252
+ {"task_id": "task-a", "question": "a"},
253
+ {"task_id": "task-b", "question": "b"},
254
+ ],
255
+ tmp_path,
256
+ )
257
+
258
+ assert sleeps == [300]
259
+ assert [answer["task_id"] for answer in answers] == ["task-a", "task-b"]