[Bugfix][Rust Frontend][Renderer] Align DeepSeek V4 historical developer message handling (#54854)
Signed-off-by: reidliu41 <reid201711@gmail.com>
Reid committed
Sep 3, 2026 at 04:43 UTC
60857baa53a743913d4e919f12b5fd8d608ded86
2 files changed
+162
-7
rust/src/chat/src/renderer/deepseek_v4/encoding.rs
+40
-6
@@ -57,6 +57,8 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
57
let synthetic_tool_system = needs_synthetic_tool_system(request, request_tools);
58
let drop_thinking = request.parse_template_bool("drop_thinking")?.unwrap_or(true)
59
&& !rendered_tools_present(request, request_tools);
60
+ let drop_historical_developers = thinking_mode == ThinkingMode::Thinking && drop_thinking;
61
+ let last_user_like_message_index = request.messages.iter().rposition(is_user_like_entry);
62
let last_user_render_index =
63
find_last_user_render_index(request.messages.as_slice(), synthetic_tool_system);
64
let mut out = String::from(BOS_TOKEN);
@@ -81,6 +83,12 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
83
let current_render_index = render_index;
84
render_index += 1;
85
86
+ if drop_historical_developers
87
+ && is_historical_developer(message, message_index, last_user_like_message_index)
88
+ {
89
+ continue;
90
+ }
91
+
92
match message {
93
ChatMessage::System { content } => {
94
let tools = if !request_tools_attached {
@@ -126,7 +134,12 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
134
}
135
136
if (is_user_like_entry(message) || matches!(message, ChatMessage::System { .. }))
129
- && next_rendered_entry_is_assistant_or_end(request.messages.as_slice(), message_index)
137
+ && next_rendered_entry_is_assistant_or_end(
138
+ request.messages.as_slice(),
139
+ message_index,
140
+ drop_historical_developers,
141
+ last_user_like_message_index,
142
+ )
143
{
144
write_assistant_transition(
145
&mut out,
@@ -243,14 +256,35 @@ fn is_user_like_entry(message: &ChatMessage) -> bool {
256
)
257
}
258
259
+/// Return whether a developer entry precedes another user-like turn.
260
+fn is_historical_developer(
261
+ message: &ChatMessage,
262
+ message_index: usize,
263
+ last_user_like_message_index: Option<usize>,
264
+) -> bool {
265
+ matches!(message, ChatMessage::Developer { .. })
266
+ && last_user_like_message_index.is_some_and(|last_index| message_index < last_index)
267
+}
268
+
269
/// Return whether the next rendered entry is assistant, or there is no next
270
/// entry.
248
-fn next_rendered_entry_is_assistant_or_end(messages: &[ChatMessage], message_index: usize) -> bool {
271
+fn next_rendered_entry_is_assistant_or_end(
272
+ messages: &[ChatMessage],
273
+ message_index: usize,
274
+ drop_historical_developers: bool,
275
+ last_user_like_message_index: Option<usize>,
276
+) -> bool {
277
let mut next_index = message_index + 1;
250
- if is_user_content_entry(&messages[message_index]) {
251
- while next_index < messages.len() && is_user_content_entry(&messages[next_index]) {
252
- next_index += 1;
253
- }
278
+ while next_index < messages.len()
279
+ && (is_following_user_content(messages, next_index)
280
+ || (drop_historical_developers
281
+ && is_historical_developer(
282
+ &messages[next_index],
283
+ next_index,
284
+ last_user_like_message_index,
285
+ )))
286
+ {
287
+ next_index += 1;
288
}
289
290
messages
rust/src/chat/src/renderer/deepseek_v4/tests.rs
+122
-1
@@ -10,7 +10,7 @@ use super::DeepSeekV4ChatRenderer;
10
use crate::ChatRenderer;
11
use crate::event::{AssistantContentBlock, AssistantToolCall};
12
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
13
-use crate::request::{ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort};
13
+use crate::request::{ChatMessage, ChatRequest, ChatTool, GenerationPromptMode, ReasoningEffort};
14
15
fn render_request(request: &ChatRequest) -> String {
16
DeepSeekV4ChatRenderer::new()
@@ -21,6 +21,19 @@ fn render_request(request: &ChatRequest) -> String {
21
.expect("deepseek v4 renderer should return text prompt")
22
}
23
24
+fn thinking_request(messages: Vec<ChatMessage>) -> ChatRequest {
25
+ let mut request = ChatRequest {
26
+ messages,
27
+ ..ChatRequest::for_test()
28
+ };
29
+ request
30
+ .chat_options
31
+ .template_kwargs
32
+ .insert("thinking".to_string(), Value::Bool(true));
33
+ request.chat_options.reasoning_effort = Some(ReasoningEffort::Low);
34
+ request
35
+}
36
+
37
fn fixture_request(input_name: &str) -> ChatRequest {
38
let mut request = fixture_chat_request(&fixture_path(input_name), deepseek_fixture_options());
39
request.chat_options.reasoning_effort = Some(ReasoningEffort::Low);
@@ -71,6 +84,114 @@ fn renders_developer_tools_like_hf_python() {
84
);
85
}
86
87
+#[test]
88
+fn drop_thinking_removes_developer_before_last_user() {
89
+ let request = thinking_request(vec![
90
+ ChatMessage::developer("old instruction", None),
91
+ ChatMessage::assistant_blocks(vec![
92
+ AssistantContentBlock::Reasoning {
93
+ text: "old reasoning".to_string(),
94
+ },
95
+ AssistantContentBlock::Text {
96
+ text: "old answer".to_string(),
97
+ },
98
+ ]),
99
+ ChatMessage::user("next question"),
100
+ ]);
101
+
102
+ let rendered = render_request(&request);
103
+
104
+ expect![
105
+ "<|begin▁of▁sentence|>old answer<|end▁of▁sentence|><|User|>next question<|Assistant|><think>"
106
+ ]
107
+ .assert_eq(&rendered);
108
+}
109
+
110
+#[test]
111
+fn dropped_developer_does_not_hide_following_assistant() {
112
+ let request = thinking_request(vec![
113
+ ChatMessage::user("old question"),
114
+ ChatMessage::developer("old instruction", None),
115
+ ChatMessage::assistant_text("old answer"),
116
+ ChatMessage::user("next question"),
117
+ ]);
118
+
119
+ let rendered = render_request(&request);
120
+
121
+ expect![
122
+ "<|begin▁of▁sentence|><|User|>old question<|Assistant|></think>old answer<|end▁of▁sentence|><|User|>next question<|Assistant|><think>"
123
+ ]
124
+ .assert_eq(&rendered);
125
+}
126
+
127
+#[test]
128
+fn drop_thinking_keeps_last_developer() {
129
+ let request = thinking_request(vec![
130
+ ChatMessage::user("old question"),
131
+ ChatMessage::assistant_text("old answer"),
132
+ ChatMessage::developer("latest instruction", None),
133
+ ]);
134
+
135
+ let rendered = render_request(&request);
136
+
137
+ expect![
138
+ "<|begin▁of▁sentence|><|User|>old question<|Assistant|></think>old answer<|end▁of▁sentence|><|User|>latest instruction<|Assistant|><think>"
139
+ ]
140
+ .assert_eq(&rendered);
141
+}
142
+
143
+#[test]
144
+fn drop_thinking_false_keeps_historical_developer() {
145
+ let mut request = thinking_request(vec![
146
+ ChatMessage::developer("old instruction", None),
147
+ ChatMessage::assistant_text("old answer"),
148
+ ChatMessage::user("next question"),
149
+ ]);
150
+ request
151
+ .chat_options
152
+ .template_kwargs
153
+ .insert("drop_thinking".to_string(), Value::Bool(false));
154
+
155
+ let rendered = render_request(&request);
156
+
157
+ assert!(rendered.contains("<|User|>old instruction"));
158
+}
159
+
160
+#[test]
161
+fn drop_thinking_removes_developer_before_tool_response() {
162
+ let request = thinking_request(vec![
163
+ ChatMessage::developer("old instruction", None),
164
+ ChatMessage::assistant_text("old answer"),
165
+ ChatMessage::tool_response("result", "call-1"),
166
+ ]);
167
+
168
+ let rendered = render_request(&request);
169
+
170
+ expect![
171
+ "<|begin▁of▁sentence|>old answer<|end▁of▁sentence|><|User|><tool_result>result</tool_result><|Assistant|><think>"
172
+ ]
173
+ .assert_eq(&rendered);
174
+}
175
+
176
+#[test]
177
+fn tools_keep_historical_developer() {
178
+ let tool = ChatTool {
179
+ name: "lookup".to_string(),
180
+ description: None,
181
+ parameters: Value::Object(Default::default()),
182
+ strict: None,
183
+ };
184
+ let request = thinking_request(vec![
185
+ ChatMessage::developer("old instruction", Some(vec![tool])),
186
+ ChatMessage::assistant_text("old answer"),
187
+ ChatMessage::user("next question"),
188
+ ]);
189
+
190
+ let rendered = render_request(&request);
191
+
192
+ assert!(rendered.contains("<|User|>old instruction"));
193
+}
194
+
195
#[test]
196
fn reasoning_effort_max_adds_prefix_when_thinking_is_enabled() {
197
let mut request = ChatRequest {