vLLM Explained

The OpenAI-compatible API

BeginnerLearn the basicsabout 6 min read + 14 min deep dives

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

In one sentence

vllm serve accepts OpenAI-style JSON requests on HTTP routes such as /v1/chat/completions and answers with JSON or a stream of SSE events.

The idea#

A shared request format#

Many client tools speak the HTTP API of OpenAI: the openai Python SDK, agent frameworks, benchmark tools and routers. vLLM accepts the same JSON. So you change only the base URL of the client, for example to http://localhost:8000/v1, and the client talks to your own model. The server listens on port 8000 by default (vllm/entrypoints/common/cli_args.py:286-287).

Each route is one HTTP method and one path. Two routes do most of the work:

  • POST /v1/chat/completions takes a list of chat messages. The server turns them into one prompt with the chat template of the model. This is the main route for chat models and the route of all examples on this site.
  • POST /v1/completions takes a raw prompt: a string, a list of strings, or token ids (vllm/entrypoints/openai/completion/protocol.py:53-59). There is no chat template.

Other routes list the models (GET /v1/models), check health (GET /health), give Prometheus metrics (/metrics) and turn text into tokens (POST /tokenize). The first deep dive lists them all.

Chat roles and the chat template#

A chat request is a list of messages. Each message has a role and content. The usual roles are system, user, assistant and tool. The model does not see JSON. The chat template, a Jinja template from the tokenizer files, writes the messages into one text with special tokens. Then the tokenizer turns that text into token ids (Tokens and tokenizers). The page Renderers and chat templates: from messages to token ids explains how vLLM does this.

Streaming with server-sent events#

With "stream": true, the server keeps the HTTP response open. It sends one SSE event for each new piece of text. Each event is one line data: {...} and a blank line. The last event is data: [DONE]. So the user sees the first words after the time to first token, not after the whole answer (Serving metrics: TTFT, TPOT, ITL, throughput, goodput).

An everyday comparison#

Think of a standard order form in a chain of restaurants. Every kitchen accepts the same form, so a customer can order in any of them. vLLM is a new kitchen that accepts the standard form. It also reads some extra boxes on the form that only vLLM knows, for example top_k.

The comparison stops here: a kitchen sends the whole meal at once. With streaming, vLLM sends the answer in many small parts while it still makes the rest.

The picture#

Figure: the request fields become SamplingParams (panel 1). The client gets one JSON body, or a stream of SSE events that ends with a usage chunk and data: [DONE] (panel 2). The values are an example.
Open full size ↗

Read panel 1 from left to right. Fields that the client does not send come from the model's generation_config.json first and from the OpenAI defaults second. Panel 2 shows the two answer forms of the same request. The deep dive "The stream, chunk by chunk" explains each SSE line.

In vLLM#

This is a basics page, so this part gives only pointers. The page The OpenAI-compatible server: launch modes, app, serving classes and streaming explains the server code, and stops 1, 2 and 16 of Tour: Life of a chat request follow one request through it.

  1. The route. create_chat_completion() in vllm/entrypoints/openai/chat_completion/api_router.py handles POST /v1/chat/completions. It returns a JSONResponse, or a StreamingResponse with the media type text/event-stream (vllm/entrypoints/openai/chat_completion/api_router.py:43-84).
  2. The request class. ChatCompletionRequest is a pydantic model. Its fields follow the order of the OpenAI documentation, and vLLM's extra fields come after them (vllm/entrypoints/openai/chat_completion/protocol.py:225-228).
  3. The handler. OpenAIServingChat._create_chat_completion() renders the messages, computes max_tokens, makes SamplingParams and calls engine_client.generate() (vllm/entrypoints/openai/chat_completion/serving.py:302-418).
