[Bugfix][Rust Frontend] Honor `add_generation_prompt` in DeepSeek V3.2/V4/V4.1 renderers (#56593)
Co-authored-by: Bugen Zhao <i@bugenzhao.com> Signed-off-by: YeonwooSung <neos960518@gmail.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Yeonwoo Sung committed
Sep 15, 2026 at 19:03 UTC
142020c6c2c0d4fec4d36df28290e3d9b654a385
5 files changed
+217
-24
rust/src/chat/src/renderer/deepseek.rs
+28
-19
@@ -224,21 +224,30 @@ pub(super) fn render_request_with_media_order(
224
}
225
}
226
227
- if (is_user_like_entry(message, current_render_index as usize, dialect)
228
- || (dialect == DsDialect::V4 && matches!(message, ChatMessage::System { .. })))
229
- && next_rendered_entry_is_assistant_or_end(
227
+ if is_user_like_entry(message, current_render_index as usize, dialect)
228
+ || (dialect == DsDialect::V4 && matches!(message, ChatMessage::System { .. }))
229
+ {
230
+ // Skip the opener only at the last rendered entry when
231
+ // add_generation_prompt is off. Inspect the next rendered
232
+ // entry, not the raw index: V4 merges trailing tool results.
233
+ let should_write_transition = match next_rendered_entry(
234
request.messages.as_slice(),
235
message_index,
236
drop_historical_developers,
237
last_user_like_message_index,
234
- )
235
- {
236
- write_assistant_transition(
237
- &mut out,
238
- thinking_mode,
239
- drop_thinking,
240
- current_render_index >= last_user_render_index,
241
- );
238
+ ) {
239
+ Some(ChatMessage::Assistant { .. }) => true,
240
+ None => request.chat_options.add_generation_prompt(),
241
+ Some(_) => false,
242
+ };
243
+ if should_write_transition {
244
+ write_assistant_transition(
245
+ &mut out,
246
+ thinking_mode,
247
+ drop_thinking,
248
+ current_render_index >= last_user_render_index,
249
+ );
250
+ }
251
}
252
}
253
@@ -420,14 +429,17 @@ fn is_historical_developer(
429
&& last_user_like_message_index.is_some_and(|last_index| message_index < last_index)
430
}
431
423
-/// Return whether the next rendered entry is assistant, or there is no next
424
-/// entry.
425
-fn next_rendered_entry_is_assistant_or_end(
432
+/// Return the next rendered message after `message_index`.
433
+///
434
+/// Skips merged user-content tails and dropped historical developers so the
435
+/// caller can tell a following assistant turn apart from the end of the
436
+/// conversation.
437
+fn next_rendered_entry(
438
messages: &[ChatMessage],
439
message_index: usize,
440
drop_historical_developers: bool,
441
last_user_like_message_index: Option<usize>,
430
-) -> bool {
442
+) -> Option<&ChatMessage> {
443
let mut next_index = message_index + 1;
444
while next_index < messages.len()
445
&& (is_following_user_content(messages, next_index)
@@ -441,10 +453,7 @@ fn next_rendered_entry_is_assistant_or_end(
453
next_index += 1;
454
}
455
444
- messages
445
- .get(next_index)
446
- .map(|message| matches!(message, ChatMessage::Assistant { .. }))
447
- .unwrap_or(true)
456
+ messages.get(next_index)
457
}
458
459
/// Render the tool preamble shown to the model for one DeepSeek dialect.
rust/src/chat/src/renderer/deepseek_v32/encoding.rs
+14
-4
@@ -61,6 +61,7 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
61
find_last_user_render_index(request.messages.as_slice(), render_offset);
62
let last_user_actual_index = find_last_user_actual_index(request.messages.as_slice());
63
let continue_final_message = request.chat_options.continue_final_message();
64
+ let add_generation_prompt = request.chat_options.add_generation_prompt();
65
let mut prompt = String::from(BOS_TOKEN);
66
67
if !initial_tools.is_empty() {
@@ -79,6 +80,7 @@ pub(super) fn render_request(request: &ChatRequest) -> Result<String> {
80
thinking_mode,
81
drop_thinking,
82
continue_final_message,
83
+ add_generation_prompt,
84
)?;
85
}
86
@@ -110,11 +112,13 @@ fn render_message(
112
thinking_mode: ThinkingMode,
113
drop_thinking: bool,
114
continue_final_message: bool,
115
+ add_generation_prompt: bool,
116
) -> Result<()> {
117
let render_index = message_index as isize + render_offset;
118
let opens_thinking = render_index == last_user_render_index;
119
let after_last_user_turn = render_index > last_user_render_index;
120
let after_or_at_last_user_turn = render_index >= last_user_render_index;
121
+ let add_generation_prompt = message_index + 1 != messages.len() || add_generation_prompt;
122
123
match message {
124
ChatMessage::System { content } => render_system_message(out, Some(content), &[]),
@@ -123,11 +127,13 @@ fn render_message(
127
content,
128
tools.as_deref().unwrap_or(&[]),
129
thinking_mode == ThinkingMode::Thinking && opens_thinking,
130
+ add_generation_prompt,
131
),
132
ChatMessage::User { content } => render_user_message(
133
out,
134
content,
135
thinking_mode == ThinkingMode::Thinking && opens_thinking,
136
+ add_generation_prompt,
137
),
138
ChatMessage::Assistant { content } => render_assistant_message(
139
out,
@@ -195,6 +201,7 @@ fn render_developer_message(
201
content: &ChatContent,
202
tools: &[ChatTool],
203
opens_thinking: bool,
204
+ add_generation_prompt: bool,
205
) -> Result<()> {
206
if content.is_empty() {
207
return Err(Error::ChatTemplate(
@@ -209,7 +216,7 @@ fn render_developer_message(
216
}
217
out.push_str("\n\n# The user's message is: ");
218
write_chat_content(out, content)?;
212
- write_user_like_suffix(out, opens_thinking);
219
+ write_user_like_suffix(out, opens_thinking, add_generation_prompt);
220
Ok(())
221
}
222
@@ -219,17 +226,20 @@ fn render_user_message(
226
out: &mut String,
227
content: &ChatContent,
228
opens_thinking: bool,
229
+ add_generation_prompt: bool,
230
) -> Result<()> {
231
out.push_str("<|User|>");
232
write_chat_content(out, content)?;
225
- write_user_like_suffix(out, opens_thinking);
233
+ write_user_like_suffix(out, opens_thinking, add_generation_prompt);
234
Ok(())
235
}
236
237
/// Shared trailing wrapper used by both real user turns and native developer
238
/// turns after their content has already been written.
231
-// TODO: respect `add_generation_prompt` option
232
-fn write_user_like_suffix(out: &mut String, opens_thinking: bool) {
239
+fn write_user_like_suffix(out: &mut String, opens_thinking: bool, add_generation_prompt: bool) {
240
+ if !add_generation_prompt {
241
+ return;
242
+ }
243
out.push_str("<|Assistant|>");
244
if opens_thinking {
245
out.push_str(THINKING_START_TOKEN);
rust/src/chat/src/renderer/deepseek_v32/tests.rs
+38
@@ -281,6 +281,44 @@ fn assistant_after_last_user_requires_reasoning_or_tool_calls() {
281
.assert_eq(&error.to_report_string());
282
}
283
284
+#[test]
285
+fn last_user_turn_appends_generation_prompt_by_default() {
286
+ let request = thinking_request(vec![ChatMessage::user("Hello")]);
287
+
288
+ let rendered = render_request(&request);
289
+
290
+ assert!(rendered.ends_with("<|User|>Hello<|Assistant|><think>"));
291
+}
292
+
293
+#[test]
294
+fn last_user_turn_omits_generation_prompt_when_disabled() {
295
+ let mut request = thinking_request(vec![ChatMessage::user("Hello")]);
296
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
297
+
298
+ let rendered = render_request(&request);
299
+
300
+ assert!(rendered.ends_with("<|User|>Hello"));
301
+ assert!(!rendered.contains("<|Assistant|>"));
302
+ assert!(!rendered.contains("<think>"));
303
+}
304
+
305
+#[test]
306
+fn user_then_assistant_still_writes_suffix_without_generation_prompt() {
307
+ let mut request = ChatRequest {
308
+ messages: vec![
309
+ ChatMessage::user("Hello"),
310
+ ChatMessage::assistant_text("Hi there."),
311
+ ],
312
+ ..ChatRequest::for_test()
313
+ };
314
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
315
+
316
+ let rendered = render_request(&request);
317
+
318
+ expect!["<|begin▁of▁sentence|><|User|>Hello<|Assistant|></think>Hi there.<|end▁of▁sentence|>"]
319
+ .assert_eq(&rendered);
320
+}
321
+
322
#[test]
323
fn continue_final_assistant_omits_final_eos() {
324
let mut request = ChatRequest {
rust/src/chat/src/renderer/deepseek_v4/tests.rs
+91
@@ -633,3 +633,94 @@ fn continue_final_assistant_omits_final_eos() {
633
expect!["<|begin▁of▁sentence|><|User|>write<|Assistant|></think>partial answer"]
634
.assert_eq(&rendered);
635
}
636
+
637
+#[test]
638
+fn last_user_turn_appends_generation_prompt_by_default() {
639
+ let request = thinking_request(vec![ChatMessage::user("Hello")]);
640
+
641
+ let rendered = render_request(&request);
642
+
643
+ assert!(rendered.ends_with("<|User|>Hello<|Assistant|><think>"));
644
+}
645
+
646
+#[test]
647
+fn last_user_turn_omits_generation_prompt_when_disabled() {
648
+ let mut request = thinking_request(vec![ChatMessage::user("Hello")]);
649
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
650
+
651
+ let rendered = render_request(&request);
652
+
653
+ assert!(rendered.ends_with("<|User|>Hello"));
654
+ assert!(!rendered.contains("<|Assistant|>"));
655
+ assert!(!rendered.contains("<think>"));
656
+}
657
+
658
+#[test]
659
+fn user_then_assistant_still_writes_suffix_without_generation_prompt() {
660
+ let mut request = ChatRequest {
661
+ messages: vec![
662
+ ChatMessage::user("Hello"),
663
+ ChatMessage::assistant_text("Hi there."),
664
+ ],
665
+ ..ChatRequest::for_test()
666
+ };
667
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
668
+ request
669
+ .chat_options
670
+ .template_kwargs
671
+ .insert("thinking".to_string(), Value::Bool(false));
672
+
673
+ let rendered = render_request(&request);
674
+
675
+ expect![
676
+ "<|begin▁of▁sentence|><|User|>Hello<|Assistant|></think>Hi there.<|end▁of▁sentence|>"
677
+ ]
678
+ .assert_eq(&rendered);
679
+}
680
+
681
+#[test]
682
+fn trailing_system_omits_generation_prompt_when_disabled() {
683
+ let mut request = ChatRequest {
684
+ messages: vec![
685
+ ChatMessage::system("rules"),
686
+ ChatMessage::user("question"),
687
+ ChatMessage::system("final rules"),
688
+ ],
689
+ ..ChatRequest::for_test()
690
+ };
691
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
692
+ request
693
+ .chat_options
694
+ .template_kwargs
695
+ .insert("enable_thinking".to_string(), Value::Bool(false));
696
+ request.chat_options.reasoning_effort = Some(ReasoningEffort::Low);
697
+
698
+ let rendered = render_request(&request);
699
+
700
+ expect!["<|begin▁of▁sentence|>rules<|User|>questionfinal rules"].assert_eq(&rendered);
701
+}
702
+
703
+#[test]
704
+fn last_merged_tool_result_omits_generation_prompt_when_disabled() {
705
+ let mut request = thinking_request(vec![
706
+ ChatMessage::user("question"),
707
+ ChatMessage::tool_response("result", "call-1"),
708
+ ]);
709
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
710
+
711
+ let rendered = render_request(&request);
712
+
713
+ expect!["<|begin▁of▁sentence|><|User|>question\n\n<tool_result>result</tool_result>"]
714
+ .assert_eq(&rendered);
715
+ assert!(!rendered.contains("<|Assistant|>"));
716
+}
717
+
718
+#[test]
719
+fn last_developer_omits_generation_prompt_when_disabled() {
720
+ let mut request = thinking_request(vec![ChatMessage::developer("latest instruction", None)]);
721
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
722
+
723
+ let rendered = render_request(&request);
724
+
725
+ expect!["<|begin▁of▁sentence|><|User|>latest instruction"].assert_eq(&rendered);
726
+}
rust/src/chat/src/renderer/deepseek_v41/tests.rs
+46
-1
@@ -10,7 +10,9 @@ use super::DeepSeekV41ChatRenderer;
10
use crate::ChatRenderer;
11
use crate::event::{AssistantContentBlock, AssistantToolCall};
12
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
13
-use crate::request::{ChatContent, ChatContentPart, ChatMessage, ChatRequest, ReasoningEffort};
13
+use crate::request::{
14
+ ChatContent, ChatContentPart, ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort,
15
+};
16
17
fn render(request: &ChatRequest) -> String {
18
DeepSeekV41ChatRenderer::new()
@@ -281,6 +283,49 @@ fn mid_system_advances_last_user_boundary_and_drops_old_reasoning() {
283
.assert_eq(&render(&request));
284
}
285
286
+#[test]
287
+fn last_user_turn_omits_generation_prompt_when_disabled() {
288
+ let mut request = request();
289
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
290
+
291
+ expect!["<|begin▁of▁sentence|><|System|>Reasoning Effort: 50 (range 1-100, the higher the value, the more thorough the reasoning)\n\n<|User|>question"]
292
+ .assert_eq(&render(&request));
293
+}
294
+
295
+#[test]
296
+fn last_mid_system_omits_generation_prompt_when_disabled() {
297
+ let mut request = ChatRequest {
298
+ messages: vec![
299
+ ChatMessage::system("sys"),
300
+ ChatMessage::user("q1"),
301
+ ChatMessage::assistant_blocks(vec![
302
+ AssistantContentBlock::Reasoning { text: "r1".into() },
303
+ AssistantContentBlock::Text { text: "a1".into() },
304
+ ]),
305
+ ChatMessage::developer("old policy", None),
306
+ ChatMessage::system("mid sys"),
307
+ ],
308
+ ..ChatRequest::for_test()
309
+ };
310
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
311
+ request
312
+ .chat_options
313
+ .template_kwargs
314
+ .insert("reasoning_effort".into(), json!(88));
315
+
316
+ expect!["<|begin▁of▁sentence|><|System|>Reasoning Effort: 88 (range 1-100, the higher the value, the more thorough the reasoning)\n\nsys<|User|>q1<|Assistant|></think>a1<|end▁of▁sentence|><|System|>mid sys"]
317
+ .assert_eq(&render(&request));
318
+}
319
+
320
+#[test]
321
+fn chat_mode_omits_generation_prompt_when_disabled() {
322
+ let mut request = request();
323
+ request.chat_options.generation_prompt_mode = GenerationPromptMode::NoGenerationPrompt;
324
+ request.chat_options.template_kwargs.insert("thinking".into(), json!(false));
325
+
326
+ expect!["<|begin▁of▁sentence|><|User|>question"].assert_eq(&render(&request));
327
+}
328
+
329
#[test]
330
fn joins_text_parts_with_reference_separator_and_preserves_literal_tags() {
331
let mut request = ChatRequest {