Repository navigation
test(e2e): cover router strategy selection and context-window fallback - #34840
yassin-berriai wants to merge 1 commit into
Conversation
Greptile SummaryAdds live end-to-end coverage for four router selection strategies and context-window fallback rerouting.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| tests/e2e/router/test_reliability_context_window_fallback_e2e.py | Adds an end-to-end test proving that a genuine context-window rejection reroutes to a larger fallback model. |
| tests/e2e/router/test_reliability_routing_e2e.py | Adds end-to-end coverage for cost-based, usage-based, weighted shuffle, and latency-based routing; the previously reported least-busy readiness race is no longer present. |
Reviews (3): Last reviewed commit: "test(e2e): cover router strategy selecti..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
00d4010 to
68da7af
Compare
|
@greptileai please re-review the current head 68da7af. The findings from your previous review have been addressed in an amended commit. |
|
|
Adds live coverage for four routing-strategy registry cells (cost, tpm, simple-shuffle weight, latency) and for context-window fallback rerouting. Each routing test builds a model group whose deployments differ in only the dimension the strategy under test reads, drives the strategy per request through router_settings_override, and asserts which deployment served the call from the x-litellm-model-id header. The fallback test overflows a real gpt-4 deployment's context window and asserts the same prompt is served by the gpt-5.5 fallback least_busy.picks_lowest_traffic is deliberately left uncovered. Proving it needs the router's in-flight tally for a deployment to be observable at the moment of the pick, and the proxy exposes no such read: the tally is absent from the in-memory cache dump, prometheus is off, and spend rows only land once a call finishes. Every barrier available instead observes an earlier layer, which would make the assertion a probability bet rather than a proof
68da7af to
53a2138
Compare
|
@greptileai please re-review the current head 53a2138. The least-busy cell has been removed rather than covered with a barrier that cannot observe deployment-level in-flight accounting; the remaining five cells are unaffected. |
|
Closing this since #39857 merged with live routing-strategy and context-window fallback cells, so this one is superseded. Thanks for the groundwork |
TLDR
Problem this solves:
How it solves it:
Relevant issues
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
Captured at commit
53a2138d0523a9060e8a2b71d619c70fabdc66e5, against a live proxy on port 4057 backed by postgres and redis withstore_model_in_dbon, driving realopenai/gpt-5.5andopenai/gpt-4traffic that costs real money (roughly five cents a run, almost all of it the ten thousand token prompt the context-window test sends)Every assertion these tests make was also checked in reverse. For each test the deciding setup was flipped, the flip was verified to have actually reached the file, and the test was confirmed to go red before the file was restored from a saved copy and byte-compared. Swapping the two price constants made cost-based routing serve the expensive deployment and failed on the served model id; moving the
tpm=1ceiling onto the other deployment failed the same way; moving the shuffle weight onto the unreachable deployment failed immediately with a 500 connection error from the dead base, which is the outcome the weighted pick exists to prevent; widening the seeding deadline from one millisecond to sixty seconds meant no latency penalty was ever recorded and failed on the 408 seeding assertion; and pointing the context-window primary atopenai/gpt-5.5failed on the rejection assertion because a million token window cannot be overflowed by the promptType
✅ Test
Changes
Two new files under
tests/e2e/router/, no shared harness module touched.test_reliability_routing_e2e.pycovers four of the router's routing strategies, andtest_reliability_context_window_fallback_e2e.pycovers context-window fallback rerouting. Together they add five rows totests/e2e/coverage_registry/reliability.yaml, taking Reliability and Performance from 8 of 26 to 13 of 26Each routing test builds its own model group of real
openai/gpt-5.5deployments that differ in exactly one dimension the strategy under test reads, so only one deployment can win and the pick is a statement about the strategy rather than about luck. The strategy is chosen per request throughrouter_settings_override.routing_strategyon the/chat/completionsbody, so one long-lived proxy serves every strategy with no config change and no restart. The deployment that actually served each call is read back from thex-litellm-model-idresponse header and compared against themodel_idthat/model/newreturned, so a 200 on its own never passes anythingHow each test makes the correct choice unambiguous:
1e-3against1e-9per input and output token, and the cheapmodel_idhas to servemodel_id; picking the other one fails the connection rather than merely picking differentlyThe context-window test registers a real
openai/gpt-4deployment, chosen because 8k is the smallest context window OpenAI still serves and therefore the only one a prompt can overflow for a few cents. The same oversized prompt is sent twice. The first call carries no fallback and has to come back as a real context-length rejection from OpenAI, which is what proves the prompt genuinely overflows the primary instead of the deployment being broken some other way. The second namesgpt-5.5as the context-window fallback and has to come back 200, served by a differentmodel_idthan the primary, withx-litellm-attempted-fallbacksat one or moreEvery prompt carries a unique marker because the proxy under test has its response cache on; a repeated prompt would be answered from cache before the router ever routed it
Three of the eight cells this work was scoped to are deliberately not covered here, so their absence does not read as an oversight.
reliability.routing.least_busy.picks_lowest_trafficwould have to prove that the router had counted an in-flight request against a specific deployment at the moment of the pick, and nothing the proxy exposes says that: the least-busy tally never appears in the router's in-memory cache dump,/metricsis not served, and a spend row only lands once a call finishes, which is exactly when the request stops being in flight. Barriers built on what is observable, a fixed wait or a virtual-key concurrency cap, both sit at an earlier layer than routing, so the pick could still be made while both deployments looked idle; that is a probability bet dressed as a barrier and it does not belong in the suite.reliability.fallback.content_policy.routes_to_fallbackneeds aContentPolicyViolationError, which litellm raises only for an OpenAI-shapedinvalid_request_errorcarryingcontent_policy_violation, an "Invalid prompt ... violating our usage policy" message, or "request was rejected as a result of the safety system"; OpenAI chat completions returns none of those, it answers 200 with a refusal, socontent_policy_fallbacksnever fires. That error surface belongs to the Azure content filter, Bedrock guardrails, and the image endpoints, and this environment has credentials for none of them.reliability.cache.prompt_caching_model_select.returns_cachedneeds the router constructed withoptional_pre_call_checks: ["prompt_caching"]; the default list is empty andRouter.update_settingsdoes not accept that key, so covering it requires arouter_settingsaddition to the e2e gateway config and a proxy restart rather than a test-only changeOn timing: none of these tests waits on the clock for a state it then asserts against. The latency test's one millisecond deadline cannot be met by any real provider round trip, and every other test is a sequence of completed calls whose outcome is read from response headers. The one behaviour that would need a wall-clock or concurrency setup, least-busy, is left uncovered for the reason given above rather than shipped as a timing bet
QA runbook
Prerequisites for every item below: a live proxy on
http://localhost:4000withstore_model_in_dbon andOPENAI_API_KEYin its environment, the master keysk-1234, andcurl -iso thex-litellm-*response headers are visible. Every/model/newcall returns amodel_id; note it, because each check compares it against thex-litellm-model-idof the served response. Delete the deployments withPOST /model/deletewhen donetests/e2e/router/test_reliability_routing_e2e.py::TestReliabilityRoutingStrategies::test_cost_based_picks_lowest_cost - cost-based routing spends the request on the cheaper of two identically capable deployments
curl -X POST http://localhost:4000/model/new -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"model_name":"routing-cost-demo","litellm_params":{"model":"openai/gpt-5.5","api_key":"os.environ/OPENAI_API_KEY","input_cost_per_token":0.001,"output_cost_per_token":0.001},"model_info":{}}'model_namewithinput_cost_per_tokenandoutput_cost_per_tokenof1e-9curl -i -X POST http://localhost:4000/chat/completions -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"model":"routing-cost-demo","messages":[{"role":"user","content":"answer in one word: what colour is a clear midday sky?"}],"max_tokens":256,"router_settings_override":{"routing_strategy":"cost-based-routing"}}'x-litellm-model-idequal to the cheap deployment'smodel_idtests/e2e/router/test_reliability_routing_e2e.py::TestReliabilityRoutingStrategies::test_usage_based_picks_deployment_under_tpm - usage-based routing skips a deployment whose tpm ceiling cannot fit the request
/model/newbody as above undermodel_namerouting-tpm-demo, with"tpm": 1inlitellm_paramsand no cost overridestpmat all"router_settings_override":{"routing_strategy":"usage-based-routing-v2"}x-litellm-model-idequal to the uncapped deployment'smodel_id, since a single token of budget fits no real prompttests/e2e/router/test_reliability_routing_e2e.py::TestReliabilityRoutingStrategies::test_simple_shuffle_picks_healthy_deployment - simple shuffle's weighted pick keeps every call off a zero-weight deployment that cannot answer
model_namerouting-shuffle-demowith"weight": 0and"api_base": "http://127.0.0.1:9/v1""weight": 1and noapi_base"router_settings_override":{"routing_strategy":"simple-shuffle"}x-litellm-model-idequal to the healthy deployment'smodel_id; a pick of the zero-weight deployment surfaces as a 500 naming a connection errortests/e2e/router/test_reliability_routing_e2e.py::TestReliabilityRoutingStrategies::test_latency_based_picks_lowest_latency - latency-based routing avoids the deployment it measured as slow, and does so on latency rather than on cooldown
model_namerouting-latency-demowith"weight": 1"router_settings_override":{"routing_strategy":"latency-based-routing","timeout":0.001}and expect a 408 naming the timeout; this is the observation that gives that deployment the router's worst recorded latency"weight": 0"router_settings_override":{"routing_strategy":"latency-based-routing"}and expect the second deployment'smodel_id, since a deployment with no history reads as the lowest known latency"router_settings_override":{"routing_strategy":"simple-shuffle"}and expect 200 from the first deployment, which proves it is still healthy and selectable rather than in cooldownmodel_idagain, now that both have historytests/e2e/router/test_reliability_context_window_fallback_e2e.py::TestReliabilityContextWindowFallback::test_context_window_overflow_routes_to_fallback - a prompt that overflows a deployment's context window is rerouted to the configured context-window fallback and answered
curl -X POST http://localhost:4000/model/new -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{"model_name":"reliability-ctx-demo","litellm_params":{"model":"openai/gpt-4","api_key":"os.environ/OPENAI_API_KEY"},"model_info":{}}'reliability-ctx-demowith"max_tokens":256and no fallback, and expect a 400 whose body names the model's maximum context length"router_settings_override":{"context_window_fallbacks":[{"reliability-ctx-demo":["gpt-5.5"]}]}and expect 200,x-litellm-model-nameofopenai/gpt-5.5, anx-litellm-model-iddifferent from the primary's, andx-litellm-attempted-fallbacksof at least 1Final Attestation