refactor(rate-limits): move the v3 limiter per-request stash off request metadata onto a ContextVar - #35278
Merged
mateo-berri merged 4 commits intoJul 31, 2026
Conversation
…est metadata onto a ContextVar The v3 parallel-request limiter stashed its per-request bookkeeping (TPM reservation, descriptors, parallel slot, rate-limit response snapshot, released flag) in the request body's metadata channels. On routes where metadata is a provider request parameter (Responses API and the other LITELLM_METADATA_ROUTES) that leaked internal keys upstream and produced HTTP 400s, and it required denylist stripping plus dual-channel writes to contain. The stash now lives on an asyncio ContextVar holding a single typed RequestRateLimiterStash per request. The pre-call hook writes it, and the success/failure callbacks, disconnect release, and post-call hooks read and clear the same shared instance, which keeps the refund and slot release idempotent across sibling callbacks. The request body is never touched, so the stash-key stripping, the metadata mirror writes, and the all_litellm_params denylist entries are removed
Contributor
Greptile SummaryThe PR moves v3 rate-limiter request bookkeeping from provider-facing request metadata into a typed per-request ContextVar stash.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains within the scope of the eligible follow-up findings.
|
| Filename | Overview |
|---|---|
| litellm/proxy/hooks/parallel_request_limiter_v3.py | Introduces the typed ContextVar-backed request stash and migrates reservation, parallel-slot, response-header, and cleanup bookkeeping to it. |
| litellm/proxy/hooks/dynamic_rate_limiter_v3.py | Stores dynamic rate-limit responses in the shared request stash instead of mutating request data. |
| litellm/proxy/common_request_processing.py | Updates streaming-disconnect cleanup to release limiter state without passing request data. |
| litellm/proxy/utils.py | Adapts the proxy disconnect-release helper to the ContextVar-based limiter interface. |
| litellm/types/utils.py | Removes obsolete request-parameter denylist entries for the former metadata-backed stash keys. |
| tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py | Reworks limiter lifecycle tests around isolated ContextVar state and adds ownership and request-mutation regressions. |
| tests/test_litellm/proxy/hooks/test_tpm_concurrent.py | Updates concurrent TPM reservation and reconciliation tests to use the ContextVar stash. |
Reviews (3): Last reviewed commit: "test(rate-limits): drop the removed data..." | Re-trigger Greptile
Contributor
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Merged
5 tasks
… so nested calls cannot release it
Contributor
Author
…itellm_v3_limiter_contextvar_stash # Conflicts: # litellm/proxy/hooks/parallel_request_limiter_v3.py # tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py
…miter raise-branch test
Contributor
Author
tin-berri
approved these changes
Jul 31, 2026
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.
TLDR
Problem this solves:
How it solves it:
Relevant issues
Fixes #35197. Supersedes #35207, which contained the leak by rerouting the body writes into the proxy-internal bucket and has since merged into staging; this PR replaces that containment by removing the body writes entirely
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live proxy against real OpenAI (
openai/gpt-5.6), key generated with{"tpm_limit": 100000, "rpm_limit": 100, "max_parallel_requests": 5}Before, at staging tip 6f1625d: the limiter's stash reaches OpenAI and comes back stored in the response's provider-side
metadataAfter, at this PR's head f507a11: body forwarded untouched
Rate limiting itself is unchanged; the after run reproduces the before run's chat headers byte for byte
Enforcement still fires, on a key with
{"rpm_limit": 2}TPM reservation still reconciles to actual usage through the ContextVar: call 1 holds a 25-token reservation in flight (
remaining-tokens: 99975), settles to the actual 15 once logged, and call 2 in flight shows99960 = 100000 - 15 - 25Type
🐛 Bug Fix
🧹 Refactoring
Changes
The v3 parallel-request limiter kept its per-request bookkeeping (TPM reservation, reserved scopes, parallel slot acquisition, rate-limit response snapshot, refund-released flag) in the request body's
metadata/litellm_metadatachannels and ondatatop level. That state now lives in a single typedRequestRateLimiterStashdataclass on a module-levelContextVar, created by the pre-call hook and read or cleared by the success/failure logging callbacks, the disconnect release, and the post-call hooks. The logging worker captures the request context at enqueue time and every task forked from the request shares the same stash instance, so the refund and slot release stay idempotent across sibling callbacks exactly as beforeBecause the limiter no longer touches the body, the caller-injection stripping (
_strip_stash_keys_from_all_channels), the metadata mirror and lookup helpers, the SLO scrubbing, and theall_litellm_paramsdenylist entries for the stash keys are deleted.dynamic_rate_limiter_v3writes its rate-limit response snapshot to the same stash instead ofdata, andasync_release_max_parallel_requests_on_disconnectno longer needsrequest_data. Client-supplied lookalike keys are now inert data the limiter never reads, rather than something to stripSince the stash is context-inherited, nested LiteLLM calls made inside the request (LLM-judge guardrails, silent experiments) would also see it from their own logging callbacks and could release the owning request's parallel slot or refund its TPM reservation early. The stash therefore records the request's
litellm_call_idat pre-call, and the kwargs-driven callbacks (log success/failure and the header mirror) ignore a stash owned by a different call id. Router retries and fallbacks reuse the request's call id (litellm.utils.function_setuponly mints one when absent), so the shared refund and slot idempotency across attempts is unchanged; nested calls mint fresh ids and are shut out. The guard only rejects a positive mismatch: an unclaimed stash or a callback without a call id behaves exactly as beforeTests updated to seed the ContextVar stash instead of metadata dicts, plus new regressions: the chat body is deep-equal before and after pre-call, Responses bodies with and without caller
metadataare forwarded byte-identical, injected stash lookalikes cannot trigger a refund, a full chat lifecycle proves reservation, refund, slot release, and double-refund idempotency through the stash, nested-call events with a foreign call id leave the owner's slot and reservation untouched while owner events still release them, and one-sided call-id metadata (unclaimed stash or callback without an id) keeps workingFinal Attestation