main
py 347 lines 12.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """Integration test for middleware loader functionality.
5
6 Tests that customer middlewares get called correctly with a vLLM server.
7 """
8
9 import os
10 import tempfile
11
12 import pytest
13 import requests
14
15 from tests.utils import RemoteOpenAIServer
16
17 from .conftest import (
18 MODEL_NAME_SMOLLM,
19 )
20
21
22 class TestMiddlewareIntegration:
23 """Integration test for middleware with vLLM server."""
24
25 def setup_method(self):
26 """Setup for each test - simulate fresh server startup."""
27 self._clear_caches()
28
29 def _clear_caches(self):
30 """Clear middleware registry and function loader cache."""
31 try:
32 from model_hosting_container_standards.common.fastapi.middleware import (
33 middleware_registry,
34 )
35 from model_hosting_container_standards.common.fastapi.middleware.source.decorator_loader import ( # noqa: E501
36 decorator_loader,
37 )
38 from model_hosting_container_standards.sagemaker.sagemaker_loader import (
39 SageMakerFunctionLoader,
40 )
41
42 middleware_registry.clear_middlewares()
43 decorator_loader.clear()
44 SageMakerFunctionLoader._default_function_loader = None
45 except ImportError:
46 pytest.skip("model-hosting-container-standards not available")
47
48 @pytest.mark.asyncio
49 async def test_customer_middleware_with_vllm_server(self):
50 """Test that customer middlewares work with actual vLLM server.
51
52 Tests decorator-based middlewares (@custom_middleware, @input_formatter,
53 @output_formatter)
54 on multiple endpoints (chat/completions, invocations).
55 """
56 try:
57 from model_hosting_container_standards.sagemaker.config import (
58 SageMakerEnvVars,
59 )
60 except ImportError:
61 pytest.skip("model-hosting-container-standards not available")
62
63 # Customer writes a middleware script with multiple decorators
64 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
65 f.write(
66 """
67 from model_hosting_container_standards.common.fastapi.middleware import (
68 custom_middleware, input_formatter, output_formatter
69 )
70
71 # Global flag to track if input formatter was called
72 _input_formatter_called = False
73
74 @input_formatter
75 async def customer_input_formatter(request):
76 # Process input - mark that input formatter was called
77 global _input_formatter_called
78 _input_formatter_called = True
79 return request
80
81 @custom_middleware("throttle")
82 async def customer_throttle_middleware(request, call_next):
83 response = await call_next(request)
84 response.headers["X-Customer-Throttle"] = "applied"
85 order = response.headers.get("X-Middleware-Order", "")
86 response.headers["X-Middleware-Order"] = order + "throttle,"
87 return response
88
89 @output_formatter
90 async def customer_output_formatter(response):
91 global _input_formatter_called
92 response.headers["X-Customer-Processed"] = "true"
93 # Since input_formatter and output_formatter are combined into
94 # pre_post_process middleware,
95 # if output_formatter is called, input_formatter should have been called too
96 if _input_formatter_called:
97 response.headers["X-Input-Formatter-Called"] = "true"
98 order = response.headers.get("X-Middleware-Order", "")
99 response.headers["X-Middleware-Order"] = order + "output_formatter,"
100 return response
101 """
102 )
103 script_path = f.name
104
105 try:
106 script_dir = os.path.dirname(script_path)
107 script_name = os.path.basename(script_path)
108
109 # Set environment variables to point to customer script
110 env_vars = {
111 SageMakerEnvVars.SAGEMAKER_MODEL_PATH: script_dir,
112 SageMakerEnvVars.CUSTOM_SCRIPT_FILENAME: script_name,
113 }
114
115 args = [
116 "--dtype",
117 "bfloat16",
118 "--max-model-len",
119 "2048",
120 "--enforce-eager",
121 "--max-num-seqs",
122 "32",
123 ]
124
125 with RemoteOpenAIServer(
126 MODEL_NAME_SMOLLM, args, env_dict=env_vars
127 ) as server:
128 # Test 1: Middlewares applied to chat/completions endpoint
129 chat_response = requests.post(
130 server.url_for("v1/chat/completions"),
131 json={
132 "model": MODEL_NAME_SMOLLM,
133 "messages": [{"role": "user", "content": "Hello"}],
134 "max_tokens": 5,
135 "temperature": 0.0,
136 },
137 )
138
139 assert chat_response.status_code == 200
140
141 # Verify all middlewares were executed
142 assert "X-Customer-Throttle" in chat_response.headers
143 assert chat_response.headers["X-Customer-Throttle"] == "applied"
144 assert "X-Customer-Processed" in chat_response.headers
145 assert chat_response.headers["X-Customer-Processed"] == "true"
146
147 # Verify input formatter was called
148 assert "X-Input-Formatter-Called" in chat_response.headers
149 assert chat_response.headers["X-Input-Formatter-Called"] == "true"
150
151 # Verify middleware execution order
152 execution_order = chat_response.headers.get(
153 "X-Middleware-Order", ""
154 ).rstrip(",")
155 order_parts = execution_order.split(",") if execution_order else []
156 assert "throttle" in order_parts
157 assert "output_formatter" in order_parts
158
159 # Test 2: Middlewares applied to invocations endpoint
160 invocations_response = requests.post(
161 server.url_for("invocations"),
162 json={
163 "model": MODEL_NAME_SMOLLM,
164 "messages": [{"role": "user", "content": "Hello"}],
165 "max_tokens": 5,
166 "temperature": 0.0,
167 },
168 )
169
170 assert invocations_response.status_code == 200
171
172 # Verify all middlewares were executed
173 assert "X-Customer-Throttle" in invocations_response.headers
174 assert invocations_response.headers["X-Customer-Throttle"] == "applied"
175 assert "X-Customer-Processed" in invocations_response.headers
176 assert invocations_response.headers["X-Customer-Processed"] == "true"
177
178 # Verify input formatter was called
179 assert "X-Input-Formatter-Called" in invocations_response.headers
180 assert (
181 invocations_response.headers["X-Input-Formatter-Called"] == "true"
182 )
183
184 finally:
185 os.unlink(script_path)
186
187 @pytest.mark.asyncio
188 async def test_middleware_with_ping_endpoint(self):
189 """Test that middlewares work with SageMaker ping endpoint."""
190 try:
191 from model_hosting_container_standards.sagemaker.config import (
192 SageMakerEnvVars,
193 )
194 except ImportError:
195 pytest.skip("model-hosting-container-standards not available")
196
197 # Customer writes a middleware script
198 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
199 f.write(
200 """
201 from model_hosting_container_standards.common.fastapi.middleware import (
202 custom_middleware
203 )
204
205 @custom_middleware("pre_post_process")
206 async def ping_tracking_middleware(request, call_next):
207 response = await call_next(request)
208 if request.url.path == "/ping":
209 response.headers["X-Ping-Tracked"] = "true"
210 return response
211 """
212 )
213 script_path = f.name
214
215 try:
216 script_dir = os.path.dirname(script_path)
217 script_name = os.path.basename(script_path)
218
219 env_vars = {
220 SageMakerEnvVars.SAGEMAKER_MODEL_PATH: script_dir,
221 SageMakerEnvVars.CUSTOM_SCRIPT_FILENAME: script_name,
222 }
223
224 args = [
225 "--dtype",
226 "bfloat16",
227 "--max-model-len",
228 "2048",
229 "--enforce-eager",
230 "--max-num-seqs",
231 "32",
232 ]
233
234 with RemoteOpenAIServer(
235 MODEL_NAME_SMOLLM, args, env_dict=env_vars
236 ) as server:
237 # Test ping endpoint with middleware
238 response = requests.get(server.url_for("ping"))
239
240 assert response.status_code == 200
241 assert "X-Ping-Tracked" in response.headers
242 assert response.headers["X-Ping-Tracked"] == "true"
243
244 finally:
245 os.unlink(script_path)
246
247 @pytest.mark.asyncio
248 async def test_middleware_env_var_override(self):
249 """Test middleware environment variable overrides."""
250 try:
251 from model_hosting_container_standards.common.fastapi.config import (
252 FastAPIEnvVars,
253 )
254 from model_hosting_container_standards.sagemaker.config import (
255 SageMakerEnvVars,
256 )
257 except ImportError:
258 pytest.skip("model-hosting-container-standards not available")
259
260 # Create a script with middleware functions specified via env vars
261 with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
262 f.write(
263 """
264 from fastapi import Request
265
266 # Global flag to track if pre_process was called
267 _pre_process_called = False
268
269 async def env_throttle_middleware(request, call_next):
270 response = await call_next(request)
271 response.headers["X-Env-Throttle"] = "applied"
272 return response
273
274 async def env_pre_process(request: Request) -> Request:
275 # Mark that pre_process was called
276 global _pre_process_called
277 _pre_process_called = True
278 return request
279
280 async def env_post_process(response):
281 global _pre_process_called
282 if hasattr(response, 'headers'):
283 response.headers["X-Env-Post-Process"] = "applied"
284 # Since pre_process and post_process are combined into
285 # pre_post_process middleware,
286 # if post_process is called, pre_process should have been called too
287 if _pre_process_called:
288 response.headers["X-Pre-Process-Called"] = "true"
289 return response
290 """
291 )
292 script_path = f.name
293
294 try:
295 script_dir = os.path.dirname(script_path)
296 script_name = os.path.basename(script_path)
297
298 # Set environment variables for middleware
299 # Use script_name with .py extension as per plugin example
300 env_vars = {
301 SageMakerEnvVars.SAGEMAKER_MODEL_PATH: script_dir,
302 SageMakerEnvVars.CUSTOM_SCRIPT_FILENAME: script_name,
303 FastAPIEnvVars.CUSTOM_FASTAPI_MIDDLEWARE_THROTTLE: (
304 f"{script_name}:env_throttle_middleware"
305 ),
306 FastAPIEnvVars.CUSTOM_PRE_PROCESS: f"{script_name}:env_pre_process",
307 FastAPIEnvVars.CUSTOM_POST_PROCESS: f"{script_name}:env_post_process",
308 }
309
310 args = [
311 "--dtype",
312 "bfloat16",
313 "--max-model-len",
314 "2048",
315 "--enforce-eager",
316 "--max-num-seqs",
317 "32",
318 ]
319
320 with RemoteOpenAIServer(
321 MODEL_NAME_SMOLLM, args, env_dict=env_vars
322 ) as server:
323 response = requests.get(server.url_for("ping"))
324 assert response.status_code == 200
325
326 # Check if environment variable middleware was applied
327 headers = response.headers
328
329 # Verify that env var middlewares were applied
330 assert "X-Env-Throttle" in headers, (
331 "Throttle middleware should be applied via env var"
332 )
333 assert headers["X-Env-Throttle"] == "applied"
334
335 assert "X-Env-Post-Process" in headers, (
336 "Post-process middleware should be applied via env var"
337 )
338 assert headers["X-Env-Post-Process"] == "applied"
339
340 # Verify that pre_process was called
341 assert "X-Pre-Process-Called" in headers, (
342 "Pre-process should be called via env var"
343 )
344 assert headers["X-Pre-Process-Called"] == "true"
345
346 finally:
347 os.unlink(script_path)