dynamo_renderer/deepseek/
v41.rs1use anyhow::{Context, Result, ensure};
7use serde_json::Value;
8
9use super::common::{ThinkingMode, resolve_thinking_mode, to_json};
10use super::v4::{Encoding, encode_messages_with_encoding};
11
12pub(super) fn find_last_user_index(messages: &[Value]) -> Option<usize> {
13 messages.iter().enumerate().rposition(|(index, message)| {
14 let role = message.get("role").and_then(Value::as_str);
15 role == Some("user") || (role == Some("system") && index > 0)
16 })
17}
18
19pub(super) fn drop_thinking_messages(mut messages: Vec<Value>) -> Vec<Value> {
20 if let Some(last_user) = find_last_user_index(&messages) {
21 for message in &mut messages[..last_user] {
22 if message.get("role").and_then(Value::as_str) == Some("assistant") {
23 message.as_object_mut().unwrap().remove("reasoning_content");
24 }
25 }
26 }
27 messages
28}
29
30pub(super) fn encode_arguments(tool_call: &Value) -> Result<String> {
31 let original = tool_call
32 .get("arguments")
33 .context("Missing tool arguments")?;
34 let mut arguments = original.clone();
35 for _ in 0..2 {
36 let Some(text) = arguments.as_str() else {
37 break;
38 };
39 let Ok(decoded) = serde_json::from_str(text) else {
40 break;
41 };
42 arguments = decoded;
43 }
44 if !arguments.is_object() {
45 arguments = serde_json::json!({"arguments": original});
46 }
47 let parameters = arguments
48 .as_object()
49 .unwrap()
50 .iter()
51 .map(|(name, value)| {
52 let content = value
53 .as_str()
54 .map(str::to_owned)
55 .unwrap_or_else(|| to_json(value));
56 format!(
57 "<|DSML| parameter name=\"{name}\" string=\"{}\">{content}</|DSML| parameter>",
58 value.is_string()
59 )
60 })
61 .collect::<Vec<_>>();
62 Ok(parameters.join("\n"))
63}
64
65fn normalize_text(messages: &mut [Value]) -> Result<()> {
66 for message in messages {
67 for field in ["tools", "tool_calls"] {
68 for tool in message
69 .get(field)
70 .and_then(Value::as_array)
71 .into_iter()
72 .flatten()
73 {
74 ensure!(
75 tool.get("namespace").is_none_or(Value::is_null)
76 && tool
77 .get("function")
78 .and_then(|function| function.get("namespace"))
79 .is_none_or(Value::is_null),
80 "DeepSeek V4.1 native formatter does not support explicit tool namespaces; use a qualified function name"
81 );
82 }
83 }
84 ensure!(
85 message.get("content_blocks").is_none(),
86 "DeepSeek V4.1 expects OpenAI text content blocks in content"
87 );
88 ensure!(
89 !message
90 .get("reasoning_content")
91 .and_then(Value::as_str)
92 .is_some_and(|text| text.contains("<|deepseek_image|>")),
93 "DeepSeek V4.1 native formatter supports text content only"
94 );
95 if message.get("role").and_then(Value::as_str) == Some("developer") {
96 message["role"] = Value::String("system".into());
97 }
98 if let Some(content) = message.get("content") {
99 let text = match content {
100 Value::Null => String::new(),
101 Value::String(text) => text.clone(),
102 Value::Array(blocks) => {
103 let mut texts = Vec::with_capacity(blocks.len());
104 for block in blocks {
105 ensure!(
106 block.get("type").and_then(Value::as_str) == Some("text"),
107 "DeepSeek V4.1 native formatter supports text content only"
108 );
109 texts.push(
110 block
111 .get("text")
112 .and_then(Value::as_str)
113 .context("Text block requires text")?,
114 );
115 }
116 texts.join("\n\n")
117 }
118 _ => anyhow::bail!("DeepSeek V4.1 message content must be text"),
119 };
120 ensure!(
121 !text.contains("<|deepseek_image|>"),
122 "DeepSeek V4.1 native formatter supports text content only"
123 );
124 message["content"] = Value::String(text);
125 }
126 }
127 Ok(())
128}
129
130pub fn encode_messages(
132 messages: &[Value],
133 thinking_mode: ThinkingMode,
134 drop_thinking: bool,
135 reasoning_effort: u8,
136) -> Result<String> {
137 ensure!(
138 (1..=100).contains(&reasoning_effort),
139 "DeepSeek V4.1 reasoning effort must be within 1–100"
140 );
141 let mut messages = messages.to_vec();
142 normalize_text(&mut messages)?;
143 encode_messages_with_encoding(
144 &messages,
145 thinking_mode,
146 true,
147 drop_thinking,
148 Encoding::V41(reasoning_effort),
149 )
150}
151
152#[derive(Debug, Default)]
155pub struct DeepSeekV41Formatter;
156
157impl crate::OAIPromptFormatter for DeepSeekV41Formatter {
158 fn supports_add_generation_prompt(&self) -> bool {
159 false
160 }
161
162 fn render(&self, req: &dyn crate::OAIChatLikeRequest) -> Result<String> {
163 let args = req.chat_template_args();
164 let effort = req
165 .reasoning_effort()
166 .map(|value| serde_json::to_value(value).context("Serialize reasoning effort"))
167 .transpose()?
168 .or_else(|| args.and_then(|args| args.get("reasoning_effort").cloned()));
169 let mut thinking_mode = resolve_thinking_mode(args, ThinkingMode::Thinking);
170 let budget = match effort.as_ref() {
171 None | Some(Value::Null) => 75,
172 Some(value) => match value.as_str() {
173 Some("none") => {
174 thinking_mode = ThinkingMode::Chat;
175 75
176 }
177 Some("low") => 50,
178 Some("high") => 75,
179 Some("max") => 100,
180 _ => value
181 .as_u64()
182 .filter(|v| (1..=100).contains(v))
183 .context("DeepSeek V4.1 reasoning effort must be low, high, max, none, or an integer within 1–100")?
184 as u8,
185 },
186 };
187 let drop_thinking = match args.and_then(|args| args.get("drop_thinking")) {
188 None => true,
189 Some(value) => value.as_bool().context("drop_thinking must be a boolean")?,
190 };
191 let mut messages: Vec<Value> =
192 serde_json::from_value(serde_json::to_value(req.messages())?)?;
193 let tools_enabled =
194 req.tool_choice().as_ref().and_then(|value| value.as_str()) != Some("none");
195 let tools = req
196 .tools()
197 .filter(|tools| tools_enabled && tools.len().is_some_and(|length| length > 0))
198 .map(serde_json::to_value)
199 .transpose()?;
200 let response_format = req
201 .response_format()
202 .map(serde_json::to_value)
203 .transpose()?;
204 if tools.is_some() || response_format.is_some() {
205 if !matches!(
206 messages
207 .first()
208 .and_then(|m| m.get("role"))
209 .and_then(Value::as_str),
210 Some("system" | "developer")
211 ) {
212 messages.insert(0, serde_json::json!({"role": "system", "content": ""}));
213 }
214 if let Some(tools) = tools {
215 messages[0]["tools"] = tools;
216 }
217 if let Some(response_format) = response_format {
218 messages[0]["response_format"] = response_format;
219 }
220 }
221 encode_messages(&messages, thinking_mode, drop_thinking, budget)
222 }
223}