Skip to content

feat(llm): cross-worker quota headroom store, persist real 429 headroom - #16483

Merged
mrveiss merged 6 commits into
mainfrom
issue-16480-quota-harvest-errors
Sep 14, 2026
Merged

mrveiss merged 6 commits into
mainfrom
issue-16480-quota-harvest-errors

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #16480 (sub-issue of #15026, covering exactly the error/429 harvest path — the success path is #16476, separately tracked and not in scope here).

Single-issue rationale: #16480 covers the quota headroom store and harvesting on the 429/error path only. No other open issue shares that scope. #16476, its sibling under #15026, covers success-path harvesting, which needs per-provider SDK raw-response access, so it is a different change and is tracked separately. #15028, #15029 and #15030 consume observed headroom (routing, account selection and the provider view) rather than record it.

Supersedes #16477, opened from the wrong branch (gh issue develop 15026 directly, which — since the default-branch switch — makes any PR from it a closing reference for #15026 regardless of body wording, even for a deliberately partial delivery). Same commits, same diff, opened from a sub-issue-linked branch instead so closingIssuesReferences resolves correctly. #16477 is closed with a pointer here.

  • llm_shared/quota_headroom.py — new Redis-backed QuotaHeadroomStore, mirroring provider_degradation.py's pattern (llm_shared: persistent cross-worker provider degradation marking (Redis TTL) #11519): in-process fallback when Redis is down, never raises into a caller, TTL from a registered env var. Key: (provider, account_id, window). account_id defaults to "default" so llm-auth: one credential per provider per tenant — add a multi-account credential pool with headroom-based selection #15029's future multi-account pool can extend this without a schema change. Absence is a first-class state — no entry means "no signal yet," never fabricated as a zero.
  • llm_shared/rate_limit_backoff.py — raise_if_rate_limited() now persists a remaining=0/resets_at reading on every 429, fire-and-forget (matches base_provider.py's existing _notify_* pattern), before raising — so a store hiccup can never affect the existing raise-and-retry behavior. All 15 pre-existing tests pass unchanged.
  • autobot-backend/conftest.py — real-loads llm_shared.quota_headroom into the llm_shared test stub's per-submodule allowlist (the same mechanism provider_degradation/rate_limit_backoff/base_provider already use).
  • autobot_shared/env_registry_ai.py — registers AUTOBOT_LLM_QUOTA_HEADROOM_TTL_SECONDS.

Acceptance criteria (#16480)

  • Headroom is recorded for every provider that returns a rate-limit signal on the error/429 path
  • The store is correct across multiple workers (two QuotaHeadroomStore instances sharing a fakeredis FakeServer, mirroring test_provider_degradation.py's verification style)
  • Redis unavailable → in-process fallback, no exception reaches the caller
  • A provider/window with no recorded entry yields no entry — distinguishable from "zero remaining"
  • rate_limit_backoff.py behaviour is unchanged for every existing caller — this is additive
  • Tests: harvest-on-429, absent-headers, Redis-down, expiry, cross-worker visibility

#15026 itself stays open — its success-path harvesting criteria are #16476, not delivered here.

Test plan

660 tests pass in llm_shared/ (128 skipped — heavy/optional deps). New: 14 tests for QuotaHeadroomStore, 4 new async tests for the 429-persistence wiring in rate_limit_backoff_test.py. Pre-push hook ran the relevant subset and passed.

Thinking Path

rate_limit_backoff.raise_if_rate_limited() already parsed a provider's 429 response, then discarded the reading instead of recording it. #15026 asks for a cross-worker quota headroom store fed from provider rate-limit signals. #16480 is the slice that can land now: the store itself, fed from the 429 path. Success-path harvesting needs per-provider SDK access and is #16476.

The store follows provider_degradation.py's existing pattern (#11519) rather than inventing a new one: Redis-backed, an in-process fallback when Redis is down, and never raising into a caller. The 429 write is fire-and-forget and happens before the existing raise, so the retry behaviour current callers depend on can't change. A missing entry means "no signal yet", never zero remaining, so a consumer can't read silence as exhaustion.

What Changed

  • autobot-backend/llm_shared/quota_headroom.py (new, +220): QuotaHeadroomStore, keyed by (provider, account_id, window), with account_id defaulting to "default". Redis-backed with an in-process fallback. A reading expires after AUTOBOT_LLM_QUOTA_HEADROOM_TTL_SECONDS.
  • autobot-backend/llm_shared/rate_limit_backoff.py (+33/-0): raise_if_rate_limited() records remaining=0 and resets_at on every 429, fire-and-forget, before the existing raise.
  • autobot_shared/env_registry_ai.py (+10/-0): registers AUTOBOT_LLM_QUOTA_HEADROOM_TTL_SECONDS (int, default 3600). docs/developer/CLAUDE_RULES.md (+2/-1) gets the matching row in its generated env-var table, whose count goes from 219 to 220.
  • autobot-backend/conftest.py (+5/-0): real-loads llm_shared.quota_headroom in the llm_shared test stub, ahead of rate_limit_backoff, which imports it at module level.
  • repo_tests/python_file_size_ratchet_baseline.py and scripts/python_file_size_known_large.py (+1/-1 each): the autobot-backend/conftest.py ceiling goes from 1580 to 1585 for those five lines.
  • changelog/unreleased/15026-quota-headroom-store.md (new): feature entry.
  • Tests: autobot-backend/llm_shared/tests/test_quota_headroom.py (new, +239) and autobot-backend/llm_shared/rate_limit_backoff_test.py (+85/-0), listed under Verification.

Verification

CI on this PR is the verification of record. Nothing was re-run locally when these template sections were added; the "Test plan" section above reports the author's earlier local run.

  • autobot-backend/llm_shared/tests/test_quota_headroom.py: 14 tests for QuotaHeadroomStore. They cover record and get; an absent entry returning None rather than zero; expiry restoring the absent state; filtering by provider; account_id scoping; cross-worker visibility through two stores sharing one fakeredis FakeServer; the no-Redis fallback for record, expiry and listing; and the fraction-consumed calculation, including an unknown limit, an unknown remaining count, a zero limit and a negative remaining count.
  • autobot-backend/llm_shared/rate_limit_backoff_test.py: 4 new async tests. A 429 records zero remaining; a 429 with Retry-After sets resets_at; an OK response persists nothing; and a missing provider neither persists nor raises a scheduling error. The file's existing tests are untouched (+85/-0).
  • The acceptance-criteria mapping is in the "Acceptance criteria (llm: persist observed provider headroom on the 429/error path #16480)" section above.

Model Used

Claude (AutoBot coordinator fleet)

🤖 Generated with Claude Code

@mrveiss mrveiss linked an issue Sep 12, 2026 that may be closed by this pull request
6 tasks done
@mrveiss mrveiss added this to the v0.9.0 milestone Sep 12, 2026
@mrveiss mrveiss self-assigned this Sep 12, 2026
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 19 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 22e6f9f6-025e-47e0-830f-e33b4ee0fcbe

📥 Commits

Reviewing files that changed from the base of the PR and between a494d4c and 59e37d1.

⛔ Files ignored due to path filters (2)
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py
📒 Files selected for processing (9)
  • autobot-backend/conftest.py
  • autobot-backend/llm_shared/quota_headroom.py
  • autobot-backend/llm_shared/rate_limit_backoff.py
  • autobot-backend/llm_shared/rate_limit_backoff_test.py
  • autobot-backend/llm_shared/tests/test_quota_headroom.py
  • autobot-backend/testkit/module_stubs.py
  • autobot_shared/env_registry_ai.py
  • changelog/unreleased/15026-quota-headroom-store.md
  • docs/developer/CLAUDE_RULES.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

There is no PR queue limit, and this is not a request to defer this PR. Work proceeds one issue at a time without a cap on open PRs; review capacity is the constraint.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

This was referenced Sep 12, 2026
@mrveiss

mrveiss commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

Delta review at 5c194d3 (rebase onto current main, per a7): APPROVE.

Verified the fix for the blocking finding from my earlier review directly: the three tests in TestPersistHeadroomFrom429 now mock _get_redis to raise RedisConnectionError (properly imported), not the bare RuntimeError that didn't match _STORE_FAILURES's narrow catch tuple. This correctly exercises the in-process fallback path the tests are meant to test.

CI: 0 fail, 34 success, 24 pending. Good to merge once green.

Main gained a new AUTOBOT_* var since this branch's own env_registry_ai.py
addition landed, so the autogenerated table (and its count line, 225 -> 226)
had drifted behind pipeline-scripts/check_env_var_registry.py's live count.
@mrveiss

mrveiss commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

Delta review at d466e37: APPROVE. Verified the fix directly — docs/developer/CLAUDE_RULES.md's autogen footer now reads 226, generated via the sanctioned generate_env_docs.py rather than a hand-edited count. CI: 0 fail so far (fresh run). Good to merge once green.

…#16480)

Adding llm_shared.quota_headroom to conftest.py's real-load sequence
pushed it from 1570 to 1575 lines, raising the file-size ratchet's
ceiling to admit it -- against the ratchet-only-shrinks rule. Root
cause wasn't the 5-line addition; it's that _make_pkg_stub and
_real_load_and_bind are 74 lines of general-purpose sys.modules
stubbing/real-loading with zero conftest-specific state (57 and 21
call sites respectively, all unaffected by where the functions live),
sitting in conftest.py instead of testkit/module_stubs.py where the
rest of this exact concern already lives (#13451).

Moved both, unchanged, to testkit/module_stubs.py; conftest.py now
imports and aliases them back to their original names so every call
site is untouched. Net effect: conftest.py drops to 1507 lines
(black-formatted, real measured count) -- the quota_headroom addition
fits with room to spare, so the ceiling in both baseline files goes
down to 1507, not back to 1570.

Verified with the file-size ratchet suite, the quota-headroom/
rate-limit-backoff tests that exercise this exact real-load path, the
module_stubs test suite, and a real-load call site outside llm_shared
(tool_output_filter) as a broader sanity check -- 88 tests total, all
passing; behavior-preserving move, zero logic changes.
@mrveiss

mrveiss commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

Delta review at 59e37d1 (from d466e37): approve.

This corrects a finding on my own earlier approval — I missed that the previous head raised the ratchet ceiling (1570→1575) rather than extracting, which a7 caught. This delta does it right: extracts instead of exempting.

Verified independently (reading, not running):

CI: no FAILURE/ERROR/TIMED_OUT checks; rest still queued on the fresh push. Mergeable.

@mrveiss
mrveiss merged commit 80c68ae into main Sep 14, 2026
78 checks passed
@mrveiss
mrveiss deleted the issue-16480-quota-harvest-errors branch September 14, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

llm: persist observed provider headroom on the 429/error path

1 participant