Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions python/freetoken/server/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,27 @@ def pick(value, key, framework):
def render_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Normalize OpenAI-shaped message dicts for the chat template: flatten text
content parts to a string and decode tool-call arguments from JSON. Raises
ValueError on a non-text content part (text-only server). Shared by all adapters."""
return [_render_message(m) for m in messages]
ValueError on a non-text content part (text-only server). Shared by all adapters.

Some chat templates (e.g. Qwen3.6) require the system message at index 0;
hoist system messages to the front and merge multiples into one to satisfy
that constraint."""
rendered = [_render_message(m) for m in messages]
system_msgs = [m for m in rendered if m.get("role") == "system"]
if system_msgs and rendered[0].get("role") != "system":
non_system = [m for m in rendered if m.get("role") != "system"]
rendered = system_msgs + non_system
# Merge multiple system messages into one (Qwen3.6 template rejects
# system messages that are not at loop.first, i.e. after the first).
system_msgs = [m for m in rendered if m.get("role") == "system"]
if len(system_msgs) > 1:
sep = chr(10) + chr(10)
merged_content = sep.join(
str(m.get("content") or "") for m in system_msgs if m.get("content")
)
non_system = [m for m in rendered if m.get("role") != "system"]
rendered = [{"role": "system", "content": merged_content}] + non_system
return rendered


def _render_message(message: dict[str, Any]) -> dict[str, Any]:
Expand Down
33 changes: 33 additions & 0 deletions tests/server/test_mm_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,36 @@ def test_image_token_budget_flags_land_in_the_multimodal_config():
assert parse_args(["--model", "/models/anon"])[0].mm.processor_kwargs == {}
with pytest.raises(SystemExit): # argparse reports the bad pair and exits
parse_args(["--model", "/models/anon", "--image-min-tokens", "2048", "--image-max-tokens", "1024"])


def test_render_messages_hoists_system_to_front():
"""render_messages moves system messages to index 0 for templates that
require it (e.g. Qwen3.6 'System message must be at the beginning')."""
msgs = render_messages([
{"role": "user", "content": "What is 2+2?"},
{"role": "system", "content": "You are a math tutor."},
{"role": "user", "content": "Answer briefly."},
])
assert msgs[0]["role"] == "system"
assert msgs[1]["role"] == "user"
assert msgs[2]["role"] == "user"


def test_render_messages_preserves_system_first():
"""When system is already first, no reordering happens."""
msgs = render_messages([
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hi"},
])
assert msgs[0]["role"] == "system"
assert msgs[1]["role"] == "user"


def test_render_messages_no_system_unchanged():
"""No system message → order preserved."""
msgs = render_messages([
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there"},
{"role": "user", "content": "Bye"},
])
assert [m["role"] for m in msgs] == ["user", "assistant", "user"]