perf(proxy): bound event-loop blocking from oversized requests - #31497
Conversation
|
|
Greptile SummaryThis PR reduces event-loop blocking from oversized proxy requests. The main changes are:
Confidence Score: 4/5The change is narrowly scoped to bounding synchronous request-processing work and includes regression coverage for both modified paths. The implementation preserves existing behavior for bounded request-body repair and context-window filtering while avoiding unnecessary work for oversized or unconstrained inputs. Tests cover the key behavioral branches, with remaining risk mainly around environment-specific proxy configurations and integrations not exercised here. litellm/constants.py, litellm/proxy/common_utils/http_parsing_utils.py, litellm/router.py, tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py, tests/test_litellm/test_router.py
What T-Rex did
Reviews (4): Last reviewed commit: "perf(proxy): bound event-loop blocking f..." | Re-trigger Greptile |
Greptile SummaryThis PR bounds two proxy request-path costs for oversized requests. The main changes are:
Confidence Score: 4/5The changes are narrowly scoped to request parsing and router pre-call checks, with tests covering the intended bounded-work behavior. The implementation adds focused safeguards around oversized malformed bodies and unnecessary token counting while preserving existing routing and error behavior in covered cases. No specific files require follow-up from the review.
What T-Rex did
Reviews (1): Last reviewed commit: "perf(proxy): bound event-loop blocking f..." | Re-trigger Greptile |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Skip token counting in Router._pre_call_checks when no deployment in the group declares max_input_tokens, and skip the full-body surrogate-repair regex in _read_request_body above a configurable size, raising the existing 400 immediately. Resolves LIT-3541
76afc8d to
ee252c5
Compare
|
@greptileai re-trigger: pushed a small follow-up routing the new env read through |
Document the new request-body repair size cap added in BerriAI/litellm#31497. The proxy's JSON repair fallback runs two full-body regex passes that block the event loop on large malformed payloads; this env var bounds the body size LiteLLM will attempt to repair before returning a 400. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AI#31497) Skip token counting in Router._pre_call_checks when no deployment in the group declares max_input_tokens, and skip the full-body surrogate-repair regex in _read_request_body above a configurable size, raising the existing 400 immediately. Resolves LIT-3541
…just static model_info The previous commit's optimization gated get_router_model_info() and the context-window check behind a static check of each deployment's model_info dict for an explicit max_input_tokens key. That's wrong: max_input_tokens is also commonly derived from litellm's model cost map via get_router_model_info() itself (the common case for any model without a manual model_info override), so the static check silently disabled context-window checks for the majority of real deployments. Caught by the pre-existing upstream test test_pre_call_checks_counts_once_and_filters_on_max_input_tokens, which monkeypatches get_router_model_info to return a limit and expects it to be honored. get_router_model_info() now runs unconditionally per deployment again (matching current litellm_internal_staging behavior before this PR's changes) - it's the token counting that stays lazy, which is what upstream's own lazy pre-call-checks commit (BerriAI#31497) already established as safe to skip. _model_group_has_max_input_tokens() (used to gate the Responses API input->messages conversion) is fixed the same way, since it had the identical flaw. Also updates 3 of this PR's own new tests that had baked in the same wrong assumption (asserting get_router_model_info is skippable).
…just static model_info The previous commit's optimization gated get_router_model_info() and the context-window check behind a static check of each deployment's model_info dict for an explicit max_input_tokens key. That's wrong: max_input_tokens is also commonly derived from litellm's model cost map via get_router_model_info() itself (the common case for any model without a manual model_info override), so the static check silently disabled context-window checks for the majority of real deployments. Caught by the pre-existing upstream test test_pre_call_checks_counts_once_and_filters_on_max_input_tokens, which monkeypatches get_router_model_info to return a limit and expects it to be honored. get_router_model_info() now runs unconditionally per deployment again (matching current litellm_internal_staging behavior before this PR's changes) - it's the token counting that stays lazy, which is what upstream's own lazy pre-call-checks commit (BerriAI#31497) already established as safe to skip. _model_group_has_max_input_tokens() (used to gate the Responses API input->messages conversion) is fixed the same way, since it had the identical flaw. Also updates 3 of this PR's own new tests that had baked in the same wrong assumption (asserting get_router_model_info is skippable).
…just static model_info The previous commit's optimization gated get_router_model_info() and the context-window check behind a static check of each deployment's model_info dict for an explicit max_input_tokens key. That's wrong: max_input_tokens is also commonly derived from litellm's model cost map via get_router_model_info() itself (the common case for any model without a manual model_info override), so the static check silently disabled context-window checks for the majority of real deployments. Caught by the pre-existing upstream test test_pre_call_checks_counts_once_and_filters_on_max_input_tokens, which monkeypatches get_router_model_info to return a limit and expects it to be honored. get_router_model_info() now runs unconditionally per deployment again (matching current litellm_internal_staging behavior before this PR's changes) - it's the token counting that stays lazy, which is what upstream's own lazy pre-call-checks commit (BerriAI#31497) already established as safe to skip. _model_group_has_max_input_tokens() (used to gate the Responses API input->messages conversion) is fixed the same way, since it had the identical flaw. Also updates 3 of this PR's own new tests that had baked in the same wrong assumption (asserting get_router_model_info is skippable).
…just static model_info The previous commit's optimization gated get_router_model_info() and the context-window check behind a static check of each deployment's model_info dict for an explicit max_input_tokens key. That's wrong: max_input_tokens is also commonly derived from litellm's model cost map via get_router_model_info() itself (the common case for any model without a manual model_info override), so the static check silently disabled context-window checks for the majority of real deployments. Caught by the pre-existing upstream test test_pre_call_checks_counts_once_and_filters_on_max_input_tokens, which monkeypatches get_router_model_info to return a limit and expects it to be honored. get_router_model_info() now runs unconditionally per deployment again (matching current litellm_internal_staging behavior before this PR's changes) - it's the token counting that stays lazy, which is what upstream's own lazy pre-call-checks commit (BerriAI#31497) already established as safe to skip. _model_group_has_max_input_tokens() (used to gate the Responses API input->messages conversion) is fixed the same way, since it had the identical flaw. Also updates 3 of this PR's own new tests that had baked in the same wrong assumption (asserting get_router_model_info is skippable).
Relevant issues
Resolves LIT-3541
Linear ticket
LIT-3541
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Single-worker proxy pods stall their only event loop on oversized requests, which resets co-tenant requests into edge 502s. Two on-loop costs are removed here. Before/after captured against a live proxy and against the exact production functions, with the fix toggled via
git stashLive proxy config (
router_settings.enable_pre_call_checks: true,max_request_size_mbunset, modelsmall-model -> openai/gpt-4o-mini)Token counting short-circuit (
Router._pre_call_checks)Driving the real
Router._pre_call_checkswith a ~1.5M-token prompt and a deployment that resolves to nomax_input_tokens(the affected models), while a co-tenant probe coroutine samples event-loop lag:The count is now skipped entirely when no deployment in the group declares a context-window limit, so the co-tenant probe stays responsive
End to end, a real completion still routes with
enable_pre_call_checks=trueSurrogate-repair bound (
_read_request_body)A 50MB malformed JSON body posted to
/chat/completions. The body is unparseable either way, so the 400 is identical; only the on-loop repair work differsType
🚄 Infrastructure
Changes
Two synchronous costs run on the proxy's event loop per request and dominate the stall on oversized payloads. Both are now bounded
litellm.token_counterinRouter._pre_call_checksruns tiktoken on the full prompt while routing every completion whenenable_pre_call_checks=true. Its result is only ever compared against a deployment'smax_input_tokens, so for model groups where no deployment declares that limit (the affected models) the count is pure waste. The count is now computed lazily inside the deployment loop, at most once, and only when a deployment actually declares an integermax_input_tokens; otherwise it is skipped. Token-counting failure still returns the unfiltered deployment list, and context-window filtering is unchanged when a limit is setThe surrogate-repair fallback in
_read_request_bodydecodes the whole body and runs two full-bodyre.subpasses on anorjson.JSONDecodeErrorbefore retryingjson.loads. On a multi-MB malformed body that blocks the loop for hundreds of ms. AboveMAX_REQUEST_BODY_SIZE_TO_REPAIR_MB(new constant inlitellm/constants.py, default 1MB, env-overridable, set to 0 to disable the cap) the repair is skipped and the existing 400 is raised immediately. Bodies at or below the limit are still repaired exactly as beforeRegression tests cover both paths: the token count is asserted not to run when no deployment declares
max_input_tokens, to run at most once and filter when one does, and the surrogate repair is asserted skipped above the size cap and preserved below itDocs
Documentation for the new
MAX_REQUEST_BODY_SIZE_TO_REPAIR_MBenv var: BerriAI/litellm-docs#424