LLM Provider Message Validation#
Overview#
Different LLM providers impose varying strictness on the structure and content of messages they accept. The Dify agent adapter (dify-agent/src/dify_agent/adapters/llm/model.py) normalizes messages before dispatching to the plugin daemon, applying two categories of validation:
- Empty/whitespace filtering β blank content is stripped to avoid provider rejections
- System message ordering and merging β multiple system messages are collapsed into a single leading message to satisfy strict provider templates (e.g., Qwen3.5/3.6 via vLLM)
Why This Matters#
vLLM's Qwen3.5/3.6 chat template enforces that any system message not at position 0 triggers a 400 error β including a second consecutive system message after the first . Because neither vLLM's OpenAI-compatible endpoint nor the openai_api_compatible provider plugin merge system messages server-side, the caller (Dify's adapter) must guarantee the invariant: exactly one system message at index 0 .
Key Validation Points#
1. System Prompt Whitespace Filtering#
In _map_messages_to_prompt_messages, runtime instruction parts are filtered via .strip() before being wrapped as SystemPromptMessage:
Line 308:
if part.content.strip()
This prevents blank or whitespace-only entries from becoming empty leading system messages sent to the provider.
2. System Message Ordering and Merging#
After filtering, _map_messages_to_prompt_messages calls _order_system_messages_first(prompt_messages, instruction_messages) to merge all system content into a single leading message. The function:
- Iterates through all
prompt_messagesand extracts text content from anySystemPromptMessageinstances - Extracts text content from all
instruction_messages(runtime instructions frommodel_request_parameters) - Merges all system content into a single string joined by
"\n\n" - Returns a new list with a single
SystemPromptMessagecontaining the merged content as the first element, followed by all non-system messages
Some LLM providers (e.g., Qwen via vLLM) require that the system message be exactly the first element in the messages array and reject multiple system messages. The merging behavior ensures compatibility with these strict provider templates while preserving the relative ordering of user/assistant/tool messages.
The fix was introduced to address vLLM/Qwen compatibility failures documented in and refined in . The two PRs also verify the behavior with regression tests covering:
- Scattered system messages merged into a single leading message
- Runtime instructions combined with in-history system messages
- Whitespace-only system content dropped entirely
3. Tool Description Fallback#
In _map_tool_definitions_to_prompt_tools, tool descriptions use an or "" fallback to avoid sending None to providers that require the field to be a string. No further stripping is applied to tool descriptions in the current implementation.
Message Mapping Architecture#
All validation happens inside _build_request_input , which calls:
| Helper | Responsibility |
|---|---|
_map_messages_to_prompt_messages | Convert + order + filter system prompts |
_map_tool_definitions_to_prompt_tools | Convert tool defs, fallback empty description |
_map_model_settings_to_parameters | Map model settings to provider parameters |
The result is passed directly to the plugin daemon's iter_llm_result_chunks transport .
Relevant Files & References#
- Primary adapter:
dify-agent/src/dify_agent/adapters/llm/model.py - PR #39136 β Initial system message merge fix for Qwen/vLLM
- PR #39704 β Refined fix with explicit whitespace-filtering documentation