[Bugfix][Multimodal] Preserve DeepSeek V4 image block spacing (#56882)

Signed-off-by: Xucheng Zhou <aden1350@outlook.com>
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
adenzhou1350
2026-09-18 18:44:39 +08:00
committed by GitHub
co-authored by OpenAI Codex Isotr0py mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 04a3a00eb7
commit cc09352e5c
5 changed files with 60 additions and 12 deletions
@@ -0,0 +1,39 @@
{
"thinking_mode": "thinking",
"reasoning_effort": "max",
"messages": [
{
"role": "system",
"content": "You are a helpful vision assistant."
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "请按“第一张、第二张”的顺序回答:第一张图"
},
{
"type": "image_url",
"image_url": {
"url": "examples/images/carrots.jpeg"
}
},
{
"type": "text",
"text": "和第二张图"
},
{
"type": "image_url",
"image_url": {
"url": "examples/images/corn.jpeg"
}
},
{
"type": "text",
"text": "中分别是什么食材?它们通常食用的部位分别是什么?"
}
]
}
]
}
@@ -0,0 +1,9 @@
<|begin▁of▁sentence|>You are a helpful vision assistant.<|User|>请按“第一张、第二张”的顺序回答:第一张图
<|deepseek_image|>
和第二张图
<|deepseek_image|>
中分别是什么食材?它们通常食用的部位分别是什么?<|Assistant|></think>
+7 -7
View File
@@ -54,13 +54,9 @@ def _load_reference_case(case_id: int):
def _render_reference_case(case_id: int, **kwargs):
messages, tools = _load_reference_case(case_id)
conversation, _, _ = parse_chat_messages(
messages,
_model_config(),
content_format="string",
)
# Preserve content blocks for encoding without fetching fixture media.
return _tokenizer().apply_chat_template(
conversation=conversation,
conversation=messages,
messages=messages,
tools=tools,
tokenize=False,
@@ -397,6 +393,7 @@ def test_deepseek_v4_maps_xhigh_to_high_reasoning_effort():
(2, {"thinking": True, "reasoning_effort": "low"}),
(3, {"thinking": True, "reasoning_effort": "low"}),
(4, {"thinking": False}),
(5, {"thinking": False}),
],
)
def test_deepseek_v4_matches_reference_golden_fixtures(case_id, kwargs):
@@ -447,7 +444,10 @@ def test_deepseek_v4_image_blocks_become_placeholders():
thinking=False,
)
assert "<|User|>first:<|deepseek_image|>second:<|deepseek_image|>" in prompt
assert (
"<|User|>first:\n\n<|deepseek_image|>\n\n"
"second:\n\n<|deepseek_image|>" in prompt
)
def test_deepseek_v4_image_sentinel_ids_match_tokenizer():
+2 -2
View File
@@ -40,7 +40,7 @@ class DeepseekV4Renderer(BaseRenderer[DeepseekV4Tokenizer]):
conversation, mm_data, mm_uuids = parse_chat_messages(
messages,
self.model_config,
content_format="string",
content_format="openai",
media_io_kwargs=params.media_io_kwargs,
mm_processor_kwargs=params.mm_processor_kwargs,
)
@@ -67,7 +67,7 @@ class DeepseekV4Renderer(BaseRenderer[DeepseekV4Tokenizer]):
conversation, mm_data, mm_uuids = await parse_chat_messages_async(
messages,
self.model_config,
content_format="string",
content_format="openai",
media_io_kwargs=params.media_io_kwargs,
mm_processor_kwargs=params.mm_processor_kwargs,
)
+3 -3
View File
@@ -224,7 +224,7 @@ def flatten_content_blocks(content: Any) -> Any:
raise ValueError(
f"Unsupported content block type: {block.get('type')!r}"
)
return "".join(parts)
return "\n\n".join(parts)
def render_message(
@@ -267,7 +267,7 @@ def render_message(
)
role = msg.get("role")
content = msg.get("content")
content = flatten_content_blocks(msg.get("content"))
tools = msg.get("tools")
response_format = msg.get("response_format")
tool_calls = msg.get("tool_calls")
@@ -337,7 +337,7 @@ def render_message(
parts.append(f"[Unsupported {block_type}]")
prompt += "\n\n".join(parts)
else:
prompt += flatten_content_blocks(content) or ""
prompt += content or ""
elif role == "latest_reminder":
prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(content=content)