diff --git a/components/src/dynamo/frontend/prepost.py b/components/src/dynamo/frontend/prepost.py index 003813e99ce6..0b92fdcc6cb0 100644 --- a/components/src/dynamo/frontend/prepost.py +++ b/components/src/dynamo/frontend/prepost.py @@ -136,8 +136,19 @@ def _prepare_request( ) else None ) - chat_template_kwargs = dict(request_for_sampling.chat_template_kwargs or {}) - chat_template_kwargs["reasoning_effort"] = request_for_sampling.reasoning_effort + # serde's `alias` is deserialize-only, so pythonize emits the Rust field + # name `chat_template_args`; read it too or client kwargs are dropped. + raw_template_args = ( + request.get("chat_template_args") if isinstance(request, dict) else None + ) + chat_template_kwargs = dict( + request_for_sampling.chat_template_kwargs or raw_template_args or {} + ) + # Don't let an absent top-level field clobber a nested reasoning_effort. + if request_for_sampling.reasoning_effort is not None: + chat_template_kwargs["reasoning_effort"] = request_for_sampling.reasoning_effort + else: + chat_template_kwargs.setdefault("reasoning_effort", None) # Mistral warns that tokenize=False is unsafe for chat templates. is_mistral_tokenizer = ( @@ -154,14 +165,15 @@ def _prepare_request( chat_params = ChatParams( chat_template=request_for_sampling.chat_template, chat_template_content_format="auto", - chat_template_kwargs=dict( - add_generation_prompt=request_for_sampling.add_generation_prompt, - continue_final_message=request_for_sampling.continue_final_message, - tools=tool_dicts, - documents=request_for_sampling.documents, - tokenize=tokenize_in_template, + # Renderer-managed keys last so a nested duplicate can't raise TypeError. + chat_template_kwargs={ **chat_template_kwargs, - ), + "add_generation_prompt": request_for_sampling.add_generation_prompt, + "continue_final_message": request_for_sampling.continue_final_message, + "tools": tool_dicts, + "documents": request_for_sampling.documents, + "tokenize": tokenize_in_template, + }, ) return ( diff --git a/components/src/dynamo/frontend/tests/test_vllm_processor_unit.py b/components/src/dynamo/frontend/tests/test_vllm_processor_unit.py index ed54cbe82d9b..5ebdc355edb7 100644 --- a/components/src/dynamo/frontend/tests/test_vllm_processor_unit.py +++ b/components/src/dynamo/frontend/tests/test_vllm_processor_unit.py @@ -155,6 +155,88 @@ def test_no_tools_in_request(self, tokenizer): ), "No tools in request should produce None tools in template" +class TestChatTemplateArgsPassthrough: + """Per-request chat template kwargs must survive into the rendered template. + + pythonize serializes the request field under its Rust name + ``chat_template_args`` (serde ``alias`` is deserialize-only), so the vLLM + processor must read that key, not only vLLM's native ``chat_template_kwargs``. + """ + + def test_chat_template_args_reaches_template(self, tokenizer): + """Kwargs keyed as chat_template_args (the pythonize'd key) reach the template.""" + _, _, _, _, chat_params = _prepare_request( + { + "model": MODEL, + "messages": [{"role": "user", "content": "Hello"}], + "chat_template_args": {"enable_thinking": False}, + }, + tokenizer=tokenizer, + tool_parser_class=None, + ) + assert ( + chat_params.chat_template_kwargs.get("enable_thinking") is False + ), "chat_template_args must be forwarded to the chat template" + + def test_chat_template_kwargs_native_key_still_works(self, tokenizer): + """The vLLM-native chat_template_kwargs key keeps working.""" + _, _, _, _, chat_params = _prepare_request( + { + "model": MODEL, + "messages": [{"role": "user", "content": "Hello"}], + "chat_template_kwargs": {"enable_thinking": False}, + }, + tokenizer=tokenizer, + tool_parser_class=None, + ) + assert ( + chat_params.chat_template_kwargs.get("enable_thinking") is False + ), "native chat_template_kwargs must be forwarded to the chat template" + + def test_nested_reasoning_effort_is_not_clobbered(self, tokenizer): + """A reasoning_effort nested in template kwargs survives the top-level default.""" + _, _, _, _, chat_params = _prepare_request( + { + "model": MODEL, + "messages": [{"role": "user", "content": "Hello"}], + "chat_template_args": {"reasoning_effort": "high"}, + }, + tokenizer=tokenizer, + tool_parser_class=None, + ) + assert ( + chat_params.chat_template_kwargs.get("reasoning_effort") == "high" + ), "nested reasoning_effort must not be overwritten by an absent top-level field" + + def test_top_level_reasoning_effort_wins_over_nested(self, tokenizer): + """An explicit top-level reasoning_effort overrides a nested one.""" + _, _, _, _, chat_params = _prepare_request( + { + "model": MODEL, + "messages": [{"role": "user", "content": "Hello"}], + "reasoning_effort": "low", + "chat_template_args": {"reasoning_effort": "high"}, + }, + tokenizer=tokenizer, + tool_parser_class=None, + ) + assert chat_params.chat_template_kwargs.get("reasoning_effort") == "low" + + def test_reserved_render_key_in_template_args_does_not_crash(self, tokenizer): + """A renderer-reserved key nested in template kwargs must not raise TypeError.""" + _, _, _, _, chat_params = _prepare_request( + { + "model": MODEL, + "messages": [{"role": "user", "content": "Hello"}], + "chat_template_args": {"documents": [{"text": "doc"}]}, + }, + tokenizer=tokenizer, + tool_parser_class=None, + ) + # Renderer-managed value wins over the client's nested key (no crash either). + assert chat_params.chat_template_kwargs["documents"] is None + + class TestMultimodalFeatureMetadata: def _feature( self, modality, mm_hash, offset, length, data=_DEFAULT_MM_DATA, is_embed=None