fix: merge system messages and place them first to comply with Qwen/v… (#39136)

This commit is contained in:
potoior 2026-08-21 06:24:20 +00:00 committed by GitHub
parent 7d635d9f7e
commit 34fdac2113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 106 additions and 11 deletions

View File

@ -304,16 +304,40 @@ def _map_messages_to_prompt_messages(
for part in (Model._get_instruction_parts(messages, model_request_parameters) or [])
if part.content.strip()
]
if instruction_messages:
insert_at = next(
(index for index, message in enumerate(prompt_messages) if not isinstance(message, SystemPromptMessage)),
len(prompt_messages),
)
prompt_messages[insert_at:insert_at] = instruction_messages
prompt_messages = _order_system_messages_first(prompt_messages, instruction_messages)
return prompt_messages
def _order_system_messages_first(
prompt_messages: Sequence[PromptMessage],
instruction_messages: Sequence[SystemPromptMessage],
) -> list[PromptMessage]:
"""Merge all system content into a single leading system message.
Some providers (e.g. vLLM serving Qwen3.5/3.6 chat templates) reject any
system message that is not exactly the first message, so sorting alone is
not enough: multiple system messages must be merged into one.
"""
system_contents: list[str] = []
non_system_messages: list[PromptMessage] = []
for message in prompt_messages:
if isinstance(message, SystemPromptMessage):
text = message.get_text_content()
if text.strip():
system_contents.append(text)
else:
non_system_messages.append(message)
for instruction in instruction_messages:
text = instruction.get_text_content()
if text.strip():
system_contents.append(text)
if not system_contents:
return non_system_messages
return [SystemPromptMessage(content="\n\n".join(system_contents)), *non_system_messages]
def _map_model_request_to_prompt_messages(message: ModelRequest) -> list[PromptMessage]:
prompt_messages: list[PromptMessage] = []

View File

@ -171,12 +171,11 @@ class DifyLLMAdapterModelTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(set(tools_by_name), {"weather", "incident_summary"})
self.assertEqual(tools_by_name["incident_summary"]["parameters"]["required"], ["title"])
self.assertEqual(data["prompt_messages"][0]["role"], "system")
self.assertEqual(data["prompt_messages"][0]["content"], "request system")
self.assertEqual(data["prompt_messages"][1]["content"], "be concise")
self.assertEqual(data["prompt_messages"][2]["content"], "hello")
self.assertEqual(data["prompt_messages"][0]["content"], "request system\n\nbe concise")
self.assertEqual(data["prompt_messages"][1]["content"], "hello")
self.assertEqual(data["prompt_messages"][2]["role"], "tool")
self.assertEqual(data["prompt_messages"][3]["role"], "tool")
self.assertEqual(data["prompt_messages"][4]["role"], "tool")
self.assertEqual(data["prompt_messages"][5]["role"], "assistant")
self.assertEqual(data["prompt_messages"][4]["role"], "assistant")
return build_stream_response(
LLMResultChunk(
model="demo-model",
@ -291,6 +290,78 @@ class DifyLLMAdapterModelTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(usage.time_to_first_token, 0.2)
self.assertEqual(usage.time_to_generate, 0.6)
async def test_request_merges_system_messages_before_history(self) -> None:
messages = [
ModelRequest(parts=[UserPromptPart("previous user")]),
ModelResponse(parts=[TextPart(content="previous answer")]),
ModelRequest(parts=[SystemPromptPart("current system"), UserPromptPart("current user")]),
]
request_parameters = ModelRequestParameters(instruction_parts=[InstructionPart(content="runtime instruction")])
def handler(request: httpx.Request) -> httpx.Response:
payload = json.loads(request.content.decode("utf-8"))
prompt_messages = payload["target"]["prompt_messages"]
self.assertEqual(
[message["role"] for message in prompt_messages],
["system", "user", "assistant", "user"],
)
self.assertEqual(prompt_messages[0]["content"], "current system\n\nruntime instruction")
self.assertEqual(prompt_messages[1]["content"], "previous user")
self.assertEqual(prompt_messages[2]["content"], "previous answer")
self.assertEqual(prompt_messages[3]["content"], "current user")
return build_stream_response(*single_text_chunk("adapter response", prompt_tokens=11, completion_tokens=7))
async with self.mock_gateway_stream(httpx.MockTransport(handler)):
adapter = DifyLLMAdapterModel(
"demo-model",
self.make_provider(),
model_provider="openai",
)
response = await adapter.request(
messages,
model_settings=None,
model_request_parameters=request_parameters,
)
self.assertEqual(response.model_name, "demo-model")
self.assertEqual(cast(TextPart, response.parts[0]).content, "adapter response")
async def test_request_merges_scattered_system_messages_without_instructions(self) -> None:
messages = [
ModelRequest(parts=[SystemPromptPart("first system"), UserPromptPart("hello")]),
ModelResponse(parts=[TextPart(content="answer")]),
ModelRequest(parts=[SystemPromptPart("second system"), UserPromptPart("follow up")]),
]
def handler(request: httpx.Request) -> httpx.Response:
payload = json.loads(request.content.decode("utf-8"))
prompt_messages = payload["target"]["prompt_messages"]
self.assertEqual(
[message["role"] for message in prompt_messages],
["system", "user", "assistant", "user"],
)
self.assertEqual(prompt_messages[0]["content"], "first system\n\nsecond system")
return build_stream_response(*single_text_chunk("adapter response", prompt_tokens=11, completion_tokens=7))
async with self.mock_gateway_stream(httpx.MockTransport(handler)):
adapter = DifyLLMAdapterModel(
"demo-model",
self.make_provider(),
model_provider="openai",
)
response = await adapter.request(
messages,
model_settings=None,
model_request_parameters=ModelRequestParameters(),
)
self.assertEqual(response.model_name, "demo-model")
self.assertEqual(cast(TextPart, response.parts[0]).content, "adapter response")
async def test_request_maps_tool_call_only_assistant_history_to_empty_string_content(self) -> None:
messages = [
ModelRequest(parts=[SystemPromptPart("request system"), UserPromptPart("hello")]),