Repository navigation
Conversation
The sidecar declared choices, finish_reason, message, index, delta and the assistant role as unexported constants of its own package, while the coordinator needs the same names to build a chat completions body. Two names for one wire field diverge the moment either is renamed, and goconst does not see a duplicate across packages. The names belong to the OpenAI-compatible response schema rather than to the sidecar, so they move to pkg/common/request, which already owns the request field names, and the sidecar reads them from there. No behavior change. Signed-off-by: roytman <roytman@il.ibm.com>
The decode step derived its wire format from the client's path and rejected use_openai_format, so a tokens-in pipeline sent its last leg as the client's own API while encode and prefill used /inference/v1/generate. The prompt was then tokenized twice, once by the renderer for prefix-cache scoring and again by the decode worker. Decode now resolves its format through resolveFormat like encode and prefill, so use_openai_format: false addresses every leg at the generate route. Three things follow from that route's contract: output_mode asks the worker what to return beside the token IDs: text for the detokenized text, derender for the parsed assistant message. A worker that predates the field answers with token IDs only, which carries no text for the client, so a response that does not echo the requested mode is an error rather than an answer. vLLM refuses derender together with stream: true, so that leg is sent unstreamed. sampling_params is required on the generate route and has no server-side default. A client whose output cap sits at the top level of another API's body has it moved under sampling_params, or the worker falls back to its own max_tokens default and truncates the answer. wrap_chat_completion rebuilds the chat completions envelope for a client that called /v1/chat/completions, since vLLM serves output_mode on the generate envelope rather than returning an OpenAI one. Default false. A streamed answer is a sequence of chat.completion.chunk frames this step does not assemble, so a streaming request is forwarded unchanged; the other client APIs keep the envelope their own schema declares. conditional-decode still derives its format from the client's path: it probes that route with the body the client sent. Closes llm-d#3689 Signed-off-by: roytman <roytman@il.ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What type of PR is this?
/kind feature
What this PR does / why we need it:
The
decodestep derived its wire format from the client's path and rejecteduse_openai_format, so a tokens-in pipeline sent its last leg as the client's own API whileencodeandprefillused/inference/v1/generate. The prompt was tokenized twice: once by the renderer for prefix-cache scoring, and again by the decode worker. Decode is also where the chat template and the tool and reasoning parsers run, so keeping it on the OpenAI route tied parser behavior to the GPU worker image.Decode now resolves its format through
resolveFormatlikeencodeandprefill, sopipeline.use_openai_format: falseaddresses every leg at the generate route. Three things follow from that route's contract.output_modeasks the worker what to return beside the token IDs:textfor the detokenized text,derenderfor the parsed assistant message (content,reasoning,tool_calls). A worker predating the field ignores it and answers with token IDs only, which carries no text for the client, so a response that does not echo the requested mode is an error rather than an answer. vLLM refusesderendertogether withstream: true, so that leg is sent unstreamed and a streaming client receives its answer in a single frame.sampling_paramsis required on the generate route and has no server-side default. A client whose output cap sits at the top level of another API's body (max_tokens,max_completion_tokens,max_output_tokens) has it moved undersampling_params; without the move the worker falls back to its ownmax_tokensdefault of 16 and truncates the answer.wrap_chat_completionrebuilds the chat completions envelope for a client that called/v1/chat/completions, since vLLM servesoutput_modeon the generate envelope rather than returning an OpenAI one (the response schema would otherwise depend on a request field, which the upstream RFC rejected). Defaultfalse. The message moves across whole, so thereasoningandtool_callsaderenderresponse carries survive without this package modeling them.conditional-decodestill derives its format from the client's path: it probes that route with the body the client sent, souse_openai_formatcannot apply to it and the step keeps rejecting the key.The first commit is a no-behavior-change refactor: the sidecar declared
choices,finish_reason,message,index,deltaand the assistant role as unexported constants of its own package, and the coordinator needs the same names to build a chat completions body. They move topkg/common/request, which already owns the request field names.Scope and limits
/v1/chat/completionsonly for the envelope transform. A streamed answer is a sequence ofchat.completion.chunkframes this step does not assemble, so a streaming request is forwarded unchanged. Upstream streamingderender([Frontend] Stream derender on /inference/v1/generate vllm-project/vllm#63103) is still open./v1/completions,/v1/responsesand/v1/messageskeep the envelope their own schema declares. Those transforms belong in their own issues.output_mode: tokensis refused in config: decode streams its response to the client, so that mode would answer with token IDs and no text.Required vLLM version
Both modes need a worker that loads a tokenizer; vLLM answers 400 under
--tokens-onlyor--skip-tokenizer-init, andderenderon a worker with a tool or reasoning parser configured also requires aparse_contextfrom/render, which this pipeline does not send.output_mode, valuetextoutput_mode: derenderoutput_mode: textrequires vLLM >= v0.32.0;output_mode: derenderrequires vLLM > v0.32.0 (main, or v0.33.0 once released). The e2e lane still pins an older renderer image, so it does not exercise either mode against real vLLM yet (#3620).
Test plan
Unit tests, all passing:
use_openai_formatand rewrites a chat-completions request to the generate route with the rendered token IDs and the client's cap moved undersamp ling_paramsoutput_modedefaults totext, acceptsderender, and refusestokensand any unknown value at startupderenderresponse, preservingreasoningandtool_calls, and from atextresponse by building the messagefrom its text
request_id,output_mode, the KV and EC transfer params, prompt token IDs, metrics, per-choice token IDs and weight versions)
output_modeis an error rather than an empty answerconditional-decodestill rejectsuse_openai_formatVerified end to end on a CoreWeave H200 cluster (
Qwen3-VL-32B-Instruct, TP=2, oneDisaggregatedSet, 10 pods,kv-nixlandec-nixl):/inference/v1/generate, confirmed from the coordinator's own step logs and from the model servers' access logsoutput_modeon the wire, captured at TRACE/v1/chat/completionsrequest returns achat.completionbody that the officialopenaiPython SDK (2.48.0) parses as a typedChatCompletion, with no generate-only fields present
/inference/v1/generateclient still receives the generate envelope unchangedderenderwithstream: trueis refused by the worker with 400, and the step's unstreamed leg avoids itWhich issue(s) this PR fixes:
Fixes #3689
Release note: