-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Inference: Bring chat completions API inline with vllm/official openAI spec #5276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9773d0e
c321ab6
6042300
26d5843
8f9657c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,22 +323,6 @@ def _sanitize_tools_for_template(tools): | |
| return sanitized | ||
|
|
||
|
|
||
| def _reconstruct_reasoning_content(messages: list[dict]) -> list[dict]: | ||
| """Reconstruct <think> tags from reasoning_content fields on assistant messages. | ||
|
|
||
| For parity with vLLM, assistant messages may carry reasoning in the reasoning_content field. | ||
| Before applying the chat template, we must inline those tags back into content. | ||
| """ | ||
| for message in messages: | ||
| if message.get("role") != "assistant": | ||
| continue | ||
| reasoning_content = message.pop("reasoning_content", None) | ||
| if reasoning_content is not None: | ||
| content = message.get("content") or "" | ||
| message["content"] = f"<think>{reasoning_content}</think>{content}" | ||
| return messages | ||
|
|
||
|
|
||
| def _replace_prefix_tokens( | ||
| eos_token_id, | ||
| previous_turn_token_ids, | ||
|
|
@@ -462,7 +446,6 @@ async def chat_completions(): | |
| if not isinstance(messages, list): | ||
| return Response("'messages' must be a list", status=400) | ||
| template_messages = _sanitize_messages_for_template(messages) | ||
| template_messages = _reconstruct_reasoning_content(template_messages) | ||
| template_tools = _sanitize_tools_for_template(tools) | ||
|
|
||
| try: | ||
|
|
@@ -592,6 +575,7 @@ async def chat_completions(): | |
| prompt_tokens = [tokenizer.bos] + prompt_tokens | ||
|
|
||
| max_tokens = req.get("max_completion_tokens", None) or req.get("max_tokens", None) | ||
| ignore_eos = bool(req.get("ignore_eos", False)) | ||
|
|
||
| sampling_params = SamplingParams( | ||
| temperature=temperature, | ||
|
|
@@ -602,6 +586,7 @@ async def chat_completions(): | |
| num_tokens_to_generate=(int(max_tokens) if max_tokens is not None else None), | ||
| skip_prompt_log_probs=skip_prompt_log_probs, | ||
| add_BOS=add_BOS, | ||
| termination_id=-1 if ignore_eos else None, | ||
| ) | ||
| except ValueError as e: | ||
| return Response(f"Invalid sampling parameter: {e}", status=400) | ||
|
|
@@ -663,6 +648,14 @@ async def chat_completions(): | |
| total_completion_tokens = 0 | ||
| prompt_tokens_counts = [] | ||
|
|
||
| prevent_retokenization = req.get("prevent_retokenization", True) | ||
| # return_tokenized_data controls whether prompt/generation token ids are | ||
| # included in the response. It is independent of prevent_retokenization | ||
| # (a client may want token ids without prevent_retokenization, or vice versa), | ||
| # but prevent_retokenization implicitly requires token ids so the client | ||
| # can echo them back next turn. | ||
| return_tokenized_data = req.get("return_tokenized_data", False) or prevent_retokenization | ||
| return_raw_text = req.get("return_raw_text", False) | ||
| request_idx = 0 | ||
| for result_item in batch_results: | ||
| result = unwrap_serialized_tensors(result_item) | ||
|
|
@@ -736,9 +729,14 @@ async def chat_completions(): | |
| if "reasoning" in metadata: | ||
| message["reasoning_content"] = metadata["reasoning"] | ||
|
|
||
| # Replicate data in the message field for compatibility. | ||
| message["prompt_token_ids"] = result["prompt_tokens"] | ||
| message["generation_token_ids"] = result["generated_tokens"] | ||
| if return_tokenized_data: | ||
| message["prompt_token_ids"] = result["prompt_tokens"] | ||
| message["generation_token_ids"] = result["generated_tokens"] | ||
| if return_raw_text: | ||
| prompt_str = tokenizer.detokenize(result["prompt_tokens"]) | ||
| message["raw_text"] = prompt_str + text_output | ||
| # Small RL/debug scalars (a few bytes each); harmless to keep for | ||
| # NeMo-RL compatibility. | ||
| message["generation_log_probs"] = result.get("generated_log_probs", []) | ||
|
Comment on lines
+738
to
740
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION Naming] The comment "Small RL/debug scalars (a few bytes each)" accurately describes Why it matters: a wrong comment is worse than none — a future reader trimming the payload may skip Suggested fix: move the comment below
Comment on lines
+738
to
740
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION Naming] The comment "Small RL/debug scalars (a few bytes each)" accurately describes Why it matters: a wrong comment is worse than none — a future reader trimming the payload may skip Suggested fix: move the comment below |
||
| message["policy_epoch"] = result["policy_epoch"] | ||
| message["kv_cache_epoch"] = result["kv_cache_epoch"] | ||
|
|
@@ -759,15 +757,13 @@ async def chat_completions(): | |
| else: | ||
| finish_reason = "stop" | ||
|
|
||
| # Choice-level prompt/generation_token_ids, generation_log_probs and | ||
| # raw_text were duplicates of message-level data (or reconstructable); | ||
| # dropped to match vLLM's response shape and cut payload size. | ||
| choice_data = { | ||
| "index": request_idx, | ||
| "message": message, | ||
| "prompt_token_ids": result["prompt_tokens"], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing this from the choice data and only having it in the message data is probably good to remove reduncancy, but this will require a corresponding change in MRL without NemoGym.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can make this change in this PR.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| "generation_token_ids": result["generated_tokens"], | ||
| "generation_log_probs": result.get("generated_log_probs", []), | ||
| "raw_text": result["prompt"] + result["generated_text"], | ||
| # 'logprobs' in chat API is an object containing 'content' | ||
| # "logprobs": {"content": logprobs_content} if logprobs_content else None, | ||
| "logprobs": {"content": logprobs_content} if return_log_probs else None, | ||
| "finish_reason": finish_reason, | ||
| } | ||
|
Comment on lines
+760
to
769
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility] This removes Why it matters: silent breakage of an external client contract is hard to diagnose from the server side. Suggestion: confirm NeMo-RL (and any other downstream) reads the message-level fields, and if this endpoint is externally consumed, call out the shape change in the PR description / release notes. No code change required if downstreams are already aligned.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this deviating from the official openAI spec?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, these are not part of the spec.
Comment on lines
+760
to
769
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility] This removes Why it matters: silent breakage of an external client contract is hard to diagnose from the server side. Suggestion: confirm the NeMo-RL (and any other downstream) client reads the message-level fields, and if this endpoint is externally consumed, call out the shape change in the PR description / release notes so consumers can migrate. No code change required if downstreams are already aligned. |
||
|
|
@@ -782,7 +778,7 @@ async def chat_completions(): | |
| ] | ||
|
|
||
| choices.append(choice_data) | ||
| if choice_data["generation_log_probs"] is None: | ||
| if result.get("generated_log_probs") is None: | ||
| logger.warning( | ||
| "Generation log probs is None for request:\n%s", | ||
| json.dumps(_redact_token_id_lists_for_logging(result), indent=4), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't really true. MRL uses prompt/generation token ids but doesn't always want to prevent retokenization. Getting the token ids back is a separate feature than this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's probably better to gate this off of a new switch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can have a flag called "return_tokenized_data", defaulting to False to match out-of-the-box vLLM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed below. I have added a "return_tokenized_data" flag/