From a77796f50a956a6915fb7ec237666f0ce94e37af Mon Sep 17 00:00:00 2001 From: reidliu41 Date: Tue, 1 Sep 2026 17:56:26 -0700 Subject: [PATCH] Fix DeepSeek V4 historical developer message handling Signed-off-by: reidliu41 --- .../chat/src/renderer/deepseek_v4/encoding.rs | 46 ++++++- .../chat/src/renderer/deepseek_v4/tests.rs | 123 +++++++++++++++++- 2 files changed, 162 insertions(+), 7 deletions(-) diff --git a/rust/src/chat/src/renderer/deepseek_v4/encoding.rs b/rust/src/chat/src/renderer/deepseek_v4/encoding.rs index 4909661afa2e..263edba83120 100644 --- a/rust/src/chat/src/renderer/deepseek_v4/encoding.rs +++ b/rust/src/chat/src/renderer/deepseek_v4/encoding.rs @@ -57,6 +57,8 @@ pub(super) fn render_request(request: &ChatRequest) -> Result { let synthetic_tool_system = needs_synthetic_tool_system(request, request_tools); let drop_thinking = request.parse_template_bool("drop_thinking")?.unwrap_or(true) && !rendered_tools_present(request, request_tools); + let drop_historical_developers = thinking_mode == ThinkingMode::Thinking && drop_thinking; + let last_user_like_message_index = request.messages.iter().rposition(is_user_like_entry); let last_user_render_index = find_last_user_render_index(request.messages.as_slice(), synthetic_tool_system); let mut out = String::from(BOS_TOKEN); @@ -81,6 +83,12 @@ pub(super) fn render_request(request: &ChatRequest) -> Result { let current_render_index = render_index; render_index += 1; + if drop_historical_developers + && is_historical_developer(message, message_index, last_user_like_message_index) + { + continue; + } + match message { ChatMessage::System { content } => { let tools = if !request_tools_attached { @@ -126,7 +134,12 @@ pub(super) fn render_request(request: &ChatRequest) -> Result { } if (is_user_like_entry(message) || matches!(message, ChatMessage::System { .. })) - && next_rendered_entry_is_assistant_or_end(request.messages.as_slice(), message_index) + && next_rendered_entry_is_assistant_or_end( + request.messages.as_slice(), + message_index, + drop_historical_developers, + last_user_like_message_index, + ) { write_assistant_transition( &mut out, @@ -243,14 +256,35 @@ fn is_user_like_entry(message: &ChatMessage) -> bool { ) } +/// Return whether a developer entry precedes another user-like turn. +fn is_historical_developer( + message: &ChatMessage, + message_index: usize, + last_user_like_message_index: Option, +) -> bool { + matches!(message, ChatMessage::Developer { .. }) + && last_user_like_message_index.is_some_and(|last_index| message_index < last_index) +} + /// Return whether the next rendered entry is assistant, or there is no next /// entry. -fn next_rendered_entry_is_assistant_or_end(messages: &[ChatMessage], message_index: usize) -> bool { +fn next_rendered_entry_is_assistant_or_end( + messages: &[ChatMessage], + message_index: usize, + drop_historical_developers: bool, + last_user_like_message_index: Option, +) -> bool { let mut next_index = message_index + 1; - if is_user_content_entry(&messages[message_index]) { - while next_index < messages.len() && is_user_content_entry(&messages[next_index]) { - next_index += 1; - } + while next_index < messages.len() + && (is_following_user_content(messages, next_index) + || (drop_historical_developers + && is_historical_developer( + &messages[next_index], + next_index, + last_user_like_message_index, + ))) + { + next_index += 1; } messages diff --git a/rust/src/chat/src/renderer/deepseek_v4/tests.rs b/rust/src/chat/src/renderer/deepseek_v4/tests.rs index 2226f219d74c..a0ded7bd992c 100644 --- a/rust/src/chat/src/renderer/deepseek_v4/tests.rs +++ b/rust/src/chat/src/renderer/deepseek_v4/tests.rs @@ -10,7 +10,7 @@ use super::DeepSeekV4ChatRenderer; use crate::ChatRenderer; use crate::event::{AssistantContentBlock, AssistantToolCall}; use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request}; -use crate::request::{ChatMessage, ChatRequest, GenerationPromptMode, ReasoningEffort}; +use crate::request::{ChatMessage, ChatRequest, ChatTool, GenerationPromptMode, ReasoningEffort}; fn render_request(request: &ChatRequest) -> String { DeepSeekV4ChatRenderer::new() @@ -21,6 +21,19 @@ fn render_request(request: &ChatRequest) -> String { .expect("deepseek v4 renderer should return text prompt") } +fn thinking_request(messages: Vec) -> ChatRequest { + let mut request = ChatRequest { + messages, + ..ChatRequest::for_test() + }; + request + .chat_options + .template_kwargs + .insert("thinking".to_string(), Value::Bool(true)); + request.chat_options.reasoning_effort = Some(ReasoningEffort::Low); + request +} + fn fixture_request(input_name: &str) -> ChatRequest { let mut request = fixture_chat_request(&fixture_path(input_name), deepseek_fixture_options()); request.chat_options.reasoning_effort = Some(ReasoningEffort::Low); @@ -71,6 +84,114 @@ fn renders_developer_tools_like_hf_python() { ); } +#[test] +fn drop_thinking_removes_developer_before_last_user() { + let request = thinking_request(vec![ + ChatMessage::developer("old instruction", None), + ChatMessage::assistant_blocks(vec![ + AssistantContentBlock::Reasoning { + text: "old reasoning".to_string(), + }, + AssistantContentBlock::Text { + text: "old answer".to_string(), + }, + ]), + ChatMessage::user("next question"), + ]); + + let rendered = render_request(&request); + + expect![ + "<|begin▁of▁sentence|>old answer<|end▁of▁sentence|><|User|>next question<|Assistant|>" + ] + .assert_eq(&rendered); +} + +#[test] +fn dropped_developer_does_not_hide_following_assistant() { + let request = thinking_request(vec![ + ChatMessage::user("old question"), + ChatMessage::developer("old instruction", None), + ChatMessage::assistant_text("old answer"), + ChatMessage::user("next question"), + ]); + + let rendered = render_request(&request); + + expect![ + "<|begin▁of▁sentence|><|User|>old question<|Assistant|>old answer<|end▁of▁sentence|><|User|>next question<|Assistant|>" + ] + .assert_eq(&rendered); +} + +#[test] +fn drop_thinking_keeps_last_developer() { + let request = thinking_request(vec![ + ChatMessage::user("old question"), + ChatMessage::assistant_text("old answer"), + ChatMessage::developer("latest instruction", None), + ]); + + let rendered = render_request(&request); + + expect![ + "<|begin▁of▁sentence|><|User|>old question<|Assistant|>old answer<|end▁of▁sentence|><|User|>latest instruction<|Assistant|>" + ] + .assert_eq(&rendered); +} + +#[test] +fn drop_thinking_false_keeps_historical_developer() { + let mut request = thinking_request(vec![ + ChatMessage::developer("old instruction", None), + ChatMessage::assistant_text("old answer"), + ChatMessage::user("next question"), + ]); + request + .chat_options + .template_kwargs + .insert("drop_thinking".to_string(), Value::Bool(false)); + + let rendered = render_request(&request); + + assert!(rendered.contains("<|User|>old instruction")); +} + +#[test] +fn drop_thinking_removes_developer_before_tool_response() { + let request = thinking_request(vec![ + ChatMessage::developer("old instruction", None), + ChatMessage::assistant_text("old answer"), + ChatMessage::tool_response("result", "call-1"), + ]); + + let rendered = render_request(&request); + + expect![ + "<|begin▁of▁sentence|>old answer<|end▁of▁sentence|><|User|>result<|Assistant|>" + ] + .assert_eq(&rendered); +} + +#[test] +fn tools_keep_historical_developer() { + let tool = ChatTool { + name: "lookup".to_string(), + description: None, + parameters: Value::Object(Default::default()), + strict: None, + }; + let request = thinking_request(vec![ + ChatMessage::developer("old instruction", Some(vec![tool])), + ChatMessage::assistant_text("old answer"), + ChatMessage::user("next question"), + ]); + + let rendered = render_request(&request); + + assert!(rendered.contains("<|User|>old instruction")); +} + #[test] fn reasoning_effort_max_adds_prefix_when_thinking_is_enabled() { let mut request = ChatRequest {