vLLM Explained

entrypoints/openai/chat_completion/serving.py: OpenAIServingChat

IntermediateCode mapabout 9 min read + 4 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

Purpose#

vllm/entrypoints/openai/chat_completion/serving.py holds the serving class of POST /v1/chat/completions: it turns one chat request into one engine generator and turns the engine's outputs back into the OpenAI chat format.

Filevllm/entrypoints/openai/chat_completion/serving.py, 1,332 lines of Python. Three module helpers and one class.
Main classOpenAIServingChat(GenerateBaseServing) (vllm/entrypoints/openai/chat_completion/serving.py:124). Subclasses: OpenAIServingChatBatch, AnthropicServingMessages, CohereServingChatV2.
Runs inThe API server process (front end), on its asyncio event loop. Template and tokenizer work goes to the renderer's thread pool.
Called byThe route create_chat_completion (vllm/entrypoints/openai/chat_completion/api_router.py:63-67); the object comes from app.state.openai_serving_chat.
CallsOnlineRenderer.render_chat, get_max_tokens, ChatCompletionRequest.to_sampling_params, EngineClient.generate (AsyncLLM), ChatStreamProcessor and the composed Parser.
Not in this fileThe route and the HTTP response types (api_router.py), the request fields and validators (protocol.py), template and tokenizer (vllm/renderers/), tool and reasoning parsing (vllm/parser/, vllm/tool_parsers/, vllm/reasoning/), the stream step logic (stream_processor.py), detokenization (vllm/v1/engine/).

The page The OpenAI-compatible server: launch modes, app, serving classes and streaming explains the server around this class. Stops 2 and 16 of Tour: Life of a chat request run its code for one streaming request. This page reads the file method by method.

Where it sits#

Figure: OpenAIServingChat in the class tree of the serving classes (panel 1), and the methods that each layer gives it (panel 2).
Open full size ↗

Who calls it#

What it calls#

Key classes and functions#

NameLineWhat it does
_get_mm_token_counts, _make_prompt_tokens_detailsvllm/entrypoints/openai/chat_completion/serving.py:79-115Count multimodal placeholder tokens and cached tokens for usage.prompt_tokens_details.
OpenAIServingChat.__init__vllm/entrypoints/openai/chat_completion/serving.py:125-196Stores the flags, builds the parser class, reads the default sampling parameters.
_effective_chat_template_kwargsvllm/entrypoints/openai/chat_completion/serving.py:198-208Request kwargs over server defaults; also given to the parser.
_make_parservllm/entrypoints/openai/chat_completion/serving.py:210-222One parser instance with the request's tools.
_engine_reasoning_kwargsvllm/entrypoints/openai/chat_completion/serving.py:242-262reasoning_ended and reasoning_parser_kwargs for the engine core.
render_chat_requestvllm/entrypoints/openai/chat_completion/serving.py:264-285Model check, _preflight(n), render.
create_chat_completionvllm/entrypoints/openai/chat_completion/serving.py:287-300Public entry. With a KV connector and a remote prefill, it tells the connector when the request fails before the engine.
_create_chat_completionvllm/entrypoints/openai/chat_completion/serving.py:302-438The main flow: render, ids, sampling parameters, generator, stream or full.
get_chat_request_rolevllm/entrypoints/openai/chat_completion/serving.py:440-443--response-role, or the role of the last message.
chat_completion_stream_generatorvllm/entrypoints/openai/chat_completion/serving.py:473-508Turns chunks into data: ... strings; always ends with [DONE].
chat_completion_stream_generator_rawvllm/entrypoints/openai/chat_completion/serving.py:510-943Role chunk, delta chunks, final chunk, usage chunk, errors.
chat_completion_full_generatorvllm/entrypoints/openai/chat_completion/serving.py:945-1240Waits for the final output and builds one ChatCompletionResponse.
_get_top_logprobs, _create_chat_logprobsvllm/entrypoints/openai/chat_completion/serving.py:1242-1332OpenAI-format logprobs, with -inf clamped to -9999.0.

Walk through the main methods#

