test(e2e): pin the openai websocket passthrough prefixes - #37903
Conversation
The websocket routes under /openai_passthrough and /openai had no e2e coverage, so nothing catches the regression from issue #36088, where both prefixes carried HTTP routes only and refused every upgrade with a 403 before a socket ever existed. Two tests cover it. The realtime one opens /openai_passthrough/v1/realtime and asserts OpenAI's own session.created frame comes back, which proves the route is registered and relayed upstream. The responses one asserts /openai/v1/responses accepts the upgrade, since a responses.connect socket waits for the client to speak first and has no opening frame to check. A refused upgrade is an HTTP response rather than a close frame, so both assert on the handshake. ws_base_url moves into e2e_config now that a second suite needs it
Greptile SummaryThe PR adds end-to-end coverage for websocket upgrades through both OpenAI passthrough prefixes and centralizes websocket URL construction.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| tests/e2e/llm_translation/passthrough_client.py | Adds websocket connection support that records rejected upgrades and optional opening event types. |
| tests/e2e/llm_translation/test_passthrough_e2e.py | Adds regression tests for realtime relay and Responses websocket upgrade acceptance on the OpenAI passthrough prefixes. |
| tests/e2e/e2e_config.py | Centralizes conversion of the configured proxy URL to its websocket equivalent. |
| tests/e2e/llm_translation/realtime/realtime_client.py | Reuses the centralized websocket base URL helper without changing URL semantics. |
| tests/e2e/coverage_registry/llm_conversational.yaml | Registers coverage for the OpenAI Responses websocket passthrough path. |
| tests/e2e/coverage_registry/llm_nonconversational.yaml | Registers coverage for the OpenAI realtime websocket passthrough path. |
Reviews (2): Last reviewed commit: "test(e2e): drop the REALTIME_MODEL comme..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Mateo seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9b27d7a. Configure here.
TLDR
Problem this solves:
responses.connectclients would break unnoticedHow it solves it:
session.createdframe backUser Flow
Before: a realtime or
client.responses.connect()app cannot open a socket through the gateway at all, so that traffic has to route around itPOST https://litellm-domain/key/generatewith{"models": []}, and gets back ansk-...keyPOST https://litellm-domain/openai_passthrough/v1/responseswithAuthorization: Bearer sk-...and{"model": "gpt-5.5", "input": "hi"}, and get a 200 carrying aresp_...idGET wss://litellm-domain/openai_passthrough/v1/realtime?model=gpt-realtime-2withAuthorization: Bearer sk-..., and get backHTTP/1.1 403 Forbidden. No socket opens and the response carries no messageclient.responses.connect()against the other prefix: websocket upgradeGET wss://litellm-domain/openai/v1/responses, and get the same bareHTTP/1.1 403 Forbiddenwss://api.openai.com/v1/realtimewith their own OpenAI key. Those sessions never appear inGET https://litellm-domain/spend/logs, and the key's budget and rate limits never apply to themAny other user hits the identical 403 no matter what their key is allowed to reach, because the refusal never looks at the key
After: the same app opens the socket and talks to the model through the gateway
POST https://litellm-domain/key/generatewith{"models": []}, and gets back ansk-...keyPOST https://litellm-domain/openai_passthrough/v1/responseswithAuthorization: Bearer sk-...and{"model": "gpt-5.5", "input": "hi"}, and get a 200 carrying aresp_...idGET wss://litellm-domain/openai_passthrough/v1/realtime?model=gpt-realtime-2withAuthorization: Bearer sk-..., and getHTTP/1.1 101 Switching Protocols. The first frame they read is OpenAI's own{"type": "session.created", ...}client.responses.connect()againstwss://litellm-domain/openai/v1/responsesalso returnsHTTP/1.1 101 Switching Protocols, and the socket stays open waiting for the app's first eventAnother user whose key is scoped to a subset of models is still refused the upgrade with a 403, so they cannot reach a model outside their allow-list over the socket either
Relevant issues
Pins the fix for #36088
Linear ticket
Part of LIT-5902. This covers the PT-03 row; the PT-10 row stays open on that ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
The fix being pinned (#36151) is already on the merge base, so a Before captured there would be green and would prove nothing. Before is therefore the PR tip with that fix's two route registrations removed, which is exactly what the file looked like before #36151 landed. That is the only difference between the two sides
Shared setup: a proxy from this branch on
http://localhost:43127against a local Postgres,gpt-5.5andgemini-2.5-flashregistered from config, keys minted withPOST /key/generate. The realtime traffic hits real OpenAIBefore (9b27d7a, with the websocket routes unregistered)
@router.websocketlines atlitellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py:2152-2153and rebooted the proxyuv run pytest llm_translation/test_passthrough_e2e.py -k TestOpenAIPassthroughWebsocket->2 failed, 12 deselected in 0.73s, withAssertionError: /openai/v1/responses refused the websocket upgrade with HTTP 403; the prefix relays this route over HTTP but drops a responses.connect client before the socket opensws://localhost:43127/openai_passthrough/v1/realtime?model=gpt-realtime-2with an all-models key raisesInvalidStatus: server rejected WebSocket connection: HTTP 403After (9b27d7a, the PR tip)
2 passed, 12 deselected in 2.73smake check->PASS, which runsbasedpyright tests/e2e->0 errors, 0 warnings, 0 notesand the raw HTTP client banuv run pytest llm_translation/realtime/ --collect-only -q->9 tests collected, so movingws_base_urldid not break the suite it came fromuv run pytest coverage_registry/test_collector.py -q->13 passed, anduv run python -m coverage_registry.collectorprints no "marker(s) point at ids not in the registry" line, so both newcoversids resolve to the rows added hereType
✅ Test
Caveats (if any)
/openai/v1/realtimeis not testable, the app-level route claims itgpt-realtime-2is a literal in the test modulebuildkite/e2e-testsis red on three pre-existing failures outside this diffOpenAI speaks first on realtime, which is why that test can assert a
session.createdframe and prove the socket was really relayed upstream. Aresponses.connectsocket waits for the client, so there is no opening frame to read and the accepted handshake is the whole signal. It still catches the regression exactly, since that route answered 403 before the fix, but it does not prove the relay the way the realtime one doesThe second prefix is exercised through
/openai/v1/responsesrather than/openai/v1/realtime, which issue #36088 also lists. The app-level realtime endpoint inproxy_server.pyis registered before the passthrough router is included, so it wins that path and the passthrough route never sees it. Testing/openai/v1/realtimewould assert the app-level endpoint instead, which is a different behaviorREALTIME_MODELsits in the test module as a literal, the same wayEMBEDDING_MODELalready does. The passthrough route relays the model name to OpenAI verbatim, so it has to be OpenAI's own name rather than a gateway alias, but an environment that needs a different realtime model has no way to say soLIT-5902 states the signal is a handshake accepted and then closed with 1008 or 1011. That is not what happens. The websocket auth dependency and the model-restriction check both close before
accept(), and Starlette turns a pre-accept close into an HTTP 403 handshake rejection, which is indistinguishable from the unregistered route's 403. The After probe above shows it directly: the restricted key gets the same 403 as the nonexistent prefix. The usable signal is an all-models key getting an accepted handshake, which is what these tests assertbuildkite/e2e-testsis red here, and none of it comes from this change. Editinge2e_config.pymakes the gate select the entire e2e suite instead of a subset, so this is one of the few builds that runs these three tests at all, which is why every neighbouring build is green. Both new tests passed in that same run. The failures aretest_unflagged_model_converts_system_reminder_and_succeedsinTestAzureFoundryMidConversationSystem,TestVertexMidConversationSystem, andTestBedrockInvokeMidConversationSystem, each of which has now failed 16 of 16 runs over the past seven days for a reliability of 0. All three wait up to 60 seconds for a provider prompt cache to become fully readable, and neither file they live in appears in this diff. They are already tracked as LIT-5921. The check is not required onlitellm_internal_stagingand every required check is greenQA runbook
tests/e2e/llm_translation/test_passthrough_e2e.py::TestOpenAIPassthroughWebsocket::test_realtime_upgrade_reaches_openai_through_the_passthrough_prefix- a realtime client opens a socket on the dedicated prefix and reads OpenAI's opening session eventcurl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-1234" -d '{"models": []}'(needsOPENAI_API_KEYon the proxy)ws://localhost:4000/openai_passthrough/v1/realtime?model=gpt-realtime-2withAuthorization: Bearer <key>101 Switching Protocolsand a first frame whosetypeissession.created403handshake rejection instead, which is the shape the unregistered route also returnstests/e2e/llm_translation/test_passthrough_e2e.py::TestOpenAIPassthroughWebsocket::test_responses_upgrade_is_accepted_on_the_openai_prefix- aresponses.connectclient is allowed to open a socket on the/openaiprefixws://localhost:4000/openai/v1/responses101 Switching Protocolsand the socket to stay open with no frame arriving, since that route waits for the client to send first@router.websocketlines inlitellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py, reboot, and expect both upgrades to return403Final Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
9b27d7a passes /live-pr-risk