Files
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

913 lines
30 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
from vllm.config import ModelConfig
from vllm.entrypoints.chat_utils import load_chat_template
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.renderers.hf import (
_consolidate_system_messages,
_convert_developer_to_system,
_detect_developer_role_support,
_get_hf_base_chat_template_params,
_try_extract_ast,
resolve_chat_template,
resolve_chat_template_content_format,
resolve_chat_template_kwargs,
safe_apply_chat_template,
)
from vllm.tokenizers import get_tokenizer
from ..models.registry import HF_EXAMPLE_MODELS
from ..utils import VLLM_PATH
EXAMPLES_DIR = VLLM_PATH / "examples"
chatml_jinja_path = VLLM_PATH / "examples/template_chatml.jinja"
assert chatml_jinja_path.exists()
# Define models, templates, and their corresponding expected outputs
MODEL_TEMPLATE_GENERATION_OUTPUT = [
(
"facebook/opt-125m",
chatml_jinja_path,
True,
False,
"""<|im_start|>user
Hello<|im_end|>
<|im_start|>assistant
Hi there!<|im_end|>
<|im_start|>user
What is the capital of<|im_end|>
<|im_start|>assistant
""",
),
(
"facebook/opt-125m",
chatml_jinja_path,
False,
False,
"""<|im_start|>user
Hello<|im_end|>
<|im_start|>assistant
Hi there!<|im_end|>
<|im_start|>user
What is the capital of""",
),
(
"facebook/opt-125m",
chatml_jinja_path,
False,
True,
"""<|im_start|>user
Hello<|im_end|>
<|im_start|>assistant
Hi there!<|im_end|>
<|im_start|>user
What is the capital of<|im_end|>
<|im_start|>assistant
The capital of""",
),
]
TEST_MESSAGES = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "What is the capital of"},
]
ASSISTANT_MESSAGE_TO_CONTINUE = {"role": "assistant", "content": "The capital of"}
def test_load_chat_template():
# Testing chatml template
template_content = load_chat_template(chat_template=chatml_jinja_path)
# Test assertions
assert template_content is not None
# Hard coded value for template_chatml.jinja
assert (
template_content
== """{% for message in messages %}{{'<|im_start|>' + message['role'] + '\\n' + message['content']}}{% if (loop.last and add_generation_prompt) or not loop.last %}{{ '<|im_end|>' + '\\n'}}{% endif %}{% endfor %}
{% if add_generation_prompt and messages[-1]['role'] != 'assistant' %}{{ '<|im_start|>assistant\\n' }}{% endif %}""" # noqa: E501
)
def test_no_load_chat_template_filelike():
# Testing chatml template
template = "../../examples/does_not_exist"
with pytest.raises(ValueError, match="looks like a file path"):
load_chat_template(chat_template=template)
def test_no_load_chat_template_literallike():
# Testing chatml template
template = "{{ messages }}"
template_content = load_chat_template(chat_template=template)
assert template_content == template
@pytest.mark.parametrize(
"model",
[
"Qwen/Qwen2-VL-2B-Instruct", # chat_template is of type str
"NousResearch/Hermes-3-Llama-3.1-8B", # chat_template is of type dict
],
)
@pytest.mark.parametrize("use_tools", [True, False])
def test_resolve_chat_template(sample_json_schema, model, use_tools):
"""checks that chat_template is a dict type for HF models."""
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
model_info.check_available_online(on_fail="skip")
model_config = ModelConfig(
model,
tokenizer=model_info.tokenizer or model,
tokenizer_mode=model_info.tokenizer_mode,
revision=model_info.revision,
trust_remote_code=model_info.trust_remote_code,
hf_overrides=model_info.hf_overrides,
skip_tokenizer_init=model_info.require_embed_inputs,
enable_prompt_embeds=model_info.require_embed_inputs,
enable_mm_embeds=model_info.require_embed_inputs,
enforce_eager=model_info.enforce_eager,
dtype=model_info.dtype,
)
# Build the tokenizer
tokenizer = get_tokenizer(
model,
trust_remote_code=model_config.trust_remote_code,
)
tools = (
[
{
"type": "function",
"function": {
"name": "dummy_function_name",
"description": "This is a dummy function",
"parameters": sample_json_schema,
},
}
]
if use_tools
else None
)
# Test detecting the tokenizer's chat_template
chat_template = resolve_chat_template(
tokenizer,
chat_template=None,
tools=tools,
model_config=model_config,
)
assert isinstance(chat_template, str)
@pytest.mark.parametrize(
"model, expected_kwargs",
[
(
"Qwen/Qwen2-VL-2B-Instruct",
{
"add_vision_id",
"add_generation_prompt",
"continue_final_message",
"tools",
},
),
(
"Qwen/Qwen3-8B",
{
"enable_thinking",
"add_generation_prompt",
"continue_final_message",
"tools",
},
),
],
)
def test_resolve_chat_template_kwargs(sample_json_schema, model, expected_kwargs):
"""checks that chat_template is a dict type for HF models."""
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
model_info.check_available_online(on_fail="skip")
tools = [
{
"type": "function",
"function": {
"name": "dummy_function_name",
"description": "This is a dummy function",
"parameters": sample_json_schema,
},
}
]
chat_template_kwargs = {
# both unused
"unused_kwargs_1": 123,
"unused_kwargs_2": "abc",
# should not appear
"chat_template": "{% Hello world! %}",
"tokenize": True,
# used by tokenizer
"continue_final_message": True,
"tools": tools,
# both used by Qwen2-VL and Qwen3
"add_generation_prompt": True,
# only used by Qwen2-VL
"add_vision_id": True,
# only used by Qwen3
"enable_thinking": True,
}
model_config = ModelConfig(
model,
tokenizer=model_info.tokenizer or model,
tokenizer_mode=model_info.tokenizer_mode,
revision=model_info.revision,
trust_remote_code=model_info.trust_remote_code,
hf_overrides=model_info.hf_overrides,
skip_tokenizer_init=model_info.require_embed_inputs,
enable_prompt_embeds=model_info.require_embed_inputs,
enable_mm_embeds=model_info.require_embed_inputs,
enforce_eager=model_info.enforce_eager,
dtype=model_info.dtype,
)
# Build the tokenizer
tokenizer = get_tokenizer(
model,
trust_remote_code=model_config.trust_remote_code,
)
# Test detecting the tokenizer's chat_template
chat_template = resolve_chat_template(
tokenizer,
chat_template=None,
tools=tools,
model_config=model_config,
)
with pytest.raises(
ValueError, match="Found unexpected chat template kwargs from request"
):
# should raise error if `chat_template_kwargs` contains
# `chat_template` or `tokenize`
resolve_chat_template_kwargs(
tokenizer,
chat_template=chat_template,
chat_template_kwargs=chat_template_kwargs,
)
resolved_chat_template_kwargs = resolve_chat_template_kwargs(
tokenizer,
chat_template=chat_template,
chat_template_kwargs=chat_template_kwargs,
raise_on_unexpected=False,
)
assert set(resolved_chat_template_kwargs.keys()) == expected_kwargs
# Additional test: Verify HF base parameters work with **kwargs tokenizers
# This validates the fix for tokenizers like Kimi K2 that use **kwargs
# to receive standard HuggingFace parameters instead of declaring them explicitly
hf_base_params = _get_hf_base_chat_template_params()
# Verify common HF parameters are in the base class
assert {"add_generation_prompt", "tools", "continue_final_message"}.issubset(
hf_base_params
), f"Expected HF base params not found in {hf_base_params}"
# Test with a mock tokenizer that uses **kwargs (like Kimi K2)
class MockTokenizerWithKwargs:
def apply_chat_template(self, conversation, **kwargs):
return "mocked_output"
mock_tokenizer = MockTokenizerWithKwargs()
mock_kwargs = {
"add_generation_prompt": True,
"tools": tools,
"continue_final_message": False,
"unknown_param": "should_be_filtered",
}
resolved_mock = resolve_chat_template_kwargs(
mock_tokenizer, chat_template, mock_kwargs, raise_on_unexpected=False
)
# HF base params should pass through even with **kwargs tokenizer
assert "add_generation_prompt" in resolved_mock
assert "tools" in resolved_mock
assert "continue_final_message" in resolved_mock
# Unknown params should be filtered out
assert "unknown_param" not in resolved_mock
def test_resolve_chat_template_resolves_name():
"""When chat_template is a name, resolve_chat_template should return
the actual Jinja content so that kwargs detection works correctly."""
from unittest.mock import MagicMock
jinja_content = "{{ messages }}{% if tools %}{{ tools }}{% endif %}"
tokenizer = MagicMock()
tokenizer.get_chat_template.return_value = jinja_content
model_config = MagicMock()
result = resolve_chat_template(
tokenizer,
chat_template="tool_use",
tools=None,
model_config=model_config,
)
assert result == jinja_content
tokenizer.get_chat_template.assert_called_once_with("tool_use", tools=None)
def test_resolve_chat_template_kwargs_with_template_name():
"""Ensures template kwargs are not silently dropped when chat_template
was originally a template name that has been resolved to Jinja content."""
from unittest.mock import MagicMock
jinja_content = (
"{% for m in messages %}{{ m }}{% endfor %}"
"{% if tools %}{{ tools }}{% endif %}"
"{% if documents %}{{ documents }}{% endif %}"
)
tokenizer = MagicMock()
tokenizer.apply_chat_template = MagicMock()
kwargs = {
"tools": [{"type": "function", "function": {"name": "f"}}],
"documents": [{"title": "doc"}],
"unknown_param": "should be dropped",
}
resolved = resolve_chat_template_kwargs(
tokenizer,
chat_template=jinja_content,
chat_template_kwargs=kwargs,
raise_on_unexpected=False,
)
# template vars "tools" and "documents" should be preserved
assert "tools" in resolved
assert "documents" in resolved
# unknown param should be filtered
assert "unknown_param" not in resolved
# NOTE: Qwen2-Audio default chat template is specially defined inside
# processor class instead of using `tokenizer_config.json`
@pytest.mark.parametrize(
("model", "expected_format"),
[
("microsoft/Phi-3.5-vision-instruct", "string"),
("Qwen/Qwen2-VL-2B-Instruct", "openai"),
("Qwen/Qwen2.5-VL-3B-Instruct", "openai"),
("Qwen/Qwen3.5-4B", "openai"),
("fixie-ai/ultravox-v0_5-llama-3_2-1b", "string"),
("Qwen/Qwen2-Audio-7B-Instruct", "openai"),
("meta-llama/Llama-Guard-3-1B", "openai"),
],
)
def test_resolve_content_format_hf_defined(model, expected_format):
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
model_info.check_available_online(on_fail="skip")
model_config = ModelConfig(
model,
tokenizer=model_info.tokenizer or model,
tokenizer_mode=model_info.tokenizer_mode,
revision=model_info.revision,
trust_remote_code=model_info.trust_remote_code,
hf_overrides=model_info.hf_overrides,
skip_tokenizer_init=model_info.require_embed_inputs,
enable_prompt_embeds=model_info.require_embed_inputs,
enable_mm_embeds=model_info.require_embed_inputs,
enforce_eager=model_info.enforce_eager,
dtype=model_info.dtype,
)
tokenizer = get_tokenizer(
model,
trust_remote_code=model_config.trust_remote_code,
)
# Test detecting the tokenizer's chat_template
chat_template = resolve_chat_template(
tokenizer,
chat_template=None,
tools=None,
model_config=model_config,
)
assert isinstance(chat_template, str)
print("[TEXT]")
print(chat_template)
print("[AST]")
print(_try_extract_ast(chat_template))
resolved_format = resolve_chat_template_content_format(
None, # Test detecting the tokenizer's chat_template
None,
"auto",
tokenizer,
model_config=model_config,
)
assert resolved_format == expected_format
@pytest.mark.parametrize(
("model", "expected_format"),
[
("Salesforce/blip2-opt-2.7b", "string"),
("facebook/chameleon-7b", "string"),
("deepseek-ai/deepseek-vl2-tiny", "string"),
("google/paligemma-3b-mix-224", "string"),
],
)
def test_resolve_content_format_fallbacks(model, expected_format):
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
model_info.check_available_online(on_fail="skip")
model_config = ModelConfig(
model,
tokenizer=model_info.tokenizer or model,
tokenizer_mode=model_info.tokenizer_mode,
revision=model_info.revision,
trust_remote_code=model_info.trust_remote_code,
hf_overrides=model_info.hf_overrides,
skip_tokenizer_init=model_info.require_embed_inputs,
enable_prompt_embeds=model_info.require_embed_inputs,
enable_mm_embeds=model_info.require_embed_inputs,
enforce_eager=model_info.enforce_eager,
dtype=model_info.dtype,
)
tokenizer = get_tokenizer(
model_config.tokenizer,
trust_remote_code=model_config.trust_remote_code,
)
# Test detecting the tokenizer's chat_template
chat_template = resolve_chat_template(
tokenizer,
chat_template=None,
tools=None,
model_config=model_config,
)
assert isinstance(chat_template, str)
print("[TEXT]")
print(chat_template)
print("[AST]")
print(_try_extract_ast(chat_template))
resolved_format = resolve_chat_template_content_format(
None, # Test detecting the tokenizer's chat_template
None,
"auto",
tokenizer,
model_config=model_config,
)
assert resolved_format == expected_format
@pytest.mark.parametrize(
("template_path", "expected_format"),
[
("template_alpaca.jinja", "string"),
("template_chatglm.jinja", "string"),
("template_chatglm2.jinja", "string"),
("template_chatml.jinja", "string"),
("template_falcon_180b.jinja", "string"),
("template_falcon.jinja", "string"),
("template_inkbot.jinja", "string"),
("template_teleflm.jinja", "string"),
("pooling/embed/template/dse_qwen2_vl.jinja", "openai"),
("pooling/embed/template/vlm2vec_phi3v.jinja", "openai"),
("pooling/embed/template/vlm2vec_qwen2vl.jinja", "openai"),
("tool_chat_template_granite_20b_fc.jinja", "string"),
("tool_chat_template_hermes.jinja", "string"),
("tool_chat_template_internlm2_tool.jinja", "string"),
("tool_chat_template_llama3.1_json.jinja", "openai"),
("tool_chat_template_llama3.2_json.jinja", "openai"),
("tool_chat_template_mistral_parallel.jinja", "string"),
("tool_chat_template_mistral.jinja", "string"),
],
)
def test_resolve_content_format_examples(template_path, expected_format):
model = "Qwen/Qwen2-VL-2B-Instruct" # Dummy
model_config = ModelConfig(
model,
tokenizer=model,
trust_remote_code=True,
)
dummy_tokenizer = get_tokenizer(
model,
trust_remote_code=model_config.trust_remote_code,
)
dummy_tokenizer.chat_template = None
chat_template = load_chat_template(EXAMPLES_DIR / template_path)
assert isinstance(chat_template, str)
print("[TEXT]")
print(chat_template)
print("[AST]")
print(_try_extract_ast(chat_template))
resolved_format = resolve_chat_template_content_format(
chat_template,
None,
"auto",
dummy_tokenizer,
model_config=model_config,
)
assert resolved_format == expected_format
@pytest.mark.parametrize(
"model,template,add_generation_prompt,continue_final_message,expected_output",
MODEL_TEMPLATE_GENERATION_OUTPUT,
)
def test_get_gen_prompt(
model, template, add_generation_prompt, continue_final_message, expected_output
):
model_info = HF_EXAMPLE_MODELS.find_hf_info(model)
model_info.check_available_online(on_fail="skip")
model_config = ModelConfig(
model,
tokenizer=model_info.tokenizer or model,
tokenizer_mode=model_info.tokenizer_mode,
trust_remote_code=model_info.trust_remote_code,
revision=model_info.revision,
hf_overrides=model_info.hf_overrides,
skip_tokenizer_init=model_info.require_embed_inputs,
enable_prompt_embeds=model_info.require_embed_inputs,
enable_mm_embeds=model_info.require_embed_inputs,
enforce_eager=model_info.enforce_eager,
dtype=model_info.dtype,
)
# Initialize the tokenizer
tokenizer = get_tokenizer(
tokenizer_name=model_config.tokenizer,
trust_remote_code=model_config.trust_remote_code,
)
template_content = load_chat_template(chat_template=template)
# Create a mock request object using keyword arguments
mock_request = ChatCompletionRequest(
model=model,
messages=TEST_MESSAGES + [ASSISTANT_MESSAGE_TO_CONTINUE]
if continue_final_message
else TEST_MESSAGES,
add_generation_prompt=add_generation_prompt,
continue_final_message=continue_final_message,
)
# Call the function and get the result
result = safe_apply_chat_template(
model_config,
tokenizer,
mock_request.messages,
tools=None,
chat_template=mock_request.chat_template or template_content,
add_generation_prompt=mock_request.add_generation_prompt,
continue_final_message=mock_request.continue_final_message,
tokenize=False,
)
# Test assertion
assert result == expected_output, (
f"The generated prompt does not match the expected output for "
f"model {model} and template {template}"
)
class TestConvertDeveloperToSystem:
def test_converts_role(self):
conversation = [
{"role": "developer", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
]
result = _convert_developer_to_system(conversation)
assert result[0]["role"] == "system"
assert result[0]["content"] == "You are helpful."
assert result[1]["role"] == "user"
def test_removes_tools_key(self):
conversation = [
{
"role": "developer",
"content": "Instructions",
"tools": [{"type": "function"}],
},
]
result = _convert_developer_to_system(conversation)
assert "tools" not in result[0]
def test_no_developer_messages_unchanged(self):
conversation = [
{"role": "system", "content": "System prompt"},
{"role": "user", "content": "Hello"},
]
result = _convert_developer_to_system(conversation)
assert result[0]["role"] == "system"
assert result[1]["role"] == "user"
def test_does_not_mutate_original(self):
original = {
"role": "developer",
"content": "Instructions",
"tools": [{"type": "function"}],
}
conversation = [original]
_convert_developer_to_system(conversation)
assert original["role"] == "developer"
assert "tools" in original
# --- Developer role detection and conversion tests ---
CHATML_TEMPLATE = (
"{% for message in messages %}"
"{{'<|im_start|>' + message['role'] + '\\n' + message['content']}}"
"{% if (loop.last and add_generation_prompt) or not loop.last %}"
"{{ '<|im_end|>' + '\\n'}}"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt and messages[-1]['role'] != 'assistant' %}"
"{{ '<|im_start|>assistant\\n' }}"
"{% endif %}"
)
TEMPLATE_WITH_DEVELOPER = (
"{% for message in messages %}"
"{% if message['role'] == 'developer' %}"
"{{'<|im_start|>developer\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'system' %}"
"{{'<|im_start|>system\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'user' %}"
"{{'<|im_start|>user\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'assistant' %}"
"{{'<|im_start|>assistant\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{ '<|im_start|>assistant\\n' }}"
"{% endif %}"
)
STRICT_ROLE_TEMPLATE = (
"{% for message in messages %}"
"{% if message['role'] == 'system' %}"
"{{'<|im_start|>system\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'user' %}"
"{{'<|im_start|>user\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'assistant' %}"
"{{'<|im_start|>assistant\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% else %}"
"{{ raise_exception('Unexpected message role: ' + message['role']) }}"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{ '<|im_start|>assistant\\n' }}"
"{% endif %}"
)
class TestDetectDeveloperRoleSupport:
def test_absent_in_chatml(self):
assert _detect_developer_role_support(CHATML_TEMPLATE) is False
def test_present_double_quotes(self):
assert _detect_developer_role_support(TEMPLATE_WITH_DEVELOPER) is True
def test_present_single_quotes(self):
template = TEMPLATE_WITH_DEVELOPER.replace('"developer"', "'developer'")
assert _detect_developer_role_support(template) is True
def test_absent_in_strict_template(self):
assert _detect_developer_role_support(STRICT_ROLE_TEMPLATE) is False
class TestSafeApplyChatTemplateDeveloperRole:
@pytest.fixture
def model_config(self):
return ModelConfig(
"facebook/opt-125m",
tokenizer="facebook/opt-125m",
tokenizer_mode="auto",
trust_remote_code=False,
dtype="float16",
)
@pytest.fixture
def tokenizer(self):
return get_tokenizer("facebook/opt-125m")
def test_developer_converted_to_system_for_chatml(self, model_config, tokenizer):
conversation = [
{"role": "developer", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello"},
]
result = safe_apply_chat_template(
model_config,
tokenizer,
conversation,
chat_template=CHATML_TEMPLATE,
tokenize=False,
add_generation_prompt=True,
)
assert "<|im_start|>system" in result
assert "You are a helpful assistant." in result
assert "<|im_start|>developer" not in result
def test_developer_preserved_when_template_supports_it(
self, model_config, tokenizer
):
conversation = [
{"role": "developer", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello"},
]
result = safe_apply_chat_template(
model_config,
tokenizer,
conversation,
chat_template=TEMPLATE_WITH_DEVELOPER,
tokenize=False,
add_generation_prompt=True,
)
assert "<|im_start|>developer" in result
assert "You are a helpful assistant." in result
def test_developer_does_not_crash_strict_template(self, model_config, tokenizer):
conversation = [
{"role": "developer", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello"},
]
result = safe_apply_chat_template(
model_config,
tokenizer,
conversation,
chat_template=STRICT_ROLE_TEMPLATE,
tokenize=False,
add_generation_prompt=True,
)
assert "<|im_start|>system" in result
assert "You are a helpful assistant." in result
def test_no_developer_messages_no_overhead(self, model_config, tokenizer):
conversation = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
]
result = safe_apply_chat_template(
model_config,
tokenizer,
conversation,
chat_template=CHATML_TEMPLATE,
tokenize=False,
add_generation_prompt=True,
)
assert "<|im_start|>system" in result
assert "You are helpful." in result
def test_developer_at_non_first_position_consolidated(
self, model_config, tokenizer
):
conversation = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "developer", "content": "Be concise."},
{"role": "user", "content": "What is 2+2?"},
]
result = safe_apply_chat_template(
model_config,
tokenizer,
conversation,
chat_template=SYSTEM_FIRST_TEMPLATE,
tokenize=False,
add_generation_prompt=True,
)
assert "<|im_start|>system" in result
assert "You are helpful." in result
assert "Be concise." in result
assert "What is 2+2?" in result
def test_developer_only_no_prior_system(self, model_config, tokenizer):
conversation = [
{"role": "user", "content": "Hello"},
{"role": "developer", "content": "Be concise."},
{"role": "user", "content": "What is 2+2?"},
]
result = safe_apply_chat_template(
model_config,
tokenizer,
conversation,
chat_template=SYSTEM_FIRST_TEMPLATE,
tokenize=False,
add_generation_prompt=True,
)
assert "<|im_start|>system" in result
assert "Be concise." in result
SYSTEM_FIRST_TEMPLATE = (
"{% for message in messages %}"
"{% if message['role'] == 'system' %}"
"{% if not loop.first %}"
"{{ raise_exception('System message must be at the beginning.') }}"
"{% endif %}"
"{{'<|im_start|>system\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'user' %}"
"{{'<|im_start|>user\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% elif message['role'] == 'assistant' %}"
"{{'<|im_start|>assistant\\n' + message['content'] + '<|im_end|>\\n'}}"
"{% else %}"
"{{ raise_exception('Unexpected message role: ' + message['role']) }}"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{ '<|im_start|>assistant\\n' }}"
"{% endif %}"
)
class TestConsolidateSystemMessages:
def test_no_system_messages_unchanged(self):
conversation = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi"},
]
result = _consolidate_system_messages(conversation)
assert result == conversation
def test_single_system_at_start_unchanged(self):
conversation = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
]
result = _consolidate_system_messages(conversation)
assert result == conversation
def test_system_at_non_first_position_moved(self):
conversation = [
{"role": "user", "content": "Hello"},
{"role": "system", "content": "You are helpful."},
]
result = _consolidate_system_messages(conversation)
assert result[0]["role"] == "system"
assert result[0]["content"] == "You are helpful."
assert result[1]["role"] == "user"
assert result[1]["content"] == "Hello"
def test_multiple_system_messages_merged(self):
conversation = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
{"role": "system", "content": "Be concise."},
]
result = _consolidate_system_messages(conversation)
assert len(result) == 2
assert result[0]["role"] == "system"
assert result[0]["content"] == "You are helpful.\n\nBe concise."
assert result[1]["role"] == "user"
def test_list_content_handled(self):
conversation = [
{"role": "user", "content": "Hello"},
{
"role": "system",
"content": [
{"type": "text", "text": "Rule 1."},
{"type": "text", "text": "Rule 2."},
],
},
]
result = _consolidate_system_messages(conversation)
assert result[0]["role"] == "system"
assert result[0]["content"] == "Rule 1.\nRule 2."
assert result[1]["role"] == "user"
def test_does_not_mutate_original(self):
conversation = [
{"role": "user", "content": "Hello"},
{"role": "system", "content": "You are helpful."},
]
original_len = len(conversation)
_consolidate_system_messages(conversation)
assert len(conversation) == original_len
assert conversation[0]["role"] == "user"
assert conversation[1]["role"] == "system"