fix(proxy): preserve team aliases in Responses WebSockets (#51380)
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Cost map pins recheck / rerun-cost-map-guard (push) Waiting to run
Publish lint base counts / publish (basedpyright, litellm/scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, litellm/scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, litellm/scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, litellm/scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, litellm/scripts/type_discipline_gate.py) (push) Waiting to run
Root entry limit / root-entry-limit (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
smoke / dashboard-build (push) Waiting to run
smoke / py3.10 (push) Waiting to run
smoke / py3.11 (push) Waiting to run
smoke / py3.12 (push) Waiting to run
smoke / py3.13 (push) Waiting to run
smoke / py3.14 (push) Waiting to run
smoke / py3.15 (push) Waiting to run
smoke / all-extras-py3.12 (push) Waiting to run
smoke / all-extras-py3.13 (push) Waiting to run
smoke / windows-sdk (push) Waiting to run
smoke / proxy-build-from-pip (push) Waiting to run
smoke / smoke passed (push) Blocked by required conditions
LiteLLM Rust DB / db-queries (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
unit / endpoints (push) Blocked by required conditions
unit / enterprise-package (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / mcp-server (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / proxy-infra-db-spend (push) Blocked by required conditions
unit / proxy-infra-root-a-p (push) Blocked by required conditions
unit / proxy-infra-root-q-z (push) Blocked by required conditions
unit / proxy-infra-routing (push) Blocked by required conditions
unit / proxy-subprocess (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / assert-shard-coverage (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 3.25.0 compatibility (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / rust-bridge (push) Waiting to run
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / proxy-server (push) Blocked by required conditions
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.0.0 compatibility (push) Blocked by required conditions
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run

* fix(proxy): preserve team aliases in Responses WebSockets

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>

* test(proxy): drop the pre-call kwargs assertion from the Responses WebSocket routing test

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Co-authored-by: mateo <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Brian Hong 2026-11-13 17:42:38 +09:00 • committed by GitHub
parent 76794359cc
commit 6abacd329b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 106 additions and 2 deletions

View file

@ -2678,7 +2678,6 @@ async def _responses_websocket_session(
user_request_timeout=user_request_timeout,
user_max_tokens=user_max_tokens,
user_api_base=user_api_base,
model=resolved_model,
route_type="_aresponses_websocket",
skip_guardrails=True,
)

View file

@ -2546,6 +2546,112 @@ def _key() -> UserAPIKeyAuth:
class TestResponsesWSFirstFrameModelAuth:
@pytest.mark.parametrize("query_model", [None, "gpt-6.1-sol"])
@pytest.mark.parametrize("nested", [False, True])
def test_team_alias_keeps_websocket_turns_on_the_authorized_deployment(
self, monkeypatch: pytest.MonkeyPatch, query_model: str | None, nested: bool
) -> None:
from openai.types.responses import ResponseCompletedEvent
import litellm.proxy.proxy_server as ps
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth_websocket
from litellm.responses.utils import ResponsesAPIRequestUtils
from litellm.types.llms.openai import InputTokensDetails, OutputTokensDetails
model: Final = "gpt-6.1-sol"
authorized_model: Final = "restricted/" + model
deployment_id: Final = "restricted-deployment"
router: Final = litellm.Router(
model_list=[
{
"model_name": "shared/" + model,
"litellm_params": {
"model": model,
"custom_llm_provider": "openai",
"api_key": "shared-fixture-key",
"order": 0,
},
"model_info": {"id": "shared-deployment"},
},
{
"model_name": authorized_model,
"litellm_params": {
"model": model,
"custom_llm_provider": "openai",
"api_key": "restricted-fixture-key",
"order": 1,
},
"model_info": {"id": deployment_id, "access_groups": ["Restricted"]},
},
],
num_retries=0,
)
auth: Final = UserAPIKeyAuth(
api_key="hashed-fixture-key",
team_id="alias-team",
models=["all-team-models"],
team_models=["Restricted"],
team_model_aliases={model: authorized_model},
)
completed: Final = json.dumps(
{
"type": "response.completed",
"sequence_number": 1,
"response": ResponsesAPIResponse(
id="resp_alias",
object="response",
created_at=1,
status="completed",
model=model,
output=[],
parallel_tool_calls=True,
tool_choice="auto",
tools=[],
usage=ResponseAPIUsage(
input_tokens=1,
output_tokens=0,
total_tokens=1,
input_tokens_details=InputTokensDetails(cached_tokens=0),
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
),
).model_dump(mode="json"),
}
)
def provider_connect(*_args: object, **_kwargs: object) -> MagicMock:
replies: Final[asyncio.Queue[str]] = asyncio.Queue()
async def send(_message: str) -> None:
await replies.put(completed)
async def receive(**_kwargs: object) -> str:
return await replies.get()
peer: Final = MagicMock()
peer.send = AsyncMock(side_effect=send)
peer.recv = AsyncMock(side_effect=receive)
peer.close = AsyncMock()
connection: Final = MagicMock()
connection.__aenter__ = AsyncMock(return_value=peer)
connection.__aexit__ = AsyncMock(return_value=None)
return connection
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "master_key", None)
monkeypatch.setattr(ps, "general_settings", {})
monkeypatch.setitem(app.dependency_overrides, user_api_key_auth_websocket, lambda: auth)
monkeypatch.setattr("websockets.connect", provider_connect)
payload: Final = {"model": model, "input": "hello"}
frame: Final = (
{"type": "response.create", "response": payload} if nested else {"type": "response.create", **payload}
)
path: Final = "/v1/responses" + (f"?model={query_model}" if query_model is not None else "")
with TestClient(app).websocket_connect(path) as websocket:
websocket.send_json(frame)
response: Final = ResponseCompletedEvent.model_validate_json(websocket.receive_text())
assert ResponsesAPIRequestUtils.get_model_id_from_response_id(response.response.id) == deployment_id
@pytest.mark.asyncio
async def test_endpoint_enforces_auth_after_model_from_first_frame(self):
from litellm.proxy.response_api_endpoints.endpoints import (
@ -2726,7 +2832,6 @@ class TestResponsesWSFirstFrameModelAuth:
assert routed["model"] == "gpt-4o-mini"
assert routed["input"] == replayed_input
assert routed["previous_response_id"] == "resp_prev"
assert processor.common_processing_pre_call_logic.await_args.kwargs["model"] == "gpt-4o-mini"
assert processor.common_processing_pre_call_logic.await_args.kwargs["skip_guardrails"] is True
assert callable(routed["responses_ws_frame_guard"])
assert mock_route_request.await_args.kwargs["route_type"] == "_aresponses_websocket"