Skip to content

Add optional client-side rate limiting (max_rpm) and preserve partial results on parallel failure - #520

Closed
ojassharma7 wants to merge 3 commits into
google:mainfrom
ojassharma7:feat/gemini-throttle-and-partial-results
Closed

ojassharma7 wants to merge 3 commits into
google:mainfrom
ojassharma7:feat/gemini-throttle-and-partial-results

Conversation

@ojassharma7

@ojassharma7 ojassharma7 commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

Addresses the two still-open items of problem #2 in #358 (Gemini 429 RESOURCE_EXHAUSTED handling). The exponential backoff that issue also asked for already landed in 3aab86c, so this PR covers what remains:

  • max_rpm — client-side throttle. A new optional GeminiLanguageModel parameter. When > 0, real-time requests are spaced by a thread-safe leaky bucket so at most max_rpm start per minute (shared across max_workers). This prevents 429 RESOURCE_EXHAUSTED on quota-limited tiers (e.g. the free tier's 15 RPM) rather than only retrying after the fact. Defaults to 0 (disabled) — no behavior change for existing callers. Does not apply to the Batch API.
  • Preserve completed chunks on failure. The parallel infer() path raised on the first failed chunk, which abandoned the still-running futures and discarded every chunk that had already succeeded. It now lets the pool drain, then raises once with the completed work attached to InferenceRuntimeError (partial_results, aligned to the input prompts, plus failed_indices). The all-or-nothing raise is preserved, so annotation's zip(batch, outputs) alignment is unaffected; callers who want to recover already-done work can now read it off the exception.

This is a partial fix for #358 (chunking for very large documents is a separate design question, not touched here), so:

Related to #358

Type: Feature (the max_rpm throttle; the partial-result change also fixes a real loss-of-work bug).

How Has This Been Tested?

Unit tests in tests/gemini_ratelimit_partial_test.py cover the limiter's spacing (mocked clock), max_rpm wiring + validation, and partial-result preservation across the all-success, single-failure, and multi-failure cases.

$ python -m pytest tests/gemini_ratelimit_partial_test.py tests/gemini_retry_test.py

Differential-clean against the full suite: the only failures on my machine are pre-existing and environmental (the optional openai extra isn't installed, plus 3 plugin-packaging tests) — identical with and without this change — and the 10 new tests pass on top. pyink + isort + pylint --rcfile=.pylintrc / --rcfile=tests/.pylintrc (CI's exact configs) all clean.

Checklist:

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

⚠️ Branch Update Required

Your branch is 1 commits behind main. Please update your branch to ensure CI checks run with the latest code:

git fetch origin main
git merge origin/main
git push

Note: Enable "Allow edits by maintainers" to allow automatic updates.

@ojassharma7
ojassharma7 force-pushed the feat/gemini-throttle-and-partial-results branch from 4b318b0 to 7e535ee Compare September 11, 2026 20:44
@ojassharma7

Copy link
Copy Markdown
Contributor Author

Rebased onto main to clear the conflict with #534.

Worth flagging what the conflict actually was, since it is not visible in the diff. Textually it was just two additions landing in the same place (_no_text_diagnostic from #534, _RateLimiter here), and keeping both resolves cleanly. But that resolution quietly disabled this branch: #534 added an except InferenceRuntimeError: raise pass-through to the parallel result loop, and it runs before the draining logic here. Once _process_single_prompt wraps a failure, the first failing chunk short-circuits and the completed chunks are discarded again, which is the behaviour this branch exists to fix.

Failures are now collected uniformly and the pool is allowed to drain. What gets raised then depends on whether anything survived:

  • Nothing completed: the provider's own InferenceRuntimeError is re-raised unchanged, so a blocked prompt or an exhausted token budget still reports its own reason instead of being buried a level down. The drained bookkeeping is attached to it.
  • Some chunks completed: the aggregate error carries partial_results and failed_indices, keeps the first underlying error as original, and quotes it in the message.

That keeps both behaviours: the no-text diagnostic from #534 propagates intact when it is the whole story, and completed work survives when there is some.

Verification on the rebased tree:

python -m pytest tests/gemini_ratelimit_partial_test.py tests/gemini_retry_test.py -m "not live_api"
100 passed

The full suite is differential-clean against main: the only failures are the 3 in tests/create_provider_plugin_test.py, and those reproduce identically on unmodified main in the same environment. isort, pyink and pylint --rcfile=.pylintrc are clean on the changed files.

@github-actions

Copy link
Copy Markdown

⚠️ Branch Update Required

Your branch is 3 commits behind main. Please update your branch to ensure CI checks run with the latest code:

git fetch origin main
git merge origin/main
git push

Note: Enable "Allow edits by maintainers" to allow automatic updates.

…n failure

Addresses the two remaining items of problem google#2 in google#358 (Gemini 429 handling);
the exponential backoff it also asked for already landed in 3aab86c.

max_rpm: a new optional GeminiLanguageModel parameter that spaces real-time
requests client-side so at most N start per minute, shared across workers via a
thread-safe leaky bucket. It prevents 429 RESOURCE_EXHAUSTED on quota-limited
tiers (e.g. the free tier's 15 RPM) rather than only retrying after the fact.
Defaults to 0 (disabled), so existing behavior is unchanged. Not applied to the
Batch API.

Partial results: the parallel infer() path raised on the first failed chunk,
abandoning the still-running futures and discarding every chunk that had already
succeeded. It now drains the pool, then raises once with the completed work
attached to InferenceRuntimeError (partial_results, aligned to the input
prompts, plus failed_indices), so a caller can recover instead of losing a whole
batch. The all-or-nothing raise is preserved, so annotation's zip(batch,
outputs) alignment is unaffected.

Adds tests for the limiter's spacing, max_rpm wiring/validation, and partial
preservation across the success, single-failure and multi-failure cases.
fake_time.sleep.side_effect = lambda s: sleeps.append(s) -> sleeps.append.
No behavior change; CI's lint-tests job runs a stricter pylintrc than the
root one I checked before the first push.
…ogle#534

Rebasing onto main surfaced a behavioural conflict that merged cleanly as
text. google#534 added an `except InferenceRuntimeError: raise` pass-through to the
parallel result loop, which returns the provider's own diagnostic unwrapped.
That pass-through runs before the draining logic here, so once
_process_single_prompt started wrapping failures as InferenceRuntimeError,
every parallel failure short-circuited and the completed chunks were discarded
again, which is the exact behaviour this branch set out to fix.

Failures are now collected uniformly and the pool is allowed to drain. Which
error is raised then depends on whether there is anything to preserve:

- Nothing completed: re-raise the provider's own InferenceRuntimeError
  unchanged, so a blocked prompt or an exhausted token budget still reports its
  own reason rather than being buried a level down, with the drained
  bookkeeping attached to it.
- Some chunks completed: raise the aggregate error carrying partial_results and
  failed_indices, with the first underlying error kept as `original` and quoted
  in the message.

Both test suites pass together: the no-text tests added by google#534 and the
partial-result tests on this branch.
@ojassharma7
ojassharma7 force-pushed the feat/gemini-throttle-and-partial-results branch from 7e535ee to 50c7e76 Compare September 16, 2026 06:32
@aksg87

aksg87 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks for putting this together. Gemini already has configurable retries with backoff for transient API errors, and custom providers can handle more specialized retry and rate-limiting policies.

I’m closing this PR for now and holding off on adding rate-limiting and partial-result recovery APIs. If a specific use case shows where the existing options fall short, feel free to open a focused issue. Happy to revisit and reopen this if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Pull request with 150-600 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants