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 <cppcoffee@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
This commit is contained in:
authored and GitHub committed 2026-09-16 09:49:13 +00:00
1 parent 78de9335df
commit 1469507d1b
4 files changed
+187 -51

No files matched your search

+59 -10
View File
@@ -50,8 +50,8 @@ impl From<Role> 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<Self> {
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<u64> {
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<String>,
},
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<MessagePart>),
}
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<Vec<MessagePart>> for MessageContent {
fn from(mut parts: Vec<MessagePart>) -> 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<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
+114 -27
View File
@@ -166,7 +166,7 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
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<String> = 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<String> = Vec::new();
for part in &tool_result.content {
match part {
let content: Vec<deepseek::MessagePart> = 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() {
"<Tool returned an empty string>".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<deepseek::RequestMessage>,
) {
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<deepseek::Thinking> {
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<()> {
+12 -12
View File
@@ -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,
+2 -2
View File
@@ -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
},