__init__: what is fixed at start-up#

        self.enable_auto_tools: bool = enable_auto_tools
        self._include_reasoning_tokens_details = bool(reasoning_parser)
        self.parser_cls = ParserManager.get_parser(
            tool_parser_name=tool_parser,
            reasoning_parser_name=reasoning_parser,
            enable_auto_tools=enable_auto_tools,
            tool_strict_level=tool_strict_level,
            model_name=self.model_config.model,
            is_harmony=self.model_config.hf_config.model_type == "gpt_oss",
            tokenizer=self.renderer.tokenizer,
        )
        self.exclude_tools_when_tool_choice_none = exclude_tools_when_tool_choice_none

        self.enable_prompt_tokens_details = enable_prompt_tokens_details
        self.enable_force_include_usage = enable_force_include_usage
        self.enable_per_request_metrics = enable_per_request_metrics
        self.default_sampling_params = self.model_config.get_diff_sampling_param()
        mc = self.model_config
        self.override_max_tokens = (
            self.default_sampling_params.get("max_tokens")
            if mc.generation_config not in ("auto", "vllm")
            else getattr(mc, "override_generation_config", {}).get("max_new_tokens")
        )
  1. Lines 166-175. The parser class combines the tool parser (only with --enable-auto-tool-choice) and the reasoning parser. gpt-oss models are flagged as Harmony (Tool and reasoning parsers: from model text to tool_calls and reasoning).
  2. Line 166. With a reasoning parser, usage reports reasoning_tokens.
  3. Line 181. The server's default sampling parameters come from the model's generation config, read once here (The OpenAI-compatible API, defaults deep dive).
  4. Lines 183-187. The max_tokens override: from --override-generation-config for the modes auto and vllm, else from the generation config folder.

Lines 191-196 turn off browsing and the code interpreter for this route; the comments point to the Responses API for those.

render_chat_request: checks, then render#

    async def render_chat_request(
        self,
        request: ChatCompletionRequest,
    ) -> tuple[list[ConversationMessage], list[EngineInput]] | ErrorResponse:
        """Validate the model and preprocess a chat completion request.

        Delegates preprocessing logic to OnlineRenderer, adding the
        engine-aware checks (LoRA model validation, engine health).

        Returns:
            A tuple of (conversation, engine_inputs) on success,
            or an ErrorResponse on failure.

        """
        error_check_ret = await self._check_model(request)
        if error_check_ret is not None:
            logger.error("Error with model %s", error_check_ret)
            return error_check_ret

        self._preflight(request.n or 1)

        return await self.online_renderer.render_chat(request)
  1. Line 278. _check_model: a wrong model name returns HTTP 404 here.
  2. Line 283. _preflight(n) raises the engine's dead error, or a 503 from check_admission (vllm/entrypoints/generate/base/serving.py:169-181). These are exceptions, not error responses; the route's exception handlers turn them into HTTP errors.
  3. Line 285. The renderer returns the parsed conversation and a list with one EngineInput (Renderers and chat templates: from messages to token ids).