@router.post(
    "/v1/chat/completions",
    dependencies=[Depends(validate_json_request)],
    **json_or_sse_responses(
        ChatCompletionResponse,
        ChatCompletionStreamResponse,
        {
            HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
            HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
            HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
            HTTPStatus.NOT_IMPLEMENTED.value: {"model": ErrorResponse},
        },
    ),
)
@with_cancellation
@load_aware_call
async def create_chat_completion(request: ChatCompletionRequest, raw_request: Request):
    metrics_header_format = raw_request.headers.get(
        ENDPOINT_LOAD_METRICS_FORMAT_HEADER_LABEL, ""
    )
    handler = chat(raw_request)
    if handler is None:
        raise NotImplementedError("The model does not support Chat Completions API")

    generator = await handler.create_chat_completion(request, raw_request)

    if isinstance(generator, ErrorResponse):
        return JSONResponse(
            content=generator.model_dump(), status_code=generator.error.code
        )

    elif isinstance(generator, ChatCompletionResponse):
        return JSONResponse(
            content=generator.model_dump(),
            headers=metrics_header(metrics_header_format),
        )

    args = getattr(raw_request.app.state, "args", None)
    keep_alive_interval = getattr(args, "sse_keep_alive_interval", 0)
    return StreamingResponse(
        content=with_sse_keep_alive(generator, float(keep_alive_interval)),
        media_type="text/event-stream",
The chat route. An ErrorResponse becomes JSON with its own status code. A full response becomes JSON. A generator becomes an SSE stream.

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

The routes that vLLM registers

build_app() makes the FastAPI app and calls register_api_routers() (vllm/entrypoints/launchers/app.py:19-45). That function adds the routers in groups. Some groups depend on the tasks of the model or on a flag (vllm/entrypoints/launchers/api_server/routers.py:12-90). After that, endpoint plugins can add their own routes (vllm/entrypoints/launchers/app.py:47-51).

RouteDefined atWhen registered
POST /v1/chat/completionsvllm/entrypoints/openai/chat_completion/api_router.py:43-44the model supports the task generate (vllm/entrypoints/launchers/api_server/routers.py:55-60, vllm/entrypoints/generate/api_router.py:21-26)
POST /v1/chat/completions/batchvllm/entrypoints/openai/chat_completion/api_router.py:88-89same
POST /v1/completionsvllm/entrypoints/openai/completion/api_router.py:37-38same (vllm/entrypoints/generate/api_router.py:34-38)
POST /v1/responses, GET /v1/responses/{id}, POST /v1/responses/{id}/cancelvllm/entrypoints/openai/responses/api_router.py:48-49, vllm/entrypoints/openai/responses/api_router.py:80, vllm/entrypoints/openai/responses/api_router.py:110same (vllm/entrypoints/generate/api_router.py:28-32)
POST /v1/messages, POST /v1/messages/count_tokens (Anthropic format)vllm/entrypoints/anthropic/api_router.py:51-52, vllm/entrypoints/anthropic/api_router.py:89-90same (vllm/entrypoints/generate/api_router.py:40-44)
GET /v1/modelsvllm/entrypoints/openai/models/api_router.py:20always (vllm/entrypoints/launchers/api_server/routers.py:22-26)
GET /healthvllm/entrypoints/serve/instrumentator/health.py:22always (vllm/entrypoints/serve/instrumentator/__init__.py:7-14)
/metrics (Prometheus)vllm/entrypoints/serve/instrumentator/metrics.py:56-81always
GET /version, GET /loadvllm/entrypoints/serve/instrumentator/basic.py:53, vllm/entrypoints/serve/instrumentator/basic.py:30always
POST /tokenize, POST /detokenizevllm/entrypoints/serve/tokenize/api_router.py:35-36, vllm/entrypoints/serve/tokenize/api_router.py:61-62always (vllm/entrypoints/serve/__init__.py:36-40)
POST /v1/embeddings, /pooling, /score, /rerank and othersvllm/entrypoints/pooling/embed/api_router.py:28-29, vllm/entrypoints/pooling/pooling/api_router.py:27-28the model has a pooling task (vllm/entrypoints/launchers/api_server/routers.py:80-83)
POST /v1/load_lora_adapter, /v1/unload_lora_adaptervllm/entrypoints/serve/lora/api_router.py:63, vllm/entrypoints/serve/lora/api_router.py:81-82only with VLLM_ALLOW_RUNTIME_LORA_UPDATING (vllm/entrypoints/serve/lora/api_router.py:30-32)
/reset_prefix_cache, /sleep, /collective_rpc and other development routesvllm/entrypoints/serve/__init__.py:43-65only with VLLM_SERVER_DEV_MODE (vllm/entrypoints/launchers/api_server/routers.py:46-53)

The health check. GET /health asks the engine client. It returns 200 when the engine is alive and 503 after an EngineDeadError (vllm/entrypoints/serve/instrumentator/health.py:22-33).

API keys. With --api-key (or VLLM_API_KEY when the flag is not set), a middleware checks the Authorization: Bearer header (vllm/entrypoints/serve/middleware/register.py:32-36). It guards only paths that start with /v1, /v2, /inference or /cohere, and answers 401 otherwise (vllm/entrypoints/serve/middleware/authenticate.py:11, vllm/entrypoints/serve/middleware/authenticate.py:59-60). So /health and /metrics stay open.

Deep dive

Request fields that matter for serving

The table lists the OpenAI fields of ChatCompletionRequest that change how the server works (vllm/entrypoints/openai/chat_completion/protocol.py:228-249).

FieldDefaultWhat vLLM does with it
modelNoneSelects the served model name or a LoRA adapter. A wrong name gives an error response (vllm/entrypoints/openai/chat_completion/serving.py:278-281).
messagesrequiredThe chat. The renderer applies the chat template (vllm/entrypoints/openai/chat_completion/serving.py:285).
max_completion_tokens, max_tokensNonemax_tokens is marked as deprecated; the new field wins when both are set (vllm/entrypoints/openai/chat_completion/protocol.py:234-239, vllm/entrypoints/openai/chat_completion/serving.py:349-357). See the next deep dive for the default.
temperature, top_pNoneRequest value, else the server default, else 1.0 (vllm/entrypoints/openai/chat_completion/protocol.py:692-700).
streamFalseTrue gives an SSE stream and RequestOutputKind.DELTA; false gives one body and FINAL_ONLY (vllm/entrypoints/openai/chat_completion/protocol.py:797-799).
stream_optionsNoneinclude_usage adds a final usage chunk. continuous_usage_stats adds usage to every chunk, but only together with include_usage. The server flag --enable-force-include-usage turns on both for every stream (vllm/entrypoints/serve/utils/api_utils.py:273-285). Allowed only with stream: true (vllm/entrypoints/openai/chat_completion/protocol.py:845-856).
n1The number of answers per request (vllm/entrypoints/openai/chat_completion/protocol.py:240).
stop[]Stop strings (vllm/entrypoints/openai/chat_completion/protocol.py:244).
tools, tool_choiceNone, "none"Tool definitions. With tools and no tool_choice, the server sets "auto" (vllm/entrypoints/openai/chat_completion/protocol.py:975-978).
response_formatNoneJSON mode or a JSON schema. It becomes structured-output parameters (vllm/entrypoints/openai/chat_completion/protocol.py:728-733); see Structured outputs: JSON schemas, grammars and token bitmasks in vLLM.
logprobs, top_logprobsFalse, 0When logprobs is true, the sampler returns top_logprobs alternatives per token (vllm/entrypoints/openai/chat_completion/protocol.py:784-788).
userNoneIgnored: "this will be ignored by vLLM" (vllm/entrypoints/openai/chat_completion/protocol.py:274-275).

vLLM's extra fields. These are not in the OpenAI specification. The OpenAI SDK sends them in its extra_body argument.

Deep dive

Defaults: where temperature and max_tokens come from

Sampling defaults. to_sampling_params() uses this order for each of seven fields: the request value, else the server default, else the OpenAI default (vllm/entrypoints/openai/chat_completion/protocol.py:735-747). The OpenAI defaults are temperature 1.0, top_p 1.0, top_k 0, min_p 0.0 and penalties that do nothing (vllm/entrypoints/openai/chat_completion/protocol.py:692-700). The server default comes from ModelConfig.get_diff_sampling_param() (vllm/entrypoints/openai/chat_completion/serving.py:181):

  1. With generation_config = "vllm", it reads no file. Otherwise it reads the model's generation config (vllm/config/model.py:1875-1877). The default is "auto" (vllm/config/model.py:358).
  2. --override-generation-config replaces single values (vllm/config/model.py:1879-1880).
  3. It keeps only nine known keys, and it renames max_new_tokens to max_tokens (vllm/config/model.py:1882-1903).
  4. It logs a warning when a generation config file changes the defaults (vllm/config/model.py:1906-1915).

max_tokens. get_max_tokens() returns the smallest of these values that is set (vllm/entrypoints/serve/utils/api_utils.py:234-270):

  • max_model_len − prompt length;
  • the request value, or max_tokens from the server default when the request has none;
  • an override: max_new_tokens from --override-generation-config when --generation-config is auto or vllm, else max_new_tokens from the generation config folder that you give (vllm/entrypoints/openai/chat_completion/serving.py:183-187);
  • a platform limit.

Worked example. max_model_len is 4,096 and the rendered prompt has 13 tokens. A request without max_completion_tokens can make up to 4,096 − 13 = 4,083 tokens. With "max_completion_tokens": 32, it makes at most 32. A prompt of 4,100 tokens raises ValueError("Input length (4100) exceeds model's maximum context length (4096).") (vllm/entrypoints/serve/utils/api_utils.py:248-252). The renderer normally rejects such a prompt before this, with HTTP 400.

Deep dive

The stream, chunk by chunk (worked example)

Start a server with vllm serve Qwen/Qwen3-0.6B. Then send this request (example commands, not in the repo). The option -N turns off the output buffer of curl, so you see each event when it comes.

curl -N http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "Qwen/Qwen3-0.6B",
       "messages": [{"role": "user", "content": "Hi"}],
       "max_completion_tokens": 32,
       "stream": true,
       "stream_options": {"include_usage": true},
       "chat_template_kwargs": {"enable_thinking": false}}'

