[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 {