From 574d4b56704d15a5fb4904c7007dcbaba0e3bb4a Mon Sep 17 00:00:00 2001 From: alvcol Date: Fri, 16 Oct 2026 09:37:44 +0200 Subject: [PATCH] fix(vllm): send the deployment api_key on /vllm passthrough calls VLLMModelInfo.get_api_key always returned None, so /vllm/{endpoint} calls to a router model reached the vLLM server with no auth header even when the deployment had an api_key, and a server started with --api-key answered 401 (#23443). Return the deployment key (or VLLM_API_KEY) and send it as Authorization: Bearer, the header vllm serve --api-key checks. x-api-key was never actually sent before, since the key was always None. --- litellm/llms/vllm/common_utils.py | 4 ++-- tests/unit/passthrough/test_main.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/litellm/llms/vllm/common_utils.py b/litellm/llms/vllm/common_utils.py index 6484cdc88e3..11975c8dab7 100644 --- a/litellm/llms/vllm/common_utils.py +++ b/litellm/llms/vllm/common_utils.py @@ -40,7 +40,7 @@ class VLLMModelInfo(BaseLLMModelInfo): api_base: str | None = None, ) -> dict: if api_key is not None: - headers["x-api-key"] = api_key + headers["Authorization"] = f"Bearer {api_key}" return headers @staticmethod @@ -54,7 +54,7 @@ class VLLMModelInfo(BaseLLMModelInfo): @staticmethod def get_api_key(api_key: str | None = None) -> str | None: - return None + return api_key or get_secret_str("VLLM_API_KEY") @staticmethod def get_base_model(model: str) -> str | None: diff --git a/tests/unit/passthrough/test_main.py b/tests/unit/passthrough/test_main.py index 78fa83d126b..41324802375 100644 --- a/tests/unit/passthrough/test_main.py +++ b/tests/unit/passthrough/test_main.py @@ -51,3 +51,30 @@ async def test_allm_passthrough_route_hosted_vllm_sends_normalized_model(respx_m outbound: Final = json.loads(route.calls[0].request.content) assert outbound["model"] == "my-deployment" assert outbound["messages"] == [{"role": "user", "content": "Hello"}] + + +@pytest.mark.asyncio +async def test_allm_passthrough_route_hosted_vllm_sends_deployment_api_key(respx_mock: respx.MockRouter) -> None: + route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + response: Final = await allm_passthrough_route( + method="POST", + endpoint="v1/chat/completions", + model="hosted_vllm/my-deployment", + api_base=_API_BASE, + api_key="sk-upstream", + json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]}, + client=AsyncHTTPHandler(), + ) + assert response.status_code == 200 + assert route.calls[0].request.headers["authorization"] == "Bearer sk-upstream" + assert "x-api-key" not in route.calls[0].request.headers + + +def test_vllm_passthrough_api_key_falls_back_to_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("VLLM_API_KEY", "sk-from-env") + assert VLLMPassthroughConfig.get_api_key(None) == "sk-from-env" + assert VLLMPassthroughConfig.get_api_key("sk-explicit") == "sk-explicit" + monkeypatch.delenv("VLLM_API_KEY") + assert VLLMPassthroughConfig.get_api_key(None) is None