_create_chat_completion: from request to generator#

        tokenizer = self.renderer.tokenizer
        assert tokenizer is not None
        chat_template_kwargs = self._effective_chat_template_kwargs(request)
        parser: Parser | None = None
        if self.parser_cls is not None:
            parser = self._make_parser(request, tokenizer, chat_template_kwargs)
        result = await self.render_chat_request(request)
        if isinstance(result, ErrorResponse):
            return result

        conversation, engine_inputs = result

        request_id = (
            f"chatcmpl-{self._base_request_id(raw_request, request.request_id)}"
        )

        request_metadata = RequestResponseMetadata(request_id=request_id)
        if raw_request:
            raw_request.state.request_metadata = request_metadata

        lora_request = self._maybe_get_adapters(request, supports_default_mm_loras=True)

        model_name = self.models.model_name(lora_request)

        # Extract data_parallel_rank from header (router can inject it)
        data_parallel_rank = self._get_data_parallel_rank(raw_request)
  1. Lines 310-313. The parser is made before rendering; it needs the effective template kwargs.
  2. Lines 314-316. An error response from the checks or the renderer returns at once.
  3. Lines 320-322. The request id: "chatcmpl-" plus the X-Request-Id header or the body's request_id.
  4. Line 328. The LoRA adapter: by model name, or a default multimodal adapter that matches the request's content types.
        for i, engine_input in enumerate(engine_inputs):
            prompt_token_ids = self._extract_prompt_components(engine_input).token_ids
            mm_token_counts = _get_mm_token_counts(engine_input)

            # If we are creating sub requests for multiple prompts, ensure that they
            # have unique request ids.
            sub_request_id = (
                request_id if len(engine_inputs) == 1 else f"{request_id}_{i}"
            )

            max_tokens = get_max_tokens(
                max_model_len,
                request.max_completion_tokens
                if request.max_completion_tokens is not None
                else request.max_tokens,
                self._extract_prompt_len(engine_input),
                self.default_sampling_params,
                self.override_max_tokens,
                truncate_prompt_tokens=request.truncate_prompt_tokens,
            )

            sampling_params: SamplingParams | BeamSearchParams
            if request.use_beam_search:
                sampling_params = request.to_beam_search_params(
                    max_tokens, self.default_sampling_params
                )
            else:
                sampling_params = request.to_sampling_params(
                    max_tokens,
                    self.default_sampling_params,
                )
  1. Line 339. The loop handles one engine input; the chat renderer always returns one (line 412 asserts it).
  2. Lines 349-358. max_completion_tokens wins over the deprecated max_tokens; get_max_tokens takes the smallest limit.
  3. Lines 366-369. Sampling parameters: request value, else server default, else OpenAI default; DELTA for a stream, FINAL_ONLY otherwise.
            else:
                generator = self.engine_client.generate(
                    engine_input,
                    sampling_params,
                    sub_request_id,
                    lora_request=lora_request,
                    trace_headers=trace_headers,
                    priority=self._get_priority(request, raw_request),
                    data_parallel_rank=data_parallel_rank,
                    session_id=session_id,
                    **self._engine_reasoning_kwargs(
                        request, parser, chat_template_kwargs, prompt_token_ids
                    ),
                )

            generators.append(generator)

        assert len(generators) == 1
        (result_generator,) = generators

        if request.stream:
            return self.chat_completion_stream_generator(
                request,
                result_generator,
                request_id,
                model_name,
                conversation,
                tokenizer,
                request_metadata,
                chat_template_kwargs=chat_template_kwargs,
                mm_token_counts=mm_token_counts,
            )

        return await self.chat_completion_full_generator(
            request,
            result_generator,
            request_id,
            model_name,
            conversation,
            tokenizer,
            request_metadata,
            parser=parser,
            mm_token_counts=mm_token_counts,
        )
  1. Lines 396-408. generate() makes an async generator; nothing reaches the engine yet. The priority comes from the X-Vllm-Priority header or the body. _engine_reasoning_kwargs adds the two reasoning fields.
  2. Lines 415-426. For a stream, the method returns the SSE generator. The route sends HTTP 200 when it starts iterating it.
  3. Line 428. Without a stream, the method awaits the full generator, so the route gets a finished response.

chat_completion_stream_generator: the SSE framing#

        try:
            async for chunk in self.chat_completion_stream_generator_raw(
                request,
                result_generator,
                request_id,
                model_name,
                conversation,
                tokenizer,
                request_metadata,
                chat_template_kwargs=chat_template_kwargs,
                mm_token_counts=mm_token_counts,
            ):
                if isinstance(chunk, ErrorResponse):
                    data = chunk.model_dump_json()
                else:
                    data = chunk.model_dump_json(
                        exclude_unset=True, exclude_none=not chunk.choices
                    )
                yield f"data: {data}\n\n"
        except Exception as e:
            logger.exception("Error serializing chat completion stream.")
            data = self.create_streaming_error_response(e)
            yield f"data: {data}\n\n"
        yield "data: [DONE]\n\n"
  1. Lines 497-498. An error chunk is serialized in full.
  2. Lines 500-502. A normal chunk keeps only the fields that the code set; a chunk without choices (the usage chunk) also drops None fields.
  3. Lines 504-507. An exception during serialization still gives one error chunk.
  4. Line 508. data: [DONE] ends every stream, also after an error.

chat_completion_stream_generator_raw: chunks per output#

