entrypoints/openai/chat_completion/serving.py: OpenAIServingChat
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.
| File | vllm/entrypoints/openai/chat_completion/serving.py, 1,332 lines of Python. Three module helpers and one class. |
|---|---|
| Main class | OpenAIServingChat(GenerateBaseServing) (vllm/entrypoints/openai/chat_completion/serving.py:124). Subclasses: OpenAIServingChatBatch, AnthropicServingMessages, CohereServingChatV2. |
| Runs in | The API server process (front end), on its asyncio event loop. Template and tokenizer work goes to the renderer's thread pool. |
| Called by | The route create_chat_completion (vllm/entrypoints/openai/chat_completion/api_router.py:63-67); the object comes from app.state.openai_serving_chat. |
| Calls | OnlineRenderer.render_chat, get_max_tokens, ChatCompletionRequest.to_sampling_params, EngineClient.generate (AsyncLLM), ChatStreamProcessor and the composed Parser. |
| Not in this file | The 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#
Who calls it#
- Start-up.
init_generate_statemakes oneOpenAIServingChatwith the CLI flags, only for models with the generate task (vllm/entrypoints/generate/api_router.py:127-151). - The route.
create_chat_completionawaitshandler.create_chat_completion(request, raw_request). It sends anErrorResponseor aChatCompletionResponseas JSON, and wraps a generator in aStreamingResponse(vllm/entrypoints/openai/chat_completion/api_router.py:67-85). - Subclasses. The Anthropic
/v1/messagesand Cohere v2 handlers reuse its generators, and the hooks_create_chat_messageand_finalize_response_messageexist for them (vllm/entrypoints/openai/chat_completion/serving.py:445-471).
What it calls#
ParserManager.get_parseronce at start-up (vllm/entrypoints/openai/chat_completion/serving.py:167-175).self.online_renderer.render_chat(request)for each request (vllm/entrypoints/openai/chat_completion/serving.py:285).self.engine_client.generate(...), orself.beam_search(...)for beam search (vllm/entrypoints/openai/chat_completion/serving.py:385-408).ChatStreamProcessor.stepfor each output of a stream (vllm/entrypoints/openai/chat_completion/serving.py:705-710).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
_get_mm_token_counts, _make_prompt_tokens_details | vllm/entrypoints/openai/chat_completion/serving.py:79-115 | Count multimodal placeholder tokens and cached tokens for usage.prompt_tokens_details. |
OpenAIServingChat.__init__ | vllm/entrypoints/openai/chat_completion/serving.py:125-196 | Stores the flags, builds the parser class, reads the default sampling parameters. |
_effective_chat_template_kwargs | vllm/entrypoints/openai/chat_completion/serving.py:198-208 | Request kwargs over server defaults; also given to the parser. |
_make_parser | vllm/entrypoints/openai/chat_completion/serving.py:210-222 | One parser instance with the request's tools. |
_engine_reasoning_kwargs | vllm/entrypoints/openai/chat_completion/serving.py:242-262 | reasoning_ended and reasoning_parser_kwargs for the engine core. |
render_chat_request | vllm/entrypoints/openai/chat_completion/serving.py:264-285 | Model check, _preflight(n), render. |
create_chat_completion | vllm/entrypoints/openai/chat_completion/serving.py:287-300 | Public entry. With a KV connector and a remote prefill, it tells the connector when the request fails before the engine. |
_create_chat_completion | vllm/entrypoints/openai/chat_completion/serving.py:302-438 | The main flow: render, ids, sampling parameters, generator, stream or full. |
get_chat_request_role | vllm/entrypoints/openai/chat_completion/serving.py:440-443 | --response-role, or the role of the last message. |
chat_completion_stream_generator | vllm/entrypoints/openai/chat_completion/serving.py:473-508 | Turns chunks into data: ... strings; always ends with [DONE]. |
chat_completion_stream_generator_raw | vllm/entrypoints/openai/chat_completion/serving.py:510-943 | Role chunk, delta chunks, final chunk, usage chunk, errors. |
chat_completion_full_generator | vllm/entrypoints/openai/chat_completion/serving.py:945-1240 | Waits for the final output and builds one ChatCompletionResponse. |
_get_top_logprobs, _create_chat_logprobs | vllm/entrypoints/openai/chat_completion/serving.py:1242-1332 | OpenAI-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")
)- 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). - Line 166. With a reasoning parser, usage reports
reasoning_tokens. - 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).
- Lines 183-187. The
max_tokensoverride: from--override-generation-configfor the modesautoandvllm, 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)- Line 278.
_check_model: a wrong model name returns HTTP 404 here. - Line 283.
_preflight(n)raises the engine's dead error, or a 503 fromcheck_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. - 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)- Lines 310-313. The parser is made before rendering; it needs the effective template kwargs.
- Lines 314-316. An error response from the checks or the renderer returns at once.
- Lines 320-322. The request id:
"chatcmpl-"plus theX-Request-Idheader or the body'srequest_id. - 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,
)- Line 339. The loop handles one engine input; the chat renderer always returns one (line 412 asserts it).
- Lines 349-358.
max_completion_tokenswins over the deprecatedmax_tokens;get_max_tokenstakes the smallest limit. - 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,
)- Lines 396-408.
generate()makes an async generator; nothing reaches the engine yet. The priority comes from theX-Vllm-Priorityheader or the body._engine_reasoning_kwargsadds the two reasoning fields. - Lines 415-426. For a stream, the method returns the SSE generator. The route sends HTTP 200 when it starts iterating it.
- 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"- Lines 497-498. An error chunk is serialized in full.
- Lines 500-502. A normal chunk keeps only the fields that the code set; a chunk without choices (the usage chunk) also drops
Nonefields. - Lines 504-507. An exception during serialization still gives one error chunk.
- 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,
)- Lines 676-677. A choice that already sent its finish reason is skipped.
- Line 679. A finish reason
"error"raisesGenerationError. - Lines 696-703. An output with no text and no tokens, before the first token, is a chunked-prefill step: no chunk.
- 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,
)- Lines 854-856. The usage chunk sums the output tokens of all choices; the prompt counts once.
- Lines 866-871.
prompt_tokens_detailsonly 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- 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".
- Lines 966-971. No output at all gives HTTP 500.
- Lines 984-990. A new parser for each further choice; parsers keep state.
- Lines 1007-1019. One
parsecall splits reasoning, content and tool calls. Withinclude_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 diveData flow: types in and out
| Value | Type | Where |
|---|---|---|
| input | ChatCompletionRequest (pydantic), fastapi.Request | vllm/entrypoints/openai/chat_completion/serving.py:287-291 |
| after render | list[ConversationMessage], list[EngineInput] (one item) | vllm/entrypoints/openai/chat_completion/serving.py:264-267 |
| to the engine | SamplingParams or BeamSearchParams, request id chatcmpl-… | vllm/entrypoints/openai/chat_completion/serving.py:360-408 |
| from the engine | AsyncGenerator[RequestOutput] | vllm/entrypoints/openai/chat_completion/serving.py:337 |
| stream output | AsyncGenerator[str] of data: ...\n\n lines | vllm/entrypoints/openai/chat_completion/serving.py:473-484 |
| full output | ChatCompletionResponse or ErrorResponse | vllm/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 diveWorked 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).
_preflight(2):check_admission(2)counts two slots (vllm/entrypoints/openai/chat_completion/serving.py:283).num_choices= 2, so twoChatStreamProcessors (no parser configured) (vllm/entrypoints/openai/chat_completion/serving.py:528,vllm/entrypoints/openai/chat_completion/serving.py:552-555).- The first
RequestOutputtriggers two role chunks,index0 and 1, before any text (vllm/entrypoints/openai/chat_completion/serving.py:590-629). - Each later
RequestOutputholds 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 itsindex(vllm/entrypoints/openai/chat_completion/serving.py:672-674). So chunks of choice 0 and choice 1 can interleave. - Choice 0 ends first. Its EOS output has empty text, so the chunk has
delta: {"content": ""}andfinish_reason: "stop", andfinish_reason_sent[0]becomes true (vllm/entrypoints/openai/chat_completion/serving.py:787-806). Choice 1 goes on. - After the last output, the usage chunk:
prompt_tokens26,completion_tokens= 3 + 4 = 7 (each processor counts the token ids it saw, EOS included),total_tokens33 (vllm/entrypoints/openai/chat_completion/serving.py:854-859,vllm/entrypoints/openai/chat_completion/stream_processor.py:75). 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 diveError paths: what raises, what becomes a chunk
| Situation | Result | Where |
|---|---|---|
| wrong model name | ErrorResponse 404, returned before any stream | vllm/entrypoints/openai/chat_completion/serving.py:278-281 |
| engine dead, queue full | exception from _preflight → 500 / 503 by the handlers | vllm/entrypoints/openai/chat_completion/serving.py:283 |
| template or length error | exception or error response from the renderer → 400 | vllm/entrypoints/openai/chat_completion/serving.py:285 |
| parser cannot be built in a stream | one error chunk, then [DONE] | vllm/entrypoints/openai/chat_completion/serving.py:556-559 |
finish reason error | GenerationError → 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 loop | error chunk with the mapped status code | vllm/entrypoints/openai/chat_completion/serving.py:941-943 |
| client disconnect, full response | error response "Client disconnected" | vllm/entrypoints/openai/chat_completion/serving.py:963-964 |
| no output from the engine | 500 "No output received from the engine." | vllm/entrypoints/openai/chat_completion/serving.py:966-971 |
| logprobs asked but missing | AssertionError ("Did not output logprobs") | vllm/entrypoints/openai/chat_completion/serving.py:681-682 |
Deep diveFlags that reach this class
| Constructor argument | CLI flag | Effect 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-parser | The parser class (line 167). |
enable_force_include_usage | --enable-force-include-usage | Usage on every stream, per chunk too (vllm/entrypoints/openai/chat_completion/serving.py:562-564). |
enable_prompt_tokens_details | --enable-prompt-tokens-details | Cached and multimodal prompt tokens in usage. |
enable_per_request_metrics | --enable-per-request-metrics | Timing 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-deltas | Log the generated text, per delta and in full. |
return_tokens_as_token_ids | --return-tokens-as-token-ids | Logprob 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.
Related concepts#
- The OpenAI-compatible server: launch modes, app, serving classes and streaming: the app, the routes and the serving class tree.
- Renderers and chat templates: from messages to token ids: what
render_chatdoes. - Tool and reasoning parsers: from model text to tool_calls and reasoning: the parser that the stream processor and the full generator call.
- Structured outputs: JSON schemas, grammars and token bitmasks in vLLM: the constraint in the sampling parameters.
- v1/engine/async_llm.py: AsyncLLM, the engine client of vllm serve: the other side of
generate(). - Tour: Life of a chat request: stops 1, 2 and 16.
Check yourself#
- 1Which method returns the async generator of SSE strings, and which line always ends the stream?easy
Show answer
chat_completion_stream_generator; line 508 yieldsdata: [DONE]\n\n, after the loop and after any error chunk (vllm/entrypoints/openai/chat_completion/serving.py:485-508). - 2Why doesmedium
_create_chat_completionbuild a parser before it renders the request?Show answer
The parser needs the effective
chat_template_kwargs, and its reasoning half decidesreasoning_endedfor the engine from the prompt ids (vllm/entrypoints/openai/chat_completion/serving.py:310-313,vllm/entrypoints/openai/chat_completion/serving.py:242-262). - 3An 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). - 4A non-streaming request withhard
n= 2 sets--enable-per-request-metricson the server. Does the response carry the timing metrics?Show answer
No. The code builds per-request metrics only when
nis 1, because the stats belong to one of the sequences (vllm/entrypoints/openai/chat_completion/serving.py:1173-1181).