From 62cea4e6b03336b4bb974b0268fe94f643bc8c62 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 23 Mar 2026 14:38:40 +0100 Subject: [PATCH] deepseek: Fix empty text segments causing issues (#52199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Deepseek seems to have started to return empty text deltas, which causes issues cause their upstream API does not like empty text blocks to be included in their request. In practice this caused issues like `An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'..., because the payload looked like this: ``` User { content: "Call the now tool a bunch of times", }, Assistant { content: Some( "", ), }, Tool { content: "The current datetime is 2026-03-23T13:21:24.084848+00:00.", tool_call_id: "call_00_iozCfHXJAPR13XwHiAwJ9OEw", }, ``` Now we filter out that empty text, which seems to fix the issue. This matches our implementation for OpenAI Closes #51854 ## How to Review ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - deepseek: Fixed issue with tool calling (`An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'...`) --- .../language_models/src/provider/deepseek.rs | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index e27bd510dbb0b0f518e615e31fc194675a5c3cfe..bd2469d865fd8421d6ad31208e6a4be413c0fe14 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -331,15 +331,25 @@ pub fn into_deepseek( for message in request.messages { for content in message.content { match content { - MessageContent::Text(text) => messages.push(match message.role { - Role::User => deepseek::RequestMessage::User { content: text }, - Role::Assistant => deepseek::RequestMessage::Assistant { - content: Some(text), - tool_calls: Vec::new(), - reasoning_content: current_reasoning.take(), - }, - Role::System => deepseek::RequestMessage::System { content: text }, - }), + MessageContent::Text(text) => { + let should_add = if message.role == Role::User { + !text.trim().is_empty() + } else { + !text.is_empty() + }; + + if should_add { + messages.push(match message.role { + Role::User => deepseek::RequestMessage::User { content: text }, + Role::Assistant => deepseek::RequestMessage::Assistant { + content: Some(text), + tool_calls: Vec::new(), + reasoning_content: current_reasoning.take(), + }, + Role::System => deepseek::RequestMessage::System { content: text }, + }); + } + } MessageContent::Thinking { text, .. } => { // Accumulate reasoning content for next assistant message current_reasoning.get_or_insert_default().push_str(&text); @@ -445,7 +455,9 @@ impl DeepSeekEventMapper { }; let mut events = Vec::new(); - if let Some(content) = choice.delta.content.clone() { + if let Some(content) = choice.delta.content.clone() + && !content.is_empty() + { events.push(Ok(LanguageModelCompletionEvent::Text(content))); }