Set-up: one ChatStreamProcessor per choice, each with its own parser; usage flags from stream_options (vllm/entrypoints/openai/chat_completion/serving.py:528-564). Then the loop over RequestOutputs:

                for output in res.outputs:
                    i = output.index
                    processor = processors[i]
                    parser = processor.parser
                    if finish_reason_sent[i]:
                        continue

                    self._raise_if_error(output.finish_reason, request_id)

                    if request.logprobs:
                        assert output.logprobs is not None, "Did not output logprobs"
                        logprobs = self._create_chat_logprobs(
                            token_ids=output.token_ids,
                            top_logprobs=output.logprobs,
                            tokenizer=tokenizer,
                            num_output_top_logprobs=request.top_logprobs,
                            logprob_token_ids=request.logprob_token_ids,
                            return_as_token_id=request.return_tokens_as_token_ids,
                        )
                    else:
                        logprobs = None

                    delta_text = output.text

                    if (
                        not delta_text
                        and not output.token_ids
                        and not processor.num_output_tokens
                        and output.finish_reason is None
                    ):
                        # Chunked prefill case, don't return empty chunks
                        continue

                    delta_message, finish_reason = processor.step(
                        delta_text,
                        as_list(output.token_ids),
                        output.finish_reason,
                        res.prompt_token_ids,
                    )
  1. Lines 676-677. A choice that already sent its finish reason is skipped.
  2. Line 679. A finish reason "error" raises GenerationError.
  3. Lines 696-703. An output with no text and no tokens, before the first token, is a chunked-prefill step: no chunk.
  4. Lines 705-710. The processor runs the parser and maps the finish reason ("tool_calls" when tools were streamed).

The first output also makes one role chunk per choice, with delta = {"role": ..., "content": ""} (vllm/entrypoints/openai/chat_completion/serving.py:578-629). A None delta sends nothing, unless the output has a finish reason, logprobs, or token ids to return (vllm/entrypoints/openai/chat_completion/serving.py:731-741). The final chunk of a choice carries finish_reason and stop_reason, and the fingerprint when no usage chunk follows (vllm/entrypoints/openai/chat_completion/serving.py:787-824).

            # once the final token is handled, if stream_options.include_usage
            # is sent, send the usage
            if include_usage:
                completion_tokens = sum(p.num_output_tokens for p in processors)
                final_usage = UsageInfo(
                    prompt_tokens=num_prompt_tokens,
                    completion_tokens=completion_tokens,
                    total_tokens=num_prompt_tokens + completion_tokens,
                    completion_tokens_details=_make_completion_tokens_details(
                        sum(previous_reasoning_tokens)
                    )
                    if self._include_reasoning_tokens_details
                    else None,
                )
                final_usage.prompt_tokens_details = _make_prompt_tokens_details(
                    self.enable_prompt_tokens_details,
                    num_cached_tokens,
                    num_cache_creation_tokens,
                    mm_token_counts,
                )
  1. Lines 854-856. The usage chunk sums the output tokens of all choices; the prompt counts once.
  2. Lines 866-871. prompt_tokens_details only with --enable-prompt-tokens-details.

The method ends with two handlers: a GenerationError becomes an error chunk with its own status code, and any other exception becomes an error chunk with the code from create_error_response (vllm/entrypoints/openai/chat_completion/serving.py:937-943).

chat_completion_full_generator: one response#

        created_time = int(time.time())
        final_res: RequestOutput | None = None

        try:
            async for res in result_generator:
                final_res = res
        except asyncio.CancelledError:
            return self.create_error_response("Client disconnected")

        if final_res is None:
            return self.create_error_response(
                "No output received from the engine.",
                err_type="InternalServerError",
                status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
            )

        choices: list[ChatCompletionResponseChoice] = []
        total_reasoning_tokens = 0

        role = self.get_chat_request_role(request)
        tool_parser_cls = (
            self.parser_cls.tool_parser_cls if self.parser_cls is not None else None
        )
        for i, output in enumerate(final_res.outputs):
            # check for error finish reason and raise GenerationError
            # finish_reason='error' indicates a retryable request-level internal error
            self._raise_if_error(output.finish_reason, request_id)
            if i > 0 and parser is not None:
                # Parsers are stateful: like streaming, use one per choice.
                parser = self._make_parser(
                    request, tokenizer, self._effective_chat_template_kwargs(request)
                )
            if parser is not None and final_res.prompt_token_ids is not None:
                parser.set_prompt_token_ids(final_res.prompt_token_ids)
            token_ids = output.token_ids
            out_logprobs = output.logprobs

            if request.logprobs:
                assert out_logprobs is not None, "Did not output logprobs"
                logprobs = self._create_chat_logprobs(
                    token_ids=token_ids,
                    top_logprobs=out_logprobs,
                    num_output_top_logprobs=request.top_logprobs,
                    logprob_token_ids=request.logprob_token_ids,
                    tokenizer=tokenizer,
                    return_as_token_id=request.return_tokens_as_token_ids,
                )
            else:
                logprobs = None

            if parser is not None:
                reasoning, content, tool_calls = parser.parse(
                    output.text,
                    request,
                    enable_auto_tools=self.enable_auto_tools,
                    model_output_token_ids=token_ids,
                )
                suppress_metadata = not request.include_reasoning and parser is not None
                if not request.include_reasoning:
                    reasoning = None
                total_reasoning_tokens += parser.count_reasoning_tokens(token_ids)
                if suppress_metadata:
                    logprobs = None
            else:
                reasoning = None
                content = output.text
                tool_calls = []
                suppress_metadata = False
  1. Lines 961-964. Only the last output is kept. With FINAL_ONLY there is normally just one. A client disconnect gives the error response "Client disconnected".
  2. Lines 966-971. No output at all gives HTTP 500.
  3. Lines 984-990. A new parser for each further choice; parsers keep state.
  4. Lines 1007-1019. One parse call splits reasoning, content and tool calls. With include_reasoning: false, the reasoning and the logprobs are dropped.

