feat(rate-limit): stepped cooldown — rebased + aux client coverage (supersedes #3910) - #12250
Closed
thapecroth wants to merge 3 commits into
Closed
thapecroth wants to merge 3 commits into
thapecroth wants to merge 3 commits into
Conversation
Adds agent/rate_limiter.py — a thread-safe per-model cooldown ladder (30s → 60s → 5min, resets after 10min idle) for cases where provider fallback and credential rotation can't save us. Originally from the feat/rate-limiter branch (1ddb03b); cherry-picked onto current main unmodified, since the original wiring targeted a March-30 snapshot of run_agent.py / auxiliary_client.py. Wiring lands in follow-up commits. Includes the original 23-test suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends agent.rate_limiter coverage to the paths that actually fire
today:
- auxiliary_client.{call_llm,async_call_llm}: check cooldown before
create(); record on 429. Prevents vision/session-search from
flooding a rate-limited model with repeated calls inside the
cooldown window (observed as 9 x 429 "temporarily overloaded" on
vision in recent logs).
- run_agent.py main loop: when rate-limited and the provider didn't
return Retry-After, use the stepped cooldown as wait_time instead
of jittered_backoff (which caps at 60s — too tight for transient
provider overloads).
Also widens _is_payment_error to catch z.ai's 429 code 1311
"subscription plan does not yet include <model>" so it routes to the
payment fallback chain (GLM-5V-Turbo wasn't in the user's coding
plan; a cooldown would do nothing for a permanent plan issue). Adds
_is_rate_limit_error as the counterpart classifier.
Adds _log_400_diag — on HTTP 400 from any aux call, logs the kwargs
key list and message block shape (no content — image data URLs would
bloat the log). Makes the next 1210 "Invalid API parameter"
regression diagnosable at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_run_git now clears any index.lock older than 60s in the shadow repo before invoking git. Checkpoint ops per shadow are strictly serial (one gateway agent per session), so an old lock at entry is unambiguously orphaned from a crashed or killed git subprocess. Observed impact without this: a single crashed git add wedged checkpointing on one shadow for 6 days (56+ identical errors in the log, all fatal: Unable to create '.../index.lock': File exists). 60s threshold is conservative — real git ops finish in milliseconds, so any lock that old is a zombie. Adds 4 tests: removal of stale lock, preservation of fresh lock, noop when no lock, and an end-to-end that shows git add -A succeeds after cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Closing after direction check — maintainer triage on #3910 ("not pursuing this approach") and similar triage on the closely-related #12148 makes it clear the repo is moving toward proactive concurrency limiting (#7479) rather than reactive cooldown ladders. Splitting the orthogonal, uncontroversial pieces of this PR into two small focused PRs:
Links to the replacements will follow shortly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Per-model stepped rate-limit cooldown (ladder: 30s → 60s → 5min, resets after 10 min idle, tracked per model). Replaces the generic jittered backoff on 429s when provider fallback and credential rotation can't save us.
Rate limiter module + tests are cherry-picked verbatim from @teknium1's #3910. This PR rebases that work onto current
mainand extends the wiring to cover the auxiliary client path.Why a new PR instead of updating #3910
I considered three options:
feat/rate-limiterto auto-update feat: per-model rate limit handler with stepped cooldown #3910. Rejected — that branch is authored by another contributor, and overwriting someone else's branch without coordination is the wrong default.run_agent.py/auxiliary_client.py) and easier to show than describe.If maintainers prefer, #3910 can be closed in favor of this one; attribution to the original author is preserved via commit message. I'll leave the decision there to maintainers rather than commenting on #3910 until this has been reviewed.
What changed vs. #3910
#3910 was cut 2026-03-30. Since then
run_agent.pyandauxiliary_client.pyhave evolved substantially (Opus 4.7 migration, vision resolver refactor, TCP keepalives, payment fallback chain, credential pool rotation). The original wiring no longer applies cleanly.agent/rate_limiter.py+ 23 tests — unchanged from #3910auxiliary_client.{call_llm,async_call_llm}— check cooldown beforecreate(), record on 429. Covers vision/session-search/compression, which #3910 didn't touch. (b)run_agent.pymain loop — stepped cooldown replacesjittered_backoffwhen rate-limited without aRetry-Afterheader. (c)_is_payment_errorextended to classify z.aicode 1311("subscription plan does not yet include") as payment, so it routes to the payment fallback chain instead of an infinitely-retripping cooldown. (d)_log_400_diag— logs kwargs keys + message block shape (no content) on HTTP 400s so the next1210 Invalid API parameterregression is diagnosable.tools/checkpoint_manager._clear_stale_lock— before any git op in a shadow repo, removeindex.lockfiles older than 60s. A single crashedgit addhad been wedging checkpointing on one prod shadow for 6 days (56+ identical errors). Checkpoint ops per shadow are strictly serial, so any lock older than 60s is unambiguously orphaned. Happy to split into a separate PR if preferred.Rationale for extending
_is_payment_errorz.ai returns HTTP 429 for two very different conditions:
code 1305/ "temporarily overloaded" — transient, cooldown helpscode 1311/ "subscription plan does not yet include <model>" — permanent, cooldown is a trapWithout classification, the stepped cooldown would just keep firing against a model the plan doesn't include. Routing
1311to the payment fallback chain lets a different vision backend pick up instead.Wiring order (existing logic preserved)
Retry-Afterheader present — honor it (cap 120s) — unchangedjittered_backoffwhich caps at 60s, too tight for provider overloads)Cooldown behavior (from #3910)
Per-model state. Resets after 10 min of no hits.
Test plan
pytest tests/agent/test_rate_limiter.py— 23/23 pass (unchanged from feat: per-model rate limit handler with stepped cooldown #3910)pytest tests/agent/test_auxiliary_client.py tests/agent/test_auxiliary_config_bridge.py tests/agent/test_rate_limit_tracker.py tests/agent/test_nous_rate_guard.py— 147/148 pass (1 pre-existing async-plugin failure on cleanmain, unchanged by this PR)pytest tests/run_agent/test_fallback_model.py tests/run_agent/test_long_context_tier_429.py tests/run_agent/test_provider_fallback.py tests/test_retry_utils.py— 64/64 passpytest tests/tools/test_checkpoint_manager.py tests/test_batch_runner_checkpoint.py— 69/69 pass incl. 4 new stale-lock testsRuntimeError: rate_limit_cooldown: <model> cooling down Ns remainingcode 1311 → _is_payment_error=True, _is_rate_limit_error=False;code 1305 → _is_payment_error=False, _is_rate_limit_error=Truegit stash+ rerun on cleanmainFor reviewers
auxiliary_client.py(+108 lines) andrun_agent.py(+24 lines). The aux-client change is small and defensively scoped — cooldown check gates the call, record happens on 429 only, errors other than rate-limit are untouched.Closes nothing automatically, since I can't close #3910 myself. Maintainers welcome to close #3910 at their discretion.
🤖 Generated with Claude Code