From f0e9df2934927a4f2ce34496cc545181ec6334b1 Mon Sep 17 00:00:00 2001 From: L4XB Date: Tue, 15 Sep 2026 00:29:03 +0200 Subject: [PATCH 1/6] fix(proxy): let the Responses WebSocket follow the proxy keyless policy user_api_key_auth_websocket rejected a missing key before delegating, so a proxy running without general_settings.master_key accepted every HTTP route and refused the WebSocket with 403 No API key provided. Reading the key is now separate from deciding whether one is required: user_api_key_auth makes that call, allowing a keyless request when no master key is set and raising No api key passed in. when one is. Rejections raise WebSocketException alone. Closing the socket and then raising an HTTPException asked Starlette to start an HTTP response on a closed socket, which surfaced as RuntimeError: Unexpected ASGI message 'websocket.http.response.start' on top of the real auth failure. --- litellm/proxy/auth/user_api_key_auth.py | 62 ++++++---- .../proxy/auth/test_user_api_key_auth.py | 116 ++++++++++++++++++ 2 files changed, 152 insertions(+), 26 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 22826f48b52..478f4a7e07d 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -544,6 +544,36 @@ def _apply_budget_limits_to_end_user_params( verbose_proxy_logger.debug("Applied budget limits to end user %s", end_user_id) +def _get_websocket_api_key(websocket: WebSocket) -> str | None: + """Read the API key a WebSocket client presented, or None when it presented none. + + Whether a key is required is decided by ``user_api_key_auth``, which allows a + keyless request when no master key is configured. + """ + authorization: Final = websocket.headers.get("authorization") + if authorization: + if not authorization.startswith("Bearer "): + raise WebSocketException( + code=status.WS_1008_POLICY_VIOLATION, + reason="Invalid Authorization header format", + ) + return authorization[len("Bearer ") :].strip() + + header_key: Final = websocket.headers.get("api-key") + if header_key: + return header_key + + subprotocol_prefix: Final = "openai-insecure-api-key." + return next( + ( + protocol.strip()[len(subprotocol_prefix) :] + for protocol in websocket.headers.get("sec-websocket-protocol", "").split(",") + if protocol.strip().startswith(subprotocol_prefix) + ), + None, + ) + + async def user_api_key_auth_websocket(websocket: WebSocket): # Accept the WebSocket connection @@ -575,38 +605,18 @@ async def user_api_key_auth_websocket(websocket: WebSocket): request.body = return_body - authorization: Final = websocket.headers.get("authorization") - # If no Authorization header, try the api-key header - if not authorization: - api_key = websocket.headers.get("api-key") - if not api_key: - # Try extracting from WebSocket subprotocol (browser clients) - for protocol in websocket.headers.get("sec-websocket-protocol", "").split(","): - protocol = protocol.strip() - if protocol.startswith("openai-insecure-api-key."): - api_key = protocol[len("openai-insecure-api-key.") :] - break - if not api_key: - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) - raise HTTPException(status_code=403, detail="No API key provided") - else: - # Extract the API key from the Bearer token - if not authorization.startswith("Bearer "): - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) - raise HTTPException(status_code=403, detail="Invalid Authorization header format") + api_key: Final = _get_websocket_api_key(websocket) - api_key = authorization[len("Bearer ") :].strip() - - # Call user_api_key_auth with the extracted API key - # Note: You'll need to modify this to work with WebSocket context if needed try: - return await user_api_key_auth(request=request, api_key=f"Bearer {api_key}") + return await user_api_key_auth( + request=request, + api_key=f"Bearer {api_key}" if api_key else None, # pyright: ignore[reportArgumentType] # None = no key + ) except Exception as e: if is_invalid_virtual_key_error(e): raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) verbose_proxy_logger.exception(e) - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) - raise HTTPException(status_code=403, detail=str(e)) + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION, reason=str(e)) def update_valid_token_with_end_user_params(valid_token: UserAPIKeyAuth, end_user_params: dict) -> UserAPIKeyAuth: diff --git a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py index c9ae105d982..b3a6f1286b9 100644 --- a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py +++ b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py @@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from textwrap import dedent from types import SimpleNamespace +from typing import Final from unittest.mock import ANY, AsyncMock, MagicMock, patch @@ -48,6 +49,7 @@ from litellm.proxy.auth.user_api_key_auth import ( _user_api_key_auth_builder, get_api_key, user_api_key_auth, + user_api_key_auth_websocket, ) from litellm.proxy.spend_tracking.carried_budget_state import carried_budget_metadata @@ -7851,3 +7853,117 @@ async def test_auth_flow_enters_virtual_key_mapping_when_only_an_issuer_configur assert resolve_mock.await_args.kwargs["jwt_claims"][JWTHandler.LITELLM_JWT_ISSUER_CLAIM] == ISSUER_TWO assert result.api_key == "hashed-mapped-key" assert result.team_id == "svc-team" + + +def _websocket_for_auth(headers: dict | None = None) -> MagicMock: + from fastapi import WebSocket + from starlette.datastructures import URL + + websocket: Final = MagicMock(spec=WebSocket) + websocket.query_params = {"model": "test-model"} + websocket.headers = headers or {} + websocket.scope = { + "type": "websocket", + "path": "/v1/responses", + "headers": [(name.lower().encode(), value.encode()) for name, value in (headers or {}).items()], + } + websocket.url = URL(url="/v1/responses") + websocket.close = AsyncMock() + return websocket + + +_KEYLESS_PROXY_STATE: Final = { + "prisma_client": None, + "user_custom_auth": None, + "general_settings": {}, + "llm_model_list": [], + "llm_router": None, + "jwt_handler": None, + "open_telemetry_logger": None, +} + + +@pytest.mark.parametrize( + "headers", + [ + pytest.param({}, id="no headers at all"), + pytest.param({"sec-websocket-protocol": "realtime"}, id="subprotocol carrying no key"), + ], +) +@pytest.mark.asyncio +async def test_websocket_auth_forwards_a_missing_key_as_none(headers): + """A missing key must reach user_api_key_auth as None, the value + APIKeyHeader(auto_error=False) gives the HTTP routes for an absent header. + Rejecting it here meant a proxy with no master key refused the WebSocket + while accepting every HTTP route.""" + websocket: Final = _websocket_for_auth(headers) + + with patch("litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True) as mock_auth: + await user_api_key_auth_websocket(websocket) + + assert mock_auth.call_args.kwargs["api_key"] is None + websocket.close.assert_not_called() + + +@pytest.mark.asyncio +async def test_websocket_auth_without_master_key_returns_an_internal_user(): + """With no master key configured, a keyless connection authenticates.""" + websocket: Final = _websocket_for_auth({}) + + with patch.multiple("litellm.proxy.proxy_server", master_key=None, **_KEYLESS_PROXY_STATE): + result = await user_api_key_auth_websocket(websocket) + + assert isinstance(result, UserAPIKeyAuth) + assert result.user_role == LitellmUserRoles.INTERNAL_USER + + +@pytest.mark.asyncio +async def test_websocket_auth_with_master_key_still_refuses_a_keyless_client(): + """Delegating the decision is only correct if the delegate still says no.""" + from starlette.exceptions import WebSocketException + + websocket: Final = _websocket_for_auth({}) + + with patch.multiple("litellm.proxy.proxy_server", master_key="sk-master-key", **_KEYLESS_PROXY_STATE): + with pytest.raises(WebSocketException): + await user_api_key_auth_websocket(websocket) + + +@pytest.mark.asyncio +async def test_websocket_auth_rejects_a_malformed_header_without_closing_first(): + """Closing the socket and then raising an HTTPException makes Starlette + start an HTTP response on a closed socket, which surfaces as a RuntimeError + on top of the real auth failure.""" + from starlette.exceptions import WebSocketException + + websocket: Final = _websocket_for_auth({"authorization": "Token sk-1234"}) + + with pytest.raises(WebSocketException) as exc_info: + await user_api_key_auth_websocket(websocket) + + assert exc_info.value.code == status.WS_1008_POLICY_VIOLATION + websocket.close.assert_not_called() + + +@pytest.mark.parametrize( + "headers,expected", + [ + pytest.param({"authorization": "Bearer sk-abc"}, "Bearer sk-abc", id="bearer token"), + pytest.param({"api-key": "sk-abc"}, "Bearer sk-abc", id="api-key header"), + pytest.param( + {"sec-websocket-protocol": "realtime, openai-insecure-api-key.sk-abc"}, + "Bearer sk-abc", + id="browser subprotocol", + ), + ], +) +@pytest.mark.asyncio +async def test_websocket_auth_still_reads_every_key_source(headers, expected): + """Accept control: forwarding None for every request would satisfy the + keyless assertions above and drop real keys on the floor.""" + websocket: Final = _websocket_for_auth(headers) + + with patch("litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True) as mock_auth: + await user_api_key_auth_websocket(websocket) + + assert mock_auth.call_args.kwargs["api_key"] == expected From 68c3b18e057463f37126c2dc5832e5024dfc05f0 Mon Sep 17 00:00:00 2001 From: L4XB Date: Tue, 15 Sep 2026 00:52:39 +0200 Subject: [PATCH 2/6] test(proxy): justify the websocket auth patches for the test-quality gate --- .../proxy/auth/test_user_api_key_auth.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py index b3a6f1286b9..dc997c5fd9c 100644 --- a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py +++ b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py @@ -7898,7 +7898,9 @@ async def test_websocket_auth_forwards_a_missing_key_as_none(headers): while accepting every HTTP route.""" websocket: Final = _websocket_for_auth(headers) - with patch("litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True) as mock_auth: + with patch( # test-quality-ok: what is under test is the value handed to the delegate, so it has to be observed + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True + ) as mock_auth: await user_api_key_auth_websocket(websocket) assert mock_auth.call_args.kwargs["api_key"] is None @@ -7910,7 +7912,9 @@ async def test_websocket_auth_without_master_key_returns_an_internal_user(): """With no master key configured, a keyless connection authenticates.""" websocket: Final = _websocket_for_auth({}) - with patch.multiple("litellm.proxy.proxy_server", master_key=None, **_KEYLESS_PROXY_STATE): + with patch.multiple( # test-quality-ok: master_key is a proxy_server module global with no injection seam + "litellm.proxy.proxy_server", master_key=None, **_KEYLESS_PROXY_STATE + ): result = await user_api_key_auth_websocket(websocket) assert isinstance(result, UserAPIKeyAuth) @@ -7924,7 +7928,9 @@ async def test_websocket_auth_with_master_key_still_refuses_a_keyless_client(): websocket: Final = _websocket_for_auth({}) - with patch.multiple("litellm.proxy.proxy_server", master_key="sk-master-key", **_KEYLESS_PROXY_STATE): + with patch.multiple( # test-quality-ok: master_key is a proxy_server module global with no injection seam + "litellm.proxy.proxy_server", master_key="sk-master-key", **_KEYLESS_PROXY_STATE + ): with pytest.raises(WebSocketException): await user_api_key_auth_websocket(websocket) @@ -7963,7 +7969,9 @@ async def test_websocket_auth_still_reads_every_key_source(headers, expected): keyless assertions above and drop real keys on the floor.""" websocket: Final = _websocket_for_auth(headers) - with patch("litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True) as mock_auth: + with patch( # test-quality-ok: what is under test is the value handed to the delegate, so it has to be observed + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True + ) as mock_auth: await user_api_key_auth_websocket(websocket) assert mock_auth.call_args.kwargs["api_key"] == expected From ea0e5773857cc1ca7e126dce6be751d53ea8ceab Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 17 Sep 2026 19:19:42 +0200 Subject: [PATCH 3/6] refactor(proxy): read the websocket key inline again The key extraction moves back into user_api_key_auth_websocket at its old place, with the lines that read the key unchanged from main. The behaviour change stays: a missing key reaches user_api_key_auth as None, and rejections raise WebSocketException without closing the socket first --- litellm/proxy/auth/user_api_key_auth.py | 45 ++++++++----------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 3b437d7865e..2028fed3159 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -629,36 +629,6 @@ def _apply_budget_limits_to_end_user_params( verbose_proxy_logger.debug("Applied budget limits to end user %s", end_user_id) -def _get_websocket_api_key(websocket: WebSocket) -> str | None: - """Read the API key a WebSocket client presented, or None when it presented none. - - Whether a key is required is decided by ``user_api_key_auth``, which allows a - keyless request when no master key is configured. - """ - authorization: Final = websocket.headers.get("authorization") - if authorization: - if not authorization.startswith("Bearer "): - raise WebSocketException( - code=status.WS_1008_POLICY_VIOLATION, - reason="Invalid Authorization header format", - ) - return authorization[len("Bearer ") :].strip() - - header_key: Final = websocket.headers.get("api-key") - if header_key: - return header_key - - subprotocol_prefix: Final = "openai-insecure-api-key." - return next( - ( - protocol.strip()[len(subprotocol_prefix) :] - for protocol in websocket.headers.get("sec-websocket-protocol", "").split(",") - if protocol.strip().startswith(subprotocol_prefix) - ), - None, - ) - - async def user_api_key_auth_websocket(websocket: WebSocket): # Accept the WebSocket connection @@ -690,7 +660,20 @@ async def user_api_key_auth_websocket(websocket: WebSocket): request.body = return_body - api_key: Final = _get_websocket_api_key(websocket) + authorization: Final = websocket.headers.get("authorization") + if not authorization: + api_key = websocket.headers.get("api-key") + if not api_key: + for protocol in websocket.headers.get("sec-websocket-protocol", "").split(","): + protocol = protocol.strip() + if protocol.startswith("openai-insecure-api-key."): + api_key = protocol[len("openai-insecure-api-key.") :] + break + else: + if not authorization.startswith("Bearer "): + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION, reason="Invalid Authorization header format") + + api_key = authorization[len("Bearer ") :].strip() try: return await user_api_key_auth( From 4147d2a55d861fbc9a1ee0145244085132cb79bf Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 17 Sep 2026 19:19:42 +0200 Subject: [PATCH 4/6] test(proxy): assert websocket auth outcomes instead of delegate arguments The websocket auth tests now run the real user_api_key_auth against a keyless and a master-key proxy and check what the client gets back, in place of the arguments handed to a mocked delegate --- .../proxy/auth/test_user_api_key_auth.py | 148 ++++++++---------- 1 file changed, 65 insertions(+), 83 deletions(-) diff --git a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py index c56fe10daa5..9a8dc5e262d 100644 --- a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py +++ b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py @@ -4,7 +4,7 @@ import logging import os import subprocess import sys -from contextlib import contextmanager +from contextlib import AbstractContextManager, contextmanager from datetime import datetime, timedelta, timezone from functools import partial from pathlib import Path @@ -8558,32 +8558,36 @@ async def test_router_settings_model_group_alias_authorizes_target_for_team(monk assert get_client_requested_model(request) == "AgentX-LLM" -def _websocket_for_auth(headers: dict | None = None) -> MagicMock: +def _websocket_for_auth(headers: dict[str, str]) -> tuple[MagicMock, AsyncMock]: from fastapi import WebSocket from starlette.datastructures import URL + close: Final = AsyncMock() websocket: Final = MagicMock(spec=WebSocket) websocket.query_params = {"model": "test-model"} - websocket.headers = headers or {} + websocket.headers = headers websocket.scope = { "type": "websocket", "path": "/v1/responses", - "headers": [(name.lower().encode(), value.encode()) for name, value in (headers or {}).items()], + "headers": [(name.lower().encode(), value.encode()) for name, value in headers.items()], } websocket.url = URL(url="/v1/responses") - websocket.close = AsyncMock() - return websocket + websocket.close = close + return websocket, close -_KEYLESS_PROXY_STATE: Final = { - "prisma_client": None, - "user_custom_auth": None, - "general_settings": {}, - "llm_model_list": [], - "llm_router": None, - "jwt_handler": None, - "open_telemetry_logger": None, -} +def _proxy_state(master_key: str | None) -> AbstractContextManager[object]: + return patch.multiple( # test-quality-ok: master_key is a proxy_server module global with no injection seam + "litellm.proxy.proxy_server", + master_key=master_key, + prisma_client=None, + user_custom_auth=None, + general_settings={}, + llm_model_list=[], + llm_router=None, + jwt_handler=None, + open_telemetry_logger=None, + ) @pytest.mark.parametrize( @@ -8594,87 +8598,65 @@ _KEYLESS_PROXY_STATE: Final = { ], ) @pytest.mark.asyncio -async def test_websocket_auth_forwards_a_missing_key_as_none(headers): - """A missing key must reach user_api_key_auth as None, the value - APIKeyHeader(auto_error=False) gives the HTTP routes for an absent header. - Rejecting it here meant a proxy with no master key refused the WebSocket - while accepting every HTTP route.""" - websocket: Final = _websocket_for_auth(headers) +async def test_websocket_auth_without_master_key_accepts_a_keyless_client(headers: dict[str, str]) -> None: + websocket, _ = _websocket_for_auth(headers) - with patch( # test-quality-ok: what is under test is the value handed to the delegate, so it has to be observed - "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True - ) as mock_auth: - await user_api_key_auth_websocket(websocket) + with _proxy_state(master_key=None): + result: Final = await user_api_key_auth_websocket(websocket) - assert mock_auth.call_args.kwargs["api_key"] is None - websocket.close.assert_not_called() - - -@pytest.mark.asyncio -async def test_websocket_auth_without_master_key_returns_an_internal_user(): - """With no master key configured, a keyless connection authenticates.""" - websocket: Final = _websocket_for_auth({}) - - with patch.multiple( # test-quality-ok: master_key is a proxy_server module global with no injection seam - "litellm.proxy.proxy_server", master_key=None, **_KEYLESS_PROXY_STATE - ): - result = await user_api_key_auth_websocket(websocket) - - assert isinstance(result, UserAPIKeyAuth) assert result.user_role == LitellmUserRoles.INTERNAL_USER - - -@pytest.mark.asyncio -async def test_websocket_auth_with_master_key_still_refuses_a_keyless_client(): - """Delegating the decision is only correct if the delegate still says no.""" - from starlette.exceptions import WebSocketException - - websocket: Final = _websocket_for_auth({}) - - with patch.multiple( # test-quality-ok: master_key is a proxy_server module global with no injection seam - "litellm.proxy.proxy_server", master_key="sk-master-key", **_KEYLESS_PROXY_STATE - ): - with pytest.raises(WebSocketException): - await user_api_key_auth_websocket(websocket) - - -@pytest.mark.asyncio -async def test_websocket_auth_rejects_a_malformed_header_without_closing_first(): - """Closing the socket and then raising an HTTPException makes Starlette - start an HTTP response on a closed socket, which surfaces as a RuntimeError - on top of the real auth failure.""" - from starlette.exceptions import WebSocketException - - websocket: Final = _websocket_for_auth({"authorization": "Token sk-1234"}) - - with pytest.raises(WebSocketException) as exc_info: - await user_api_key_auth_websocket(websocket) - - assert exc_info.value.code == status.WS_1008_POLICY_VIOLATION - websocket.close.assert_not_called() + assert result.api_key is None @pytest.mark.parametrize( - "headers,expected", + "headers", [ - pytest.param({"authorization": "Bearer sk-abc"}, "Bearer sk-abc", id="bearer token"), - pytest.param({"api-key": "sk-abc"}, "Bearer sk-abc", id="api-key header"), + pytest.param({"authorization": "Bearer sk-master-key"}, id="bearer token"), + pytest.param({"api-key": "sk-master-key"}, id="api-key header"), pytest.param( - {"sec-websocket-protocol": "realtime, openai-insecure-api-key.sk-abc"}, - "Bearer sk-abc", + {"sec-websocket-protocol": "realtime, openai-insecure-api-key.sk-master-key"}, id="browser subprotocol", ), ], ) @pytest.mark.asyncio -async def test_websocket_auth_still_reads_every_key_source(headers, expected): - """Accept control: forwarding None for every request would satisfy the - keyless assertions above and drop real keys on the floor.""" - websocket: Final = _websocket_for_auth(headers) +async def test_websocket_auth_accepts_the_master_key_from_every_key_source(headers: dict[str, str]) -> None: + websocket, _ = _websocket_for_auth(headers) - with patch( # test-quality-ok: what is under test is the value handed to the delegate, so it has to be observed - "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", autospec=True - ) as mock_auth: + with _proxy_state(master_key="sk-master-key"): + result: Final = await user_api_key_auth_websocket(websocket) + + assert result.user_role == LitellmUserRoles.PROXY_ADMIN + + +@pytest.mark.parametrize( + "headers", + [ + pytest.param({}, id="no key"), + pytest.param({"authorization": "Bearer sk-wrong-key"}, id="wrong key"), + ], +) +@pytest.mark.asyncio +async def test_websocket_auth_with_master_key_refuses_a_client_without_it(headers: dict[str, str]) -> None: + from starlette.exceptions import WebSocketException + + websocket, close = _websocket_for_auth(headers) + + with _proxy_state(master_key="sk-master-key"), pytest.raises(WebSocketException) as exc_info: await user_api_key_auth_websocket(websocket) - assert mock_auth.call_args.kwargs["api_key"] == expected + assert exc_info.value.code == status.WS_1008_POLICY_VIOLATION + close.assert_not_called() + + +@pytest.mark.asyncio +async def test_websocket_auth_rejects_a_malformed_header_without_closing_first() -> None: + from starlette.exceptions import WebSocketException + + websocket, close = _websocket_for_auth({"authorization": "Token sk-1234"}) + + with _proxy_state(master_key=None), pytest.raises(WebSocketException) as exc_info: + await user_api_key_auth_websocket(websocket) + + assert exc_info.value.code == status.WS_1008_POLICY_VIOLATION + close.assert_not_called() From d3e4eaff0c8a34e20dd18451bfc945acb3489bb8 Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 17 Sep 2026 19:43:11 +0200 Subject: [PATCH 5/6] test(proxy): cover a websocket bearer token with extra whitespace The master-key acceptance test now also sends the key after a double space, so dropping the strip on the bearer token, or keeping its prefix, fails a test --- tests/test_litellm/proxy/auth/test_user_api_key_auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py index 9a8dc5e262d..739cd7cfab0 100644 --- a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py +++ b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py @@ -8612,6 +8612,7 @@ async def test_websocket_auth_without_master_key_accepts_a_keyless_client(header "headers", [ pytest.param({"authorization": "Bearer sk-master-key"}, id="bearer token"), + pytest.param({"authorization": "Bearer sk-master-key"}, id="bearer token with extra space"), pytest.param({"api-key": "sk-master-key"}, id="api-key header"), pytest.param( {"sec-websocket-protocol": "realtime, openai-insecure-api-key.sk-master-key"}, From a1cb2e2f0fc6956813686ad118c3a6ee531b1623 Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 17 Sep 2026 20:35:26 +0200 Subject: [PATCH 6/6] refactor(proxy): drop a comment that no longer describes the function user_api_key_auth_websocket does not accept the connection; the route does, after this function returns. The comment has been wrong since the function was split out. --- litellm/proxy/auth/user_api_key_auth.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 2028fed3159..e3e8c33cfd8 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -630,8 +630,6 @@ def _apply_budget_limits_to_end_user_params( async def user_api_key_auth_websocket(websocket: WebSocket): - # Accept the WebSocket connection - ws_scope: Final = websocket.scope or {} scope_headers: Final = list(ws_scope.get("headers") or []) # ``get_request_route`` falls back to ``request.url.path`` when