From 1469507d1ba6b2a70cf96da3644d632158c36513 Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Wed, 16 Sep 2026 09:49:13 +0000 Subject: [PATCH] deepseek: Update DeepSeek Flash to V4.1 (#64014) # Objective deepseek API: https://api-docs.deepseek.com/updates/ ## Solution deepseek rename deepseek v4 flash to deepseek flash https://github.com/user-attachments/assets/72c473c9-a4bf-49e0-a7d6-0217a6dcc65b --- Release Notes: - agent: Added support for DeepSeek Flash 4.1 --------- Signed-off-by: Xiaobo Liu Co-authored-by: Bennet Bo Fenner Co-authored-by: Bennet Bo Fenner --- crates/deepseek/src/deepseek.rs | 69 +++++++-- .../language_models/src/provider/deepseek.rs | 141 ++++++++++++++---- crates/opencode/src/opencode.rs | 24 +-- docs/src/ai/use-api-access.md | 4 +- 4 files changed, 187 insertions(+), 51 deletions(-) diff --git a/crates/deepseek/src/deepseek.rs b/crates/deepseek/src/deepseek.rs index 95cb4523df..a12440f49f 100644 --- a/crates/deepseek/src/deepseek.rs +++ b/crates/deepseek/src/deepseek.rs @@ -50,8 +50,8 @@ impl From for String { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub enum Model { - #[serde(rename = "deepseek-v4-flash")] - V4Flash, + #[serde(rename = "deepseek-flash")] + V4_1Flash, #[serde(rename = "deepseek-v4-pro")] #[default] V4Pro, @@ -67,12 +67,12 @@ pub enum Model { impl Model { pub fn default_fast() -> Self { - Model::V4Flash + Model::V4_1Flash } pub fn from_id(id: &str) -> Result { match id { - "deepseek-v4-flash" => Ok(Self::V4Flash), + "deepseek-flash" => Ok(Self::V4_1Flash), "deepseek-v4-pro" => Ok(Self::V4Pro), _ => anyhow::bail!("invalid model id {id}"), } @@ -80,7 +80,7 @@ impl Model { pub fn id(&self) -> &str { match self { - Self::V4Flash => "deepseek-v4-flash", + Self::V4_1Flash => "deepseek-flash", Self::V4Pro => "deepseek-v4-pro", Self::Custom { name, .. } => name, } @@ -88,7 +88,7 @@ impl Model { pub fn display_name(&self) -> &str { match self { - Self::V4Flash => "DeepSeek V4 Flash", + Self::V4_1Flash => "DeepSeek V4.1 Flash", Self::V4Pro => "DeepSeek V4 Pro", Self::Custom { name, display_name, .. @@ -96,16 +96,20 @@ impl Model { } } + pub fn supports_images(&self) -> bool { + matches!(self, Self::V4_1Flash) + } + pub fn max_token_count(&self) -> u64 { match self { - Self::V4Flash | Self::V4Pro => 1_000_000, + Self::V4_1Flash | Self::V4Pro => 1_000_000, Self::Custom { max_tokens, .. } => *max_tokens, } } pub fn max_output_tokens(&self) -> Option { match self { - Self::V4Flash | Self::V4Pro => Some(384_000), + Self::V4_1Flash | Self::V4Pro => Some(384_000), Self::Custom { max_output_tokens, .. } => *max_output_tokens, @@ -195,17 +199,62 @@ pub enum RequestMessage { reasoning_content: Option, }, User { - content: String, + content: MessageContent, }, System { content: String, }, Tool { - content: String, + content: MessageContent, tool_call_id: String, }, } +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(untagged)] +pub enum MessageContent { + Plain(String), + Multipart(Vec), +} + +impl MessageContent { + pub fn push_part(&mut self, part: MessagePart) { + match self { + MessageContent::Plain(text) => { + let text = std::mem::take(text); + *self = MessageContent::Multipart(vec![MessagePart::Text { text }, part]); + } + MessageContent::Multipart(parts) => parts.push(part), + } + } +} + +impl From> for MessageContent { + fn from(mut parts: Vec) -> Self { + if let [MessagePart::Text { text }] = parts.as_mut_slice() { + MessageContent::Plain(std::mem::take(text)) + } else { + MessageContent::Multipart(parts) + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(tag = "type")] +pub enum MessagePart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image_url")] + Image { image_url: ImageUrl }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct ImageUrl { + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ToolCall { pub id: String, diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 19a988adc2..64fe5fbd69 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -166,7 +166,7 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider { fn provided_models(&self, cx: &App) -> Vec> { let mut models = IndexMap::default(); - models.insert("deepseek-v4-flash", deepseek::Model::V4Flash); + models.insert("deepseek-flash", deepseek::Model::V4_1Flash); models.insert("deepseek-v4-pro", deepseek::Model::V4Pro); for available_model in &Self::settings(cx).available_models { @@ -284,7 +284,7 @@ impl LanguageModel for DeepSeekLanguageModel { fn supports_thinking(&self) -> bool { matches!( self.model, - deepseek::Model::V4Flash | deepseek::Model::V4Pro + deepseek::Model::V4_1Flash | deepseek::Model::V4Pro ) } @@ -317,7 +317,7 @@ impl LanguageModel for DeepSeekLanguageModel { } fn supports_images(&self) -> bool { - false + self.model.supports_images() } fn telemetry_id(&self) -> String { @@ -376,6 +376,8 @@ pub fn into_deepseek( .as_ref() .is_some_and(|thinking| thinking.kind == deepseek::ThinkingType::Enabled); + let supports_images = model.supports_images(); + let mut messages = Vec::new(); let mut current_reasoning: Option = None; @@ -390,15 +392,24 @@ pub fn into_deepseek( }; 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 }, - }); + match message.role { + Role::User => { + add_user_message_content_part( + deepseek::MessagePart::Text { text }, + &mut messages, + ); + } + Role::Assistant => { + messages.push(deepseek::RequestMessage::Assistant { + content: Some(text), + tool_calls: Vec::new(), + reasoning_content: current_reasoning.take(), + }); + } + Role::System => { + messages.push(deepseek::RequestMessage::System { content: text }); + } + } } } MessageContent::Thinking { text, .. } => { @@ -406,6 +417,17 @@ pub fn into_deepseek( current_reasoning.get_or_insert_default().push_str(&text); } MessageContent::RedactedThinking(_) => {} + MessageContent::Image(image) if message.role == Role::User => { + add_user_message_content_part( + deepseek::MessagePart::Image { + image_url: deepseek::ImageUrl { + url: image.to_base64_url(), + detail: None, + }, + }, + &mut messages, + ); + } MessageContent::Image(_) => {} MessageContent::Compaction(_) => {} MessageContent::ToolUse(tool_use) => { @@ -436,24 +458,32 @@ pub fn into_deepseek( } } MessageContent::ToolResult(tool_result) => { - let mut text_parts: Vec = Vec::new(); - for part in &tool_result.content { - match part { + let content: Vec = tool_result + .content + .iter() + .filter_map(|part| match part { LanguageModelToolResultContent::Text(text) => { - text_parts.push(text.to_string()); + Some(deepseek::MessagePart::Text { + text: text.to_string(), + }) } - LanguageModelToolResultContent::Image(_) => { - text_parts.push("[Tool responded with an image]".to_string()); + LanguageModelToolResultContent::Image(image) => { + if supports_images { + Some(deepseek::MessagePart::Image { + image_url: deepseek::ImageUrl { + url: image.to_base64_url(), + detail: None, + }, + }) + } else { + None + } } - } - } - let content = if text_parts.is_empty() { - "".to_string() - } else { - text_parts.join("\n") - }; + }) + .collect(); + messages.push(deepseek::RequestMessage::Tool { - content, + content: content.into(), tool_call_id: tool_result.tool_use_id.to_string(), }); } @@ -508,12 +538,28 @@ pub fn into_deepseek( }) } +fn add_user_message_content_part( + new_part: deepseek::MessagePart, + messages: &mut Vec, +) { + match messages.last_mut() { + Some(deepseek::RequestMessage::User { content }) => { + content.push_part(new_part); + } + _ => { + messages.push(deepseek::RequestMessage::User { + content: deepseek::MessageContent::from(vec![new_part]), + }); + } + } +} + fn deepseek_thinking( model: &deepseek::Model, thinking_allowed: bool, ) -> Option { let kind = match model { - deepseek::Model::V4Flash | deepseek::Model::V4Pro => { + deepseek::Model::V4_1Flash | deepseek::Model::V4Pro => { if thinking_allowed { deepseek::ThinkingType::Enabled } else { @@ -671,6 +717,47 @@ impl DeepSeekEventMapper { #[cfg(test)] mod tests { use super::*; + use language_model::{LanguageModelImage, LanguageModelRequestMessage}; + use serde_json::json; + + #[test] + fn serializes_deepseek_image_parts() -> Result<()> { + let image = LanguageModelImage { + source: SharedString::from("aGVsbG8="), + }; + let image_url = image.to_base64_url(); + let request = into_deepseek( + LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::User, + content: vec![ + MessageContent::Text("Describe this".to_string()), + MessageContent::Image(image), + ], + cache: false, + reasoning_details: None, + }], + ..Default::default() + }, + &deepseek::Model::V4_1Flash, + Some(1024), + )?; + + assert_eq!( + serde_json::to_value(&request.messages)?, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Describe this" }, + { "type": "image_url", "image_url": { "url": image_url } } + ] + } + ]) + ); + + Ok(()) + } #[test] fn request_output_limits_reach_deepseek_payloads() -> Result<()> { diff --git a/crates/opencode/src/opencode.rs b/crates/opencode/src/opencode.rs index 8e7f998b37..51d95d385c 100644 --- a/crates/opencode/src/opencode.rs +++ b/crates/opencode/src/opencode.rs @@ -138,8 +138,8 @@ pub enum Model { // -- OpenAI Chat Completions protocol models -- #[serde(rename = "deepseek-v4-pro")] DeepSeekV4Pro, - #[serde(rename = "deepseek-v4-flash")] - DeepSeekV4Flash, + #[serde(rename = "deepseek-flash")] + DeepSeekV4_1Flash, #[serde(rename = "minimax-m2.5")] MiniMaxM2_5, #[serde(rename = "glm-5")] @@ -226,7 +226,6 @@ impl Model { | Self::MiniMaxM2_7 | Self::MiniMaxM3 | Self::DeepSeekV4Pro - | Self::DeepSeekV4Flash | Self::Gpt5_6Luna | Self::Qwen3_6Plus => &[OpenCodeSubscription::Zen, OpenCodeSubscription::Go], @@ -237,6 +236,7 @@ impl Model { | Self::Qwen3_7Plus | Self::Qwen3_7Max | Self::Qwen3_8Max + | Self::DeepSeekV4_1Flash | Self::Hy3 => &[OpenCodeSubscription::Go], // Deprecated on Go (per models.dev); still offered on Zen @@ -295,7 +295,7 @@ impl Model { Self::Gemini3_7Flash => "gemini-3.7-flash", Self::DeepSeekV4Pro => "deepseek-v4-pro", - Self::DeepSeekV4Flash => "deepseek-v4-flash", + Self::DeepSeekV4_1Flash => "deepseek-flash", Self::MiniMaxM2_5 => "minimax-m2.5", Self::Glm5 => "glm-5", Self::Glm5_1 => "glm-5.1", @@ -367,7 +367,7 @@ impl Model { Self::Gemini3_7Flash => "Gemini 3.7 Flash", Self::DeepSeekV4Pro => "DeepSeek V4 Pro", - Self::DeepSeekV4Flash => "DeepSeek V4 Flash", + Self::DeepSeekV4_1Flash => "DeepSeek V4.1 Flash", Self::MiniMaxM2_5 => "MiniMax M2.5", Self::Glm5 => "GLM 5", Self::Glm5_1 => "GLM 5.1", @@ -468,7 +468,7 @@ impl Model { | Self::MimoV2_5Pro | Self::MimoV2_5 | Self::DeepSeekV4Pro - | Self::DeepSeekV4Flash + | Self::DeepSeekV4_1Flash | Self::Hy3 => ApiProtocol::OpenAiChat, Self::Grok4_6 | Self::Grok4_5 | Self::MuseSpark1_2 => ApiProtocol::OpenAiResponses, @@ -480,7 +480,7 @@ impl Model { pub fn interleaved_reasoning(&self) -> bool { match self { Self::DeepSeekV4Pro - | Self::DeepSeekV4Flash + | Self::DeepSeekV4_1Flash | Self::KimiK2_5 | Self::KimiK2_6 | Self::KimiK2_7Code @@ -571,7 +571,7 @@ impl Model { } Self::Qwen3_8Max | Self::Qwen3_7Max | Self::Qwen3_7Plus => 1_000_000, Self::Hy3 => 256_000, - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => 1_000_000, + Self::DeepSeekV4Pro | Self::DeepSeekV4_1Flash => 1_000_000, Self::Custom { max_tokens, .. } => *max_tokens, } @@ -655,7 +655,7 @@ impl Model { Some(65_536) } Self::Qwen3_8Max => Some(131_072), - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000), + Self::DeepSeekV4Pro | Self::DeepSeekV4_1Flash => Some(384_000), Self::MimoV2_5Pro | Self::MimoV2_5 => Some(128_000), Self::Hy3 => Some(64_000), @@ -730,7 +730,8 @@ impl Model { | Self::Qwen3_6Plus | Self::Qwen3_7Plus | Self::Qwen3_8Max - | Self::MiniMaxM3 => true, + | Self::MiniMaxM3 + | Self::DeepSeekV4_1Flash => true, // OpenAI-compatible models without image support Self::MiniMaxM2_5 @@ -741,7 +742,6 @@ impl Model { | Self::MiniMaxM2_7 | Self::MimoV2_5Pro | Self::DeepSeekV4Pro - | Self::DeepSeekV4Flash | Self::Qwen3_7Max | Self::Hy3 => false, @@ -857,7 +857,7 @@ impl Model { ]), // DeepSeek models - Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(vec![ + Self::DeepSeekV4Pro | Self::DeepSeekV4_1Flash => Some(vec![ // OpenCode also supports Low&Medium but as per DeepSeek those are mapped to High ReasoningEffort::High, ReasoningEffort::Max, diff --git a/docs/src/ai/use-api-access.md b/docs/src/ai/use-api-access.md index 2064fce9c1..ea8911113b 100644 --- a/docs/src/ai/use-api-access.md +++ b/docs/src/ai/use-api-access.md @@ -285,8 +285,8 @@ limits, or a custom endpoint. "api_url": "https://api.deepseek.com/v1", "available_models": [ { - "name": "deepseek-v4-flash", - "display_name": "DeepSeek V4 Flash", + "name": "deepseek-flash", + "display_name": "DeepSeek V4.1 Flash", "max_tokens": 1000000, "max_output_tokens": 384000 },