[Bugfix] Detect OpenAI content format when message.content is passed through macro parameters (#53824)
Signed-off-by: wuhangxian <1391938827@qq.com> Signed-off-by: wuhangxian <wuhangxian@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: wuhangxian <wuhangxian@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
wuhangxian
mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent
cce50657b7
commit
56d001faf0
@@ -11,6 +11,7 @@ from vllm.exceptions import VLLMValidationError
|
||||
from vllm.renderers.hf import (
|
||||
_consolidate_system_messages,
|
||||
_convert_developer_to_system,
|
||||
_detect_content_format,
|
||||
_detect_developer_role_support,
|
||||
_get_hf_base_chat_template_params,
|
||||
_template_error_reason,
|
||||
@@ -361,6 +362,66 @@ def test_resolve_chat_template_kwargs_with_template_name():
|
||||
assert "unknown_param" not in resolved
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("chat_template", "expected_format"),
|
||||
[
|
||||
(
|
||||
"""
|
||||
{% macro render_message(role, message_content) %}
|
||||
{% for item in message_content %}{{ item['type'] }}{% endfor %}
|
||||
{% endmacro %}
|
||||
{% for message in messages %}
|
||||
{{ render_message(message['role'], message['content']) }}
|
||||
{% endfor %}
|
||||
""",
|
||||
"openai",
|
||||
),
|
||||
(
|
||||
"""
|
||||
{% macro render_message(role, message_content) %}
|
||||
{% for item in message_content %}{{ item['type'] }}{% endfor %}
|
||||
{% endmacro %}
|
||||
{% for message in messages %}
|
||||
{{ render_message(
|
||||
role=message['role'],
|
||||
message_content=message['content'],
|
||||
) }}
|
||||
{% endfor %}
|
||||
""",
|
||||
"openai",
|
||||
),
|
||||
(
|
||||
"""
|
||||
{% for message in messages %}
|
||||
{% for item in message['content'] %}{{ item['type'] }}{% endfor %}
|
||||
{% endfor %}
|
||||
""",
|
||||
"openai",
|
||||
),
|
||||
(
|
||||
"""
|
||||
{% macro render_message(content) %}
|
||||
{% for item in content %}{{ item['type'] }}{% endfor %}
|
||||
{% endmacro %}
|
||||
{% for message in messages %}
|
||||
{{ render_message(content=['text only']) }}
|
||||
{% endfor %}
|
||||
""",
|
||||
"string",
|
||||
),
|
||||
(
|
||||
"""
|
||||
{% for message in messages %}{{ message['content'] }}{% endfor %}
|
||||
""",
|
||||
"string",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_detect_content_format(chat_template, expected_format):
|
||||
"""Detect content format when content is passed through a macro."""
|
||||
assert _detect_content_format(chat_template, default="string") == expected_format
|
||||
|
||||
|
||||
# NOTE: Qwen2-Audio default chat template is specially defined inside
|
||||
# processor class instead of using `tokenizer_config.json`
|
||||
@pytest.mark.parametrize(
|
||||
@@ -373,9 +434,11 @@ def test_resolve_chat_template_kwargs_with_template_name():
|
||||
("fixie-ai/ultravox-v0_5-llama-3_2-1b", "string"),
|
||||
("Qwen/Qwen2-Audio-7B-Instruct", "openai"),
|
||||
("meta-llama/Llama-Guard-3-1B", "openai"),
|
||||
("XiaomiMiMo/MiMo-V2.5-Pro", "openai"),
|
||||
],
|
||||
)
|
||||
def test_resolve_content_format_hf_defined(model, expected_format):
|
||||
"""Detect the chat template content format for built-in HF models."""
|
||||
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
|
||||
model_info.check_available_online(on_fail="skip")
|
||||
|
||||
|
||||
+55
-3
@@ -397,10 +397,46 @@ def _iter_nodes_assign_messages_item(root: jinja2.nodes.Node):
|
||||
|
||||
|
||||
def _iter_nodes_assign_content_item(root: jinja2.nodes.Node):
|
||||
"""Yield loops that iterate over message content or macro-bound content."""
|
||||
message_varnames = [
|
||||
varname for _, varname in _iter_nodes_assign_messages_item(root)
|
||||
]
|
||||
|
||||
# Track macro parameters that receive message.content as an argument.
|
||||
# Some templates pass message.content through a macro parameter whose
|
||||
# name is not literally "content".
|
||||
macro_content_params_by_loop: dict[int, set[str]] = {}
|
||||
loops_in_macros: set[int] = set()
|
||||
for macro_node in root.find_all(jinja2.nodes.Macro):
|
||||
macro_param_names = {arg.name for arg in macro_node.args}
|
||||
macro_content_params: set[str] = set()
|
||||
for call_node in root.find_all(jinja2.nodes.Call):
|
||||
if (
|
||||
isinstance(call_node.node, jinja2.nodes.Name)
|
||||
and call_node.node.name == macro_node.name
|
||||
):
|
||||
for i, arg in enumerate(call_node.args):
|
||||
if i < len(macro_node.args) and any(
|
||||
_is_var_or_elems_access(arg, varname, "content")
|
||||
for varname in message_varnames
|
||||
):
|
||||
macro_content_params.add(macro_node.args[i].name)
|
||||
for kwarg in call_node.kwargs:
|
||||
if (
|
||||
isinstance(kwarg, jinja2.nodes.Keyword)
|
||||
and kwarg.key in macro_param_names
|
||||
and any(
|
||||
_is_var_or_elems_access(kwarg.value, varname, "content")
|
||||
for varname in message_varnames
|
||||
)
|
||||
):
|
||||
macro_content_params.add(kwarg.key)
|
||||
|
||||
for loop_ast in macro_node.find_all(jinja2.nodes.For):
|
||||
loops_in_macros.add(id(loop_ast))
|
||||
if macro_content_params:
|
||||
macro_content_params_by_loop[id(loop_ast)] = macro_content_params
|
||||
|
||||
# Search for {%- for content in message['content'] -%} loops
|
||||
# or {%- for item in content -%} loops
|
||||
for loop_ast in root.find_all(jinja2.nodes.For):
|
||||
@@ -412,10 +448,26 @@ def _iter_nodes_assign_content_item(root: jinja2.nodes.Node):
|
||||
assert isinstance(loop_target, jinja2.nodes.Name)
|
||||
yield loop_ast, loop_target.name
|
||||
break
|
||||
else:
|
||||
macro_content_params_for_loop = macro_content_params_by_loop.get(
|
||||
id(loop_ast)
|
||||
)
|
||||
if (
|
||||
isinstance(loop_iter, jinja2.nodes.Name)
|
||||
and macro_content_params_for_loop is not None
|
||||
and loop_iter.name in macro_content_params_for_loop
|
||||
):
|
||||
assert isinstance(loop_target, jinja2.nodes.Name)
|
||||
yield loop_ast, loop_target.name
|
||||
continue
|
||||
|
||||
if isinstance(loop_iter, jinja2.nodes.Name) and loop_iter.name == "content":
|
||||
assert isinstance(loop_target, jinja2.nodes.Name)
|
||||
yield loop_ast, loop_target.name
|
||||
if (
|
||||
id(loop_ast) not in loops_in_macros
|
||||
and isinstance(loop_iter, jinja2.nodes.Name)
|
||||
and loop_iter.name == "content"
|
||||
):
|
||||
assert isinstance(loop_target, jinja2.nodes.Name)
|
||||
yield loop_ast, loop_target.name
|
||||
|
||||
|
||||
def _try_extract_ast(chat_template: str) -> jinja2.nodes.Template | None:
|
||||
|
||||
Reference in New Issue
Block a user