Lines 1026-1095 choose how the message is built, by tool_choice and auto tool choice; lines 1110-1128 build the choice with resolve_finish_reason and the parallel_tool_calls filter. Usage and the response object follow (vllm/entrypoints/openai/chat_completion/serving.py:1147-1206).

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Data flow: types in and out

ValueTypeWhere
inputChatCompletionRequest (pydantic), fastapi.Requestvllm/entrypoints/openai/chat_completion/serving.py:287-291
after renderlist[ConversationMessage], list[EngineInput] (one item)vllm/entrypoints/openai/chat_completion/serving.py:264-267
to the engineSamplingParams or BeamSearchParams, request id chatcmpl-…vllm/entrypoints/openai/chat_completion/serving.py:360-408
from the engineAsyncGenerator[RequestOutput]vllm/entrypoints/openai/chat_completion/serving.py:337
stream outputAsyncGenerator[str] of data: ...\n\n linesvllm/entrypoints/openai/chat_completion/serving.py:473-484
full outputChatCompletionResponse or ErrorResponsevllm/entrypoints/openai/chat_completion/serving.py:945-956

The method also stores RequestResponseMetadata with the final usage on raw_request.state (vllm/entrypoints/openai/chat_completion/serving.py:324-326, vllm/entrypoints/openai/chat_completion/serving.py:905-916). The comment says it is for middleware; a search of vllm/entrypoints/ finds no code that reads final_usage_info, so it serves user middleware only (open question for in-tree use).

Deep dive

Worked example: a stream with n = 2 and include_usage

Request: the two ChatML messages (system and user) of the renderer worked example, "n": 2, "stream": true, "stream_options": {"include_usage": true}. The prompt has 26 tokens (Renderers and chat templates: from messages to token ids). Choice 0 writes 2 text tokens and EOS (3 tokens). Choice 1 writes 3 text tokens and EOS (4 tokens).

  1. _preflight(2): check_admission(2) counts two slots (vllm/entrypoints/openai/chat_completion/serving.py:283).
  2. num_choices = 2, so two ChatStreamProcessors (no parser configured) (vllm/entrypoints/openai/chat_completion/serving.py:528, vllm/entrypoints/openai/chat_completion/serving.py:552-555).
  3. The first RequestOutput triggers two role chunks, index 0 and 1, before any text (vllm/entrypoints/openai/chat_completion/serving.py:590-629).
  4. Each later RequestOutput holds the new outputs of one or both children; the collector merges outputs of the same index and keeps indexes apart (vllm/outputs.py:217-255). The loop sends one chunk per output, with its index (vllm/entrypoints/openai/chat_completion/serving.py:672-674). So chunks of choice 0 and choice 1 can interleave.
  5. Choice 0 ends first. Its EOS output has empty text, so the chunk has delta: {"content": ""} and finish_reason: "stop", and finish_reason_sent[0] becomes true (vllm/entrypoints/openai/chat_completion/serving.py:787-806). Choice 1 goes on.
  6. After the last output, the usage chunk: prompt_tokens 26, completion_tokens = 3 + 4 = 7 (each processor counts the token ids it saw, EOS included), total_tokens 33 (vllm/entrypoints/openai/chat_completion/serving.py:854-859, vllm/entrypoints/openai/chat_completion/stream_processor.py:75).
  7. data: [DONE].

