The fix promised in #49's follow-ups: rate-limit waits no longer hide in benchmark timings.
Why
On a 429, the client waits 20 s and retries once, and nothing records the wait. On 2026-09-27, fast models under the unit-test key's 200k tokens/minute limit had 20–45 s runs that were probably waits, not work. So the timing comparison in #49 is uncertain for those runs. Success rates weren't affected.
What changes
llm.py: both clients set last_rate_limit_wait_s after every call. It's set even when the call then fails, since a limit that never cleared still cost the run its wait, and reset per call.
context.py:BudgetedClient passes it through, like last_cost.
bench/runner.py:MeteringClient sums the waits, counting them even when the call raises. RunRecord gains rate_limit_wait_s, defaulting to 0.
bench/results.py: the report's time column now shows working time, leaving waits out. A new 429 column counts the runs in each row that waited, with a footnote explaining it.
Results stored before this change load unchanged: a missing field reads as 0.
Tests
test_llm.py: the wait is reported for a limit that clears and for one that doesn't, and it doesn't carry over to the next call.
test_bench.py: metering counts waits even when the call fails, and a run records its waits. The report leaves waits out of time and counts the runs that waited. Old results load unchanged.
CI steps run locally: ruff check, ruff format --check, and pytest -m "not integration" (403 passed).
The fix promised in #49's follow-ups: rate-limit waits no longer hide in benchmark timings.
## Why
On a 429, the client waits 20 s and retries once, and nothing records the wait. On 2026-09-27, fast models under the unit-test key's 200k tokens/minute limit had 20–45 s runs that were probably waits, not work. So the timing comparison in #49 is uncertain for those runs. Success rates weren't affected.
## What changes
- **`llm.py`:** both clients set `last_rate_limit_wait_s` after every call. It's set even when the call then fails, since a limit that never cleared still cost the run its wait, and reset per call.
- **`context.py`:** `BudgetedClient` passes it through, like `last_cost`.
- **`bench/runner.py`:** `MeteringClient` sums the waits, counting them even when the call raises. `RunRecord` gains `rate_limit_wait_s`, defaulting to 0.
- **`bench/results.py`:** the report's **time** column now shows working time, leaving waits out. A new **429** column counts the runs in each row that waited, with a footnote explaining it.
Results stored before this change load unchanged: a missing field reads as 0.
## Tests
- **`test_llm.py`:** the wait is reported for a limit that clears and for one that doesn't, and it doesn't carry over to the next call.
- **`test_bench.py`:** metering counts waits even when the call fails, and a run records its waits. The report leaves waits out of time and counts the runs that waited. Old results load unchanged.
CI steps run locally: `ruff check`, `ruff format --check`, and `pytest -m "not integration"` (403 passed).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The client waits RATE_LIMIT_RETRY_S and retries once on a 429, silently.
A benchmark run that waited looked slower than the model is: on
2026-09-27, fast models under the unit-test key's 200k tokens/minute
limit had 20-45 s runs that were probably waits.
Both clients now report last_rate_limit_wait_s after every call, even
one that then fails. The benchmark sums it per run into
rate_limit_wait_s, the report's time column leaves it out, and a new
429 column counts the runs that waited. Results stored before this
load unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The fix promised in #49's follow-ups: rate-limit waits no longer hide in benchmark timings.
Why
On a 429, the client waits 20 s and retries once, and nothing records the wait. On 2026-09-27, fast models under the unit-test key's 200k tokens/minute limit had 20–45 s runs that were probably waits, not work. So the timing comparison in #49 is uncertain for those runs. Success rates weren't affected.
What changes
llm.py: both clients setlast_rate_limit_wait_safter every call. It's set even when the call then fails, since a limit that never cleared still cost the run its wait, and reset per call.context.py:BudgetedClientpasses it through, likelast_cost.bench/runner.py:MeteringClientsums the waits, counting them even when the call raises.RunRecordgainsrate_limit_wait_s, defaulting to 0.bench/results.py: the report's time column now shows working time, leaving waits out. A new 429 column counts the runs in each row that waited, with a footnote explaining it.Results stored before this change load unchanged: a missing field reads as 0.
Tests
test_llm.py: the wait is reported for a limit that clears and for one that doesn't, and it doesn't carry over to the next call.test_bench.py: metering counts waits even when the call fails, and a run records its waits. The report leaves waits out of time and counts the runs that waited. Old results load unchanged.CI steps run locally:
ruff check,ruff format --check, andpytest -m "not integration"(403 passed).🤖 Generated with Claude Code