fix(proxy): register WebSocket passthrough for OpenAI prefixes - #36151
Conversation
create_websocket_passthrough_route existed but /openai and /openai_passthrough only registered HTTP methods, so WS upgrades were rejected at routing. Add catch-all websocket routes mirroring the HTTP passthrough target construction. Fixes BerriAI#36088
Greptile SummaryThe PR adds authenticated OpenAI WebSocket passthrough routes and updates the shared forwarder to handle UTF-8 setup frames.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py | Adds authenticated OpenAI WebSocket route registration, upstream URL construction, credential injection, and subprotocol negotiation. |
| litellm/proxy/pass_through_endpoints/pass_through_endpoints.py | Changes initial upstream WebSocket frame handling from ASCII to UTF-8. |
| tests/test_litellm/proxy/test_openai_ws_passthrough_routes.py | Adds focused coverage for route registration, query forwarding, authentication isolation, subprotocol selection, ACL handling, and missing credentials. |
| tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py | Adds regression coverage for forwarding non-ASCII setup frames. |
| ui/litellm-dashboard/src/lib/http/schema.d.ts | Regenerates schema declarations for the two WebSocket endpoints. |
Reviews (6): Last reviewed commit: "fix(proxy): close websocket cleanly when..." | Re-trigger Greptile
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: 3 · PR risk: 0/10 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Forward realtime model query string, keep OPENAI_API_KEY (forward_headers=False), satisfy ruff strict gates, sync dashboard OpenAPI types, and cover the behavior in tests.
|
Addressed the review/CI feedback in the latest commit:
CLA: please re-check if still pending — I may need to re-sign in the browser for this PR. |
- decode upstream first frame as utf-8 instead of ascii - reject model-restricted keys at connect to match HTTP model enforcement - log the actual request path for /openai_passthrough traffic
|
bugbot run |
PR SummaryCursor Bugbot is generating a summary for commit a258b2b. Configure here. |
…itellm_pr36151_ws_passthrough # Conflicts: # litellm/proxy/pass_through_endpoints/pass_through_endpoints.py # tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4ba9d6b. Configure here.
|
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 5965648. Configure here.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the contribution!
|
Thanks for picking up the remaining review items and landing them on this branch. The remaining merge block I can see is the CLA check on my commit. I'll sign that so it doesn't hold the rest of the work. |
cfb2eba
into
BerriAI:litellm_internal_staging

TLDR
Problem this solves:
How it solves it:
User Flow
Before: you cannot open any OpenAI WebSocket session through the proxy's OpenAI passthrough prefixes
http://<proxy>/openai/v1orhttp://<proxy>/openai_passthrough/v1client.realtime.connect(model="gpt-realtime-2.1-mini")against/openai_passthrough/v1/realtimeand the upgrade is rejected with HTTP 403client.responses.connect()against/openai/v1/responsesor/openai_passthrough/v1/responsesand that upgrade is also rejected with HTTP 403After: the same SDK calls open live WebSocket sessions through both prefixes
client.realtime.connect(model="gpt-realtime-2.1-mini")upgrades,session.createdarrives, and your realtime conversation streams back with token usageclient.responses.connect()upgrades on either prefix andresponse.createreturns real model output over the socketSec-WebSocket-Protocolvalues (openai-insecure-api-key.<key>,openai-beta.realtime-v1) gets its first offered subprotocol echoed back in the handshake, so spec-compliant browsers keep the connection open instead of aborting it/openai/v1/realtimeroute keeps serving that key's allowed modelsRelevant issues
Fixes #36088
Linear ticket
Resolves LIT-5396
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 booted from the repo with a one-model config (
gpt-realtime-2.1-mini+ master key), real OpenAI API calls costing real $. Clients are the plain OpenAI Python SDK (2.33.0) pointed at the proxy, plus a rawwebsocketsclient that mimics a browser's subprotocol handshakerealtime_qa.py (OpenAI SDK realtime client used below)
responses_qa.py (OpenAI SDK responses.connect client used below)
subproto_qa.py (browser-style client: auth via Sec-WebSocket-Protocol, no Authorization header)
Before, at merge base 4e5495e (proxy on port 28517)
Control on the same proxy: the native realtime bridge works, so only the passthrough surfaces were broken
After, at 5965648 (proxy on port 29226)
Subprotocol negotiation for browser clients
At a258b2b (before this fix) the route accepted the socket without selecting any subprotocol even when the client offered some; RFC 6455 requires such clients (all browsers) to fail the connection. After the fix the first offered subprotocol is echoed, mirroring the native
/openai/v1/realtimeroute, and the whole session runs authenticated purely by theopenai-insecure-api-key.subprotocolModel ACL enforcement, at 5965648 (same proxy, DB-backed)
A key restricted to one model is refused at the handshake on both WebSocket surfaces, in both auth styles, and still works on the native realtime route with its allowed model
Missing provider credential, at 5965648 (proxy on port 22245, no OPENAI_API_KEY configured)
The handshake is refused cleanly with no server-side traceback, instead of the previous close-then-raise that left an ASGI ValueError in the logs
Caveats:
Type
🐛 Bug Fix
Changes
Registers a catch-all
@router.websocketroute for the/openaiand/openai_passthroughprefixes inllm_passthrough_endpoints.py. The handler authenticates throughuser_api_key_auth_websocket, injects the configured OpenAI key upstream, preserves the client's query string, and hands off to the sharedwebsocket_passthrough_requestforwarderThe shared forwarder previously decoded the upstream's first frame as ascii, which crashed on any non-ascii byte in OpenAI's
session.createdpayload; it now decodes utf-8. Keys restricted to specific models are refused at connect with close code 1008 so these routes cannot be used to sidestep model ACLs, matching the enforcement the HTTP passthrough applies to request bodies. The logged endpoint now reflects the actual request path instead of hardcoding/openai/The route accepts the socket itself, echoing the client's first offered
Sec-WebSocket-Protocolexactly like the native/openai/v1/realtimeroute, so browser clients that carry auth in subprotocols survive the handshake. The URL join helper moved from a private static method to a module-level_join_url_pathsso the route does not reach into another class's private API. When no OpenAI credential is configured the route closes with 1011 and returns instead of raising after the close, matching the Vertex live pathUnit tests cover route registration, query and auth forwarding, subprotocol selection, the restricted-key refusal, the unrestricted-key allow list, the credential-missing clean close, and a regression test that fails on the old ascii decode
Final Attestation