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:
1 parent
78de9335df
commit
1469507d1b
4 files changed
+187
-51
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user