The reply has this form. The ids, times, text and token counts are illustrative; "..." stands for the object, created and model fields.

data: {"id":"chatcmpl-3f9c...","object":"chat.completion.chunk","created":1761140000,"model":"Qwen/Qwen3-0.6B","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"prompt_token_ids":null,"prompt_text":null}

data: {"id":"chatcmpl-3f9c...",...,"choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null,"token_ids":null}]}

data: {"id":"chatcmpl-3f9c...",...,"choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null,"token_ids":null}]}

...

data: {"id":"chatcmpl-3f9c...",...,"choices":[{"index":0,"delta":{"content":""},"logprobs":null,"finish_reason":"stop","stop_reason":null,"token_ids":null,"weight_versions":null}]}

data: {"id":"chatcmpl-3f9c...",...,"choices":[],"usage":{"prompt_tokens":13,"total_tokens":23,"completion_tokens":10},"system_fingerprint":"vllm-<version>-<hash8>"}

data: [DONE]
  1. The first chunk carries only the role and empty content (vllm/entrypoints/openai/chat_completion/serving.py:590-598).
  2. Each later chunk carries the new text of one engine output, in delta.content. Each chunk is serialized with exclude_unset=True, so only the fields that the code sets appear (vllm/entrypoints/openai/chat_completion/serving.py:497-503).
  3. The last content chunk has finish_reason. Its content is empty because the detokenizer does not decode the end token (vllm/v1/engine/detokenizer.py:107-111).
  4. The usage chunk has choices: [] and usage (vllm/entrypoints/openai/chat_completion/serving.py:854-903). With empty choices, the serializer also drops all None fields (exclude_none=not chunk.choices). The server sends it only with include_usage or --enable-force-include-usage (vllm/entrypoints/serve/utils/api_utils.py:276-285). When usage is on, the fingerprint moves from the last content chunk to this chunk (vllm/entrypoints/openai/chat_completion/serving.py:815-824).
  5. data: [DONE] always ends the stream, also after an error chunk (vllm/entrypoints/openai/chat_completion/serving.py:504-508).
        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"
The outer stream generator: one data: line and a blank line per chunk, then data: [DONE].

Check the numbers. total_tokens = 13 + 10 = 23. The 10 output tokens are nine text tokens and the end token. The 13 prompt tokens come from the Qwen3 chat template around the word "Hi" (inferred, not in the repo).

Keep-alive comments. A proxy can close an idle stream. With --sse-keep-alive-interval N (default 0, off), the server sends the comment line : keep-alive after N idle seconds (vllm/entrypoints/common/cli_args.py:156-160, vllm/entrypoints/serve/utils/sse_keep_alive.py:18-33). SSE clients ignore comment lines.

Deep dive

Chat roles in vLLM

The type of one message is a union of three types (vllm/renderers/chat_utils/types.py:316-320). They are the OpenAI message types, a custom message type and the Harmony message type of gpt-oss models. The custom type has role: Required[str], so vLLM accepts any role name that the chat template knows (vllm/renderers/chat_utils/types.py:284-288). It also has the extra keys reasoning (earlier thinking text) and tools (vllm/renderers/chat_utils/types.py:306-310).

The page Renderers and chat templates: from messages to token ids explains how the renderer finds and applies the template.

Deep dive

Tool calls and reasoning content at the API level

Tool calls. The client sends tools (function definitions). The model writes a tool call as text in its own format. A tool parser turns that text into the OpenAI form message.tool_calls[i] = {id, type: "function", function: {name, arguments}} (vllm/entrypoints/generate/base/protocol.py:316-328).

  1. For tool_choice: "auto", start the server with --enable-auto-tool-choice and --tool-call-parser <name>. Without them, an "auto" request gets the error "auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set (vllm/renderers/online_renderer.py:260-271). The flag without a parser stops start-up (vllm/entrypoints/common/cli_args.py:524-525).
  2. "required" or a named function also needs both flags. The error text names only --tool-call-parser (vllm/renderers/online_renderer.py:272-281). But without --enable-auto-tool-choice, the server loads no tool parser, even when --tool-call-parser is set (vllm/parser/parser_manager.py:34-35).
  3. In a stream, a tool call comes in pieces as delta.tool_calls. Each piece has an index, and the name and the arguments can come in separate chunks (vllm/entrypoints/generate/base/protocol.py:331-342).

Reasoning content. Reasoning models write their thinking before the answer, for example between <think> tags. With --reasoning-parser <name> (vllm/engine/arg_utils.py:1136-1139), vLLM moves that text out of content into the vLLM-specific field reasoning of the message or the delta (vllm/entrypoints/openai/chat_completion/protocol.py:79-80, vllm/entrypoints/generate/base/protocol.py:356-360). Without a parser, the thinking text stays in content. The request field include_reasoning (default true) controls whether the server returns it (vllm/entrypoints/openai/chat_completion/protocol.py:271).

The page Tool and reasoning parsers: from model text to tool_calls and reasoning explains the parsers and how to add your own.

Deep dive

Errors: HTTP 400 and the error body

Every error body has the same shape: {"error": {"message", "type", "param", "code"}} (vllm/entrypoints/serve/engine/protocol.py:79-87). The app registers its handlers in four layers (vllm/entrypoints/serve/exception_handling/register.py:26-42):

  1. FastAPI errors. A body that does not match the pydantic model gives RequestValidationError. The handler answers 400 with at most 10 error entries (vllm/entrypoints/serve/exception_handling/handlers/validation.py:149-195).
  2. vLLM errors. All vLLM error classes derive from VLLMError (vllm/exceptions.py:10-28). One handler, vllm_error_handler(), gets them all (vllm/entrypoints/serve/exception_handling/register.py:32, vllm/entrypoints/serve/exception_handling/handlers/vllm_error.py:18-25). create_error_response() then selects the status from the class. VLLMValidationError becomes 400 BadRequestError with param set (vllm/entrypoints/serve/exception_handling/error_response.py:43-46). VLLMUnprocessableEntityError becomes 422, and a not-found error becomes 404 (vllm/entrypoints/serve/exception_handling/error_response.py:47-54). A GenerationError keeps its own status: 500, or 503 for a retryable error (vllm/entrypoints/serve/exception_handling/error_response.py:60-67, vllm/exceptions.py:131-144). Other server errors become 500 (vllm/entrypoints/serve/exception_handling/error_response.py:68-77).
  3. Raw Python errors. ValueError, TypeError and OverflowError still become 400; NotImplementedError becomes 501 (vllm/entrypoints/serve/exception_handling/error_response.py:80-87).
  4. Anything else becomes 500. The one exception is a Jinja TemplateError from the chat template, which becomes 400 (vllm/entrypoints/serve/exception_handling/error_response.py:88-96).

Which handler answers a validator error. Many request checks are pydantic validators with mode="before" that raise VLLMValidationError. Examples are "Stream options can only be defined when stream=True" (vllm/entrypoints/openai/chat_completion/protocol.py:845-856) and the tool_choice checks (vllm/entrypoints/openai/chat_completion/protocol.py:959-1043). The path of such an error is not the path of a type error:

  1. VLLMValidationError is not a ValueError (vllm/exceptions.py:20, vllm/exceptions.py:28). Pydantic turns only a ValueError or an AssertionError of a validator into a ValidationError, and lets other exceptions pass unchanged (general pydantic rule). The tests of vLLM show this: building the request raises VLLMValidationError itself (tests/tool_use/test_chat_completion_request_validations.py:30-38).
  2. So FastAPI does not make a RequestValidationError, and the validation handler of layer 1 does not run. The exception goes to vllm_error_handler() of layer 2, which calls exception_handler() and then create_error_response() (vllm/entrypoints/serve/exception_handling/handlers/vllm_error.py:24-25, vllm/entrypoints/serve/exception_handling/handlers/exception.py:14-24).
  3. The message is str(exc), and the class adds the parameter name to it (vllm/entrypoints/serve/exception_handling/error_response.py:22-24, vllm/exceptions.py:49-56). The server test expects a BadRequestError (HTTP 400) for this request (tests/entrypoints/openai/chat_completion/test_chat.py:406-415).

The body for "stream": false with stream_options is then (the field order follows ErrorInfo):

HTTP/1.1 400 Bad Request

{"error": {"message": "Stream options can only be defined when `stream=True`. (parameter=stream_options)",
           "type": "BadRequestError", "param": "stream_options", "code": 400}}

A field with a wrong type, for example "n": "x", takes the other path. Pydantic raises a ValidationError, and FastAPI makes a RequestValidationError. The validation handler answers 400 with the message "1 validation error: ..." and a param such as body.n (vllm/entrypoints/serve/exception_handling/handlers/validation.py:167-195). That handler still looks for a VLLMValidationError inside the pydantic errors (vllm/entrypoints/serve/exception_handling/handlers/validation.py:158-165). Because the class is no longer a ValueError, this branch cannot match a validator error any more (derived from the class tree).

What can go wrong#

Why it matters. The API is the contract with your users. Its defaults decide how long and how random the answers are, and its stream format decides what the client sees and when.

Check yourself#

  1. 1
    Which route takes a list of chat messages, and which takes a raw prompt?
    easy
    Show answer

    POST /v1/chat/completions takes messages and applies the chat template (vllm/entrypoints/openai/chat_completion/api_router.py:43-44). POST /v1/completions takes prompt (vllm/entrypoints/openai/completion/protocol.py:53-59).

  2. 2
    How does a streamed response end?
    easy
    Show answer

    With the line data: [DONE] and a blank line (vllm/entrypoints/openai/chat_completion/serving.py:508). With include_usage, a usage chunk with empty choices comes just before it.

  3. 3
    A request sets no temperature. The model's generation_config.json has "temperature": 0.6. Which temperature does vLLM use with the default flags, and with --generation-config vllm?
    medium
    Show answer

    0.6 with the default auto, because the server default comes before the OpenAI default (vllm/entrypoints/openai/chat_completion/protocol.py:741-747, vllm/config/model.py:1875-1877). With vllm, no file is read, so the OpenAI default 1.0 applies (vllm/entrypoints/openai/chat_completion/protocol.py:692-700).

  4. 4
    max_model_len is 8,192, the prompt has 8,000 tokens, the request asks for max_completion_tokens: 500, and the generation config has no max_new_tokens. How many tokens can the answer have?
    hard
    Show answer

    192. get_max_tokens() takes the minimum of 8,192 − 8,000 = 192 and 500 (and a platform limit, if any) (vllm/entrypoints/serve/utils/api_utils.py:253-270). The request does not fail; the answer ends with finish_reason: "length" after 192 tokens.

  5. 5
    A client gets HTTP 200, then a stream that stops with an error. Why is the status not 500, and where is the error?
    hard
    Show answer

    The server sends the status with the first byte of the stream. A later error cannot change it. The stream generator catches the exception, sends it as one more data: chunk with an error object, and then sends data: [DONE] (vllm/entrypoints/openai/chat_completion/serving.py:504-508).