In total the client gets 2 role chunks, 2 + 3 text chunks (one per text token, with the default stream_interval 1), 2 final chunks, 1 usage chunk and [DONE]: 11 events. (Derived; a chunk can carry more than one token when the reader is slower than the engine.)

Deep dive

Error paths: what raises, what becomes a chunk

SituationResultWhere
wrong model nameErrorResponse 404, returned before any streamvllm/entrypoints/openai/chat_completion/serving.py:278-281
engine dead, queue fullexception from _preflight → 500 / 503 by the handlersvllm/entrypoints/openai/chat_completion/serving.py:283
template or length errorexception or error response from the renderer → 400vllm/entrypoints/openai/chat_completion/serving.py:285
parser cannot be built in a streamone error chunk, then [DONE]vllm/entrypoints/openai/chat_completion/serving.py:556-559
finish reason errorGenerationError → error chunk (stream) or 500 (full)vllm/entrypoints/openai/chat_completion/serving.py:679, vllm/entrypoints/openai/chat_completion/serving.py:983
any exception in the stream looperror chunk with the mapped status codevllm/entrypoints/openai/chat_completion/serving.py:941-943
client disconnect, full responseerror response "Client disconnected"vllm/entrypoints/openai/chat_completion/serving.py:963-964
no output from the engine500 "No output received from the engine."vllm/entrypoints/openai/chat_completion/serving.py:966-971
logprobs asked but missingAssertionError ("Did not output logprobs")vllm/entrypoints/openai/chat_completion/serving.py:681-682
Deep dive

Flags that reach this class

Constructor argumentCLI flagEffect here
response_role--response-role (default assistant)Role of the answer with add_generation_prompt (vllm/entrypoints/openai/chat_completion/serving.py:440-443).
enable_auto_tools, tool_parser, reasoning_parser--enable-auto-tool-choice, --tool-call-parser, --reasoning-parserThe parser class (line 167).
enable_force_include_usage--enable-force-include-usageUsage on every stream, per chunk too (vllm/entrypoints/openai/chat_completion/serving.py:562-564).
enable_prompt_tokens_details--enable-prompt-tokens-detailsCached and multimodal prompt tokens in usage.
enable_per_request_metrics--enable-per-request-metricsTiming metrics in the response, only for n = 1 (vllm/entrypoints/openai/chat_completion/serving.py:1173-1186).
enable_log_outputs, enable_log_deltas--enable-log-outputs, --enable-log-deltasLog the generated text, per delta and in full.
return_tokens_as_token_ids--return-tokens-as-token-idsLogprob tokens as token_id:N (vllm/entrypoints/generate/base/serving.py:322-323).

The class reads no environment variable itself. The arguments are listed at vllm/entrypoints/generate/api_router.py:127-148.

Check yourself#

  1. 1
    Which method returns the async generator of SSE strings, and which line always ends the stream?
    easy
    Show answer

    chat_completion_stream_generator; line 508 yields data: [DONE]\n\n, after the loop and after any error chunk (vllm/entrypoints/openai/chat_completion/serving.py:485-508).

  2. 2
    Why does _create_chat_completion build a parser before it renders the request?
    medium
    Show answer

    The parser needs the effective chat_template_kwargs, and its reasoning half decides reasoning_ended for the engine from the prompt ids (vllm/entrypoints/openai/chat_completion/serving.py:310-313, vllm/entrypoints/openai/chat_completion/serving.py:242-262).

  3. 3
    An output arrives with empty text, no token ids and no finish reason, before the first token of a choice. What does the stream send?
    medium
    Show answer

    Nothing: the code treats it as a chunked-prefill step and skips it (vllm/entrypoints/openai/chat_completion/serving.py:696-703).

  4. 4
    A non-streaming request with n = 2 sets --enable-per-request-metrics on the server. Does the response carry the timing metrics?
    hard
    Show answer

    No. The code builds per-request metrics only when n is 1, because the stats belong to one of the sequences (vllm/entrypoints/openai/chat_completion/serving.py:1173-1181).