diff --git a/tests/entrypoints/scale_out/render/test_render.py b/tests/entrypoints/scale_out/render/test_render.py index 308555be36..37f4b8eecf 100644 --- a/tests/entrypoints/scale_out/render/test_render.py +++ b/tests/entrypoints/scale_out/render/test_render.py @@ -367,6 +367,74 @@ async def test_completion_render_multiple_prompts(client): assert prompt["request_id"].startswith("cmpl-") +CHAT_BODY = { + "model": MODEL_NAME, + "messages": [{"role": "user", "content": "Hello"}], +} + + +@pytest.mark.asyncio +async def test_chat_render_honors_body_request_id(client): + response = await client.post( + "/v1/chat/completions/render", json={**CHAT_BODY, "request_id": "abc"} + ) + + assert response.status_code == 200 + assert response.json()["request_id"] == "chatcmpl-abc" + + +@pytest.mark.asyncio +async def test_chat_render_header_request_id_wins(client): + response = await client.post( + "/v1/chat/completions/render", + json={**CHAT_BODY, "request_id": "abc"}, + headers={"X-Request-Id": "hdr"}, + ) + + assert response.status_code == 200 + assert response.json()["request_id"] == "chatcmpl-hdr" + + +@pytest.mark.asyncio +async def test_chat_render_generates_unique_request_ids(client): + ids = set() + for _ in range(2): + response = await client.post("/v1/chat/completions/render", json=CHAT_BODY) + assert response.status_code == 200 + request_id = response.json()["request_id"] + assert request_id.startswith("chatcmpl-") + ids.add(request_id) + + assert len(ids) == 2 + + +@pytest.mark.asyncio +async def test_completion_render_request_id_single_prompt(client): + response = await client.post( + "/v1/completions/render", + json={"model": MODEL_NAME, "prompt": "Hello world", "request_id": "abc"}, + ) + + assert response.status_code == 200 + assert [r["request_id"] for r in response.json()] == ["cmpl-abc"] + + +@pytest.mark.asyncio +async def test_completion_render_request_id_multiple_prompts(client): + response = await client.post( + "/v1/completions/render", + json={ + "model": MODEL_NAME, + "prompt": ["Hello world", "Goodbye world"], + "request_id": "abc", + }, + headers={"X-Request-Id": "hdr"}, + ) + + assert response.status_code == 200 + assert [r["request_id"] for r in response.json()] == ["cmpl-hdr-0", "cmpl-hdr-1"] + + @pytest.mark.asyncio async def test_chat_completion_render_multi_turn(client): """Test chat completion render with multi-turn conversation.""" diff --git a/vllm/entrypoints/scale_out/render/api_router.py b/vllm/entrypoints/scale_out/render/api_router.py index 97df983d2e..0bf6dbe5f1 100644 --- a/vllm/entrypoints/scale_out/render/api_router.py +++ b/vllm/entrypoints/scale_out/render/api_router.py @@ -43,7 +43,7 @@ async def render_chat_completion(request: ChatCompletionRequest, raw_request: Re "The model does not support Chat Completions Render API" ) - result = await handler.render_chat_request(request) + result = await handler.render_chat_request(request, raw_request) if isinstance(result, ErrorResponse): return JSONResponse(content=result.model_dump(), status_code=result.error.code) @@ -67,7 +67,7 @@ async def render_messages(request: AnthropicMessagesRequest, raw_request: Reques if handler is None: raise NotImplementedError("The model does not support Messages Render API") - result = await handler.render_messages_request(request) + result = await handler.render_messages_request(request, raw_request) if isinstance(result, ErrorResponse): return JSONResponse(content=result.model_dump(), status_code=result.error.code) @@ -90,7 +90,7 @@ async def render_completion(request: CompletionRequest, raw_request: Request): if handler is None: raise NotImplementedError("The model does not support Completions Render API") - result = await handler.render_completion_request(request) + result = await handler.render_completion_request(request, raw_request) if isinstance(result, ErrorResponse): return JSONResponse(content=result.model_dump(), status_code=result.error.code) diff --git a/vllm/entrypoints/scale_out/render/serving.py b/vllm/entrypoints/scale_out/render/serving.py index 5caf78bffe..b8d8ab7e1b 100644 --- a/vllm/entrypoints/scale_out/render/serving.py +++ b/vllm/entrypoints/scale_out/render/serving.py @@ -2,6 +2,8 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from typing import TYPE_CHECKING +from fastapi import Request + from vllm.entrypoints.anthropic.protocol import AnthropicMessagesRequest from vllm.entrypoints.anthropic.serving import AnthropicServingMessages from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest @@ -31,7 +33,6 @@ from vllm.renderers.inputs.preprocess import ( extract_prompt_len, ) from vllm.renderers.online_renderer import OnlineRenderer -from vllm.utils import random_uuid logger = init_logger(__name__) @@ -77,6 +78,7 @@ class ServingRender(BaseServing): async def render_chat_request( self, request: ChatCompletionRequest, + raw_request: Request | None = None, ) -> GenerateRequest | ErrorResponse: """Validate the model and preprocess a chat completion request. @@ -139,7 +141,9 @@ class ServingRender(BaseServing): ) reasoning_ended = request.resolve_reasoning_ended(parser, token_ids) - request_id = f"chatcmpl-{random_uuid()}" + request_id = ( + f"chatcmpl-{self._base_request_id(raw_request, request.request_id)}" + ) return GenerateRequest( request_id=request_id, @@ -161,6 +165,7 @@ class ServingRender(BaseServing): async def render_messages_request( self, request: AnthropicMessagesRequest, + raw_request: Request | None = None, ) -> GenerateRequest | ErrorResponse: """Validate the model and preprocess an Anthropic Messages request. @@ -172,11 +177,12 @@ class ServingRender(BaseServing): request, merge_inline_system=self._merge_inline_system ) chat_req.return_mm_kwargs = request.return_mm_kwargs - return await self.render_chat_request(chat_req) + return await self.render_chat_request(chat_req, raw_request) async def render_completion_request( self, request: CompletionRequest, + raw_request: Request | None = None, ) -> list[GenerateRequest] | ErrorResponse: """Validate the model and preprocess a completion request. @@ -197,8 +203,9 @@ class ServingRender(BaseServing): ) if isinstance(result, ErrorResponse): return result + base_id = f"cmpl-{self._base_request_id(raw_request, request.request_id)}" generate_requests: list[GenerateRequest] = [] - for engine_input in result: + for i, engine_input in enumerate(result): prompt_components = extract_prompt_components( self.model_config, engine_input ) @@ -220,7 +227,7 @@ class ServingRender(BaseServing): max_tokens, self.default_sampling_params ) - request_id = f"cmpl-{random_uuid()}" + request_id = base_id if len(result) == 1 else f"{base_id}-{i}" generate_requests.append( GenerateRequest(