perf(router): shallow-copy wildcard deployments instead of deepcopy in pattern router#33809
Conversation
…n pattern router Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis PR replaces
Confidence Score: 4/5Safe to merge; the optimization is correct for the stated invariant that only litellm_params["model"] is ever written on the returned deployment. The shallow copy correctly isolates the one field that is actually mutated and the two new tests demonstrate this end-to-end. The remaining concern is that other nested objects inside litellm_params (metadata, credential fields) are now shared by reference with the stored template, so any future or undiscovered caller that mutates those objects in-place would silently corrupt the stored wildcard pattern for all subsequent requests. litellm/router_utils/pattern_match_deployments.py — the shallow-copy contract (only litellm_params["model"] is ever written by callers) should be documented with an inline comment to prevent future regressions.
|
| Filename | Overview |
|---|---|
| litellm/router_utils/pattern_match_deployments.py | Replaces copy.deepcopy with a two-level shallow dict spread in _return_pattern_matched_deployments, correctly isolating the litellm_params["model"] write while sharing all other nested references; the unused import copy is also removed. |
| tests/local_testing/test_router_pattern_matching.py | Adds two focused regression tests: one verifying that sequential route() calls don't mutate the stored template, and one asserting that the returned litellm_params is a distinct dict while shared nested objects (metadata) remain the same object by reference. |
Reviews (1): Last reviewed commit: "perf(router): shallow-copy wildcard depl..." | Re-trigger Greptile
| def _return_pattern_matched_deployments(self, matched_pattern: Match, deployments: List[Dict]) -> List[Dict]: | ||
| new_deployments = [] | ||
| for deployment in deployments: | ||
| new_deployment = copy.deepcopy(deployment) | ||
| new_deployment["litellm_params"]["model"] = PatternMatchRouter.set_deployment_model_name( | ||
| matched_pattern=matched_pattern, | ||
| litellm_deployment_litellm_model=deployment["litellm_params"]["model"], | ||
| ) | ||
| new_deployments.append(new_deployment) | ||
|
|
||
| return new_deployments | ||
| return [ | ||
| { | ||
| **deployment, | ||
| "litellm_params": { | ||
| **deployment["litellm_params"], | ||
| "model": PatternMatchRouter.set_deployment_model_name( | ||
| matched_pattern=matched_pattern, | ||
| litellm_deployment_litellm_model=deployment["litellm_params"]["model"], | ||
| ), | ||
| }, | ||
| } | ||
| for deployment in deployments | ||
| ] |
There was a problem hiding this comment.
Shared nested objects silently mutable by callers
The shallow copy isolates the top-level dict and litellm_params, but every other nested value inside litellm_params (e.g. metadata, list-typed fields, credential dicts) is still shared by reference with the stored template. Any caller that does deployment["litellm_params"]["metadata"]["key"] = value or appends to a list field will silently corrupt the stored pattern template for all future requests on that wildcard. The tests verify route() itself is safe, but they cannot prevent downstream consumers from mutating these shared objects. Worth documenting explicitly with an inline comment (matching the one in router.py) to guard against future regressions.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Relevant issues
Follow-up to #33721, which patched only the
GET /v1/modelscaller. This addresses the shared root causeLinear 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
line_profileronPatternMatchRouter._return_pattern_matched_deployments, simulating a/v1/modelslisting that routes 500 wildcard-expanded model names through a router with two wildcard patterns (openai/*,bedrock/*). Same script for both runs; the only change is this diffBefore, at commit 07e07e6 (deepcopy)
After, at commit a0efa6f (shallow copy)
Function-level time drops from 88.3 ms to 3.9 ms for the 500-model listing (about 22x). The 170.7 us/call
copy.deepcopyon line 103 disappears; what remains is dominated byset_deployment_model_name, the actual wildcard substitution. Measured end to end, the same 500-listing loop throughpattern_router.routegoes from ~203.6 us to ~40.9 us per matched model (about 5x), sinceroutealso doessorted_patternsand a regex match per callType
🚄 Infrastructure
Changes
PatternMatchRouter._return_pattern_matched_deploymentsdeep-copied every deployment it returned so it could rewritelitellm_params["model"]with the wildcard substitution without corrupting the stored pattern template.deepcopywas overkill: the only field written islitellm_params["model"], so a shallow copy of the deployment dict plus a shallow copy of itslitellm_paramsis enough to isolate the write while leaving every other nested value shared by referenceThis is the same shallow-copy-with-nested-litellm_params pattern already used for
default_deploymentinrouter.py(the one whose comment reads "Shallow copy with nested litellm_params copy (100x+ faster than deepcopy)")routeis the hot path for every wildcard request, not just/v1/models. It backs_common_checks_available_deployment->_try_early_resolve_deployments_for_model_not_in_names->get_deployments_by_pattern(per-completion routing),get_model_list,get_model_group_info->_set_model_group_info, andget_deployment_credentials_with_provider. #33721 removed only the/v1/modelscaller by addingget_configured_token_limits; the deepcopy still fired on all the others until this changeQA runbook
Not an e2e test change; the added tests live in
tests/local_testing/test_router_pattern_matching.pytest_route_does_not_mutate_stored_template- two sequentialroute()calls for different wildcard requests must each return the correct substitutedlitellm_params.modelwhile the stored template keeps itsopenai/*value. Fails if a naive shallow-outer-only copy is used (it mutates the shared nested dict)test_route_returns_shallow_copy_not_deepcopy- the returned deployment gets a freshlitellm_paramsdict (distinct object from the template) but a nested value that is not rewritten (metadata) stays the same object by reference. Fails if reverted tocopy.deepcopyFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/63644b811622411ea805c21bc4917595