dynamo_renderer/deepseek/
common.rs1use anyhow::{Context, Result};
7use serde_json::Value as JsonValue;
8
9pub mod tokens {
11 pub const BOS: &str = "<|begin▁of▁sentence|>";
12 pub const EOS: &str = "<|end▁of▁sentence|>";
13 pub const THINKING_START: &str = "<think>";
14 pub const THINKING_END: &str = "</think>";
15 pub const DSML_TOKEN: &str = "|DSML|";
16 pub const USER_START: &str = "<|User|>";
17 pub const ASSISTANT_START: &str = "<|Assistant|>";
18 pub const LATEST_REMINDER: &str = "<|latest_reminder|>";
19
20 pub const TASK_ACTION: &str = "<|action|>";
22 pub const TASK_QUERY: &str = "<|query|>";
23 pub const TASK_AUTHORITY: &str = "<|authority|>";
24 pub const TASK_DOMAIN: &str = "<|domain|>";
25 pub const TASK_TITLE: &str = "<|title|>";
26 pub const TASK_READ_URL: &str = "<|read_url|>";
27}
28
29pub(crate) const TOOL_CALLS_BLOCK_NAME: &str = "tool_calls";
30
31pub(crate) const RESPONSE_FORMAT_TEMPLATE: &str =
32 "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}";
33
34pub(crate) const TOOLS_TEMPLATE: &str = r#"## Tools
35
36You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
37
38<{dsml_token}tool_calls>
39<{dsml_token}invoke name="$TOOL_NAME">
40<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
41...
42</{dsml_token}invoke>
43<{dsml_token}invoke name="$TOOL_NAME2">
44...
45</{dsml_token}invoke>
46</{dsml_token}tool_calls>
47
48String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
49
50If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
51
52Otherwise, output directly after {thinking_end_token} with tool calls or final response.
53
54### Available Tool Schemas
55
56{tool_schemas}
57
58You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
59"#;
60
61pub(crate) const TOOLS_SYSTEM_TEMPLATE: &str = r#"## Tools
63
64You have access to a set of tools you can use to answer the user's question.
65You can invoke functions by writing a "<{dsml_token}function_calls>" block like the following as part of your reply to the user:
66<{dsml_token}function_calls>
67<{dsml_token}invoke name="$FUNCTION_NAME">
68<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
69...
70</{dsml_token}invoke>
71<{dsml_token}invoke name="$FUNCTION_NAME2">
72...
73</{dsml_token}invoke>
74</{dsml_token}function_calls>
75
76String and scalar parameters should be specified as is without any escaping or quotes, while lists and objects should use JSON format. The "string" attribute should be set to "true" for string type parameters and "false" for other types (numbers, booleans, arrays, objects).
77
78If the thinking_mode is enabled, then after function results you should strongly consider outputting a thinking block. Here is an example:
79
80<{dsml_token}function_calls>
81...
82</{dsml_token}function_calls>
83
84<function_results>
85...
86</function_results>
87
88{thinking_start_token}...thinking about results{thinking_end_token}
89
90Here are the functions available in JSONSchema format:
91<functions>
92{tool_schemas}
93</functions>
94"#;
95
96pub(crate) const TOOL_CALL_TEMPLATE: &str =
97 "<{dsml_token}invoke name=\"{name}\">\n{arguments}\n</{dsml_token}invoke>";
98
99pub(crate) const TOOL_OUTPUT_TEMPLATE: &str = "\n<result>{content}</result>";
100
101pub(crate) const REASONING_EFFORT_HIGH: &str = "Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n";
102
103pub(crate) const REASONING_EFFORT_MAX: &str = "Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\nYou MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\nDo not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n";
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ThinkingMode {
108 Chat,
109 Thinking,
110}
111
112impl ThinkingMode {
113 pub fn as_str(&self) -> &'static str {
114 match self {
115 ThinkingMode::Chat => "chat",
116 ThinkingMode::Thinking => "thinking",
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum ReasoningEffort {
124 Max,
125 High,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub(crate) enum NormalizeNonText {
130 SerializeJson,
131 LeaveUntouched,
132}
133
134pub(crate) fn to_json(value: &JsonValue) -> String {
138 use serde::Serialize;
139 use serde_json::ser::Formatter;
140 use std::io;
141
142 struct PythonFormatter;
143
144 impl Formatter for PythonFormatter {
145 fn begin_array_value<W: ?Sized + io::Write>(
146 &mut self,
147 writer: &mut W,
148 first: bool,
149 ) -> io::Result<()> {
150 if first {
151 Ok(())
152 } else {
153 writer.write_all(b", ")
154 }
155 }
156
157 fn begin_object_key<W: ?Sized + io::Write>(
158 &mut self,
159 writer: &mut W,
160 first: bool,
161 ) -> io::Result<()> {
162 if first {
163 Ok(())
164 } else {
165 writer.write_all(b", ")
166 }
167 }
168
169 fn begin_object_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
170 writer.write_all(b": ")
171 }
172 }
173
174 let mut buf = Vec::with_capacity(64);
176 let mut ser = serde_json::Serializer::with_formatter(&mut buf, PythonFormatter);
177 value
178 .serialize(&mut ser)
179 .expect("JsonValue serialization to Vec<u8> is infallible");
180 String::from_utf8(buf).expect("serde_json output is always valid UTF-8")
181}
182
183pub(crate) fn render_tools(template: &str, tools: &[JsonValue]) -> String {
184 let tools_json: Vec<String> = tools
185 .iter()
186 .filter_map(|tool| tool.get("function"))
187 .map(to_json)
188 .collect();
189
190 template
193 .replace("{dsml_token}", tokens::DSML_TOKEN)
194 .replace("{thinking_start_token}", tokens::THINKING_START)
195 .replace("{thinking_end_token}", tokens::THINKING_END)
196 .replace("{tool_schemas}", &tools_json.join("\n"))
197}
198
199pub(crate) fn find_last_user_index(messages: &[JsonValue]) -> Option<usize> {
200 messages
201 .iter()
202 .enumerate()
203 .rev()
204 .find(|(_, msg)| {
205 msg.get("role")
206 .and_then(|r| r.as_str())
207 .map(|r| r == "user" || r == "developer")
208 .unwrap_or(false)
209 })
210 .map(|(idx, _)| idx)
211}
212
213pub(crate) fn extract_visible_text(content: &JsonValue) -> String {
214 match content {
215 JsonValue::String(text) => text.clone(),
216 JsonValue::Array(items) => items
217 .iter()
218 .filter_map(|item| {
219 if let Some(text) = item.as_str() {
220 return Some(text.to_string());
221 }
222 let item_type = item.get("type").and_then(|v| v.as_str());
223 if item_type == Some("text") {
224 return item
225 .get("text")
226 .and_then(|v| v.as_str())
227 .map(|text| text.to_string());
228 }
229 tracing::warn!(
230 chunk_type = item_type.unwrap_or("unknown"),
231 "DeepSeek formatter dropped non-text content chunk while normalizing message content",
232 );
233 None
234 })
235 .collect::<String>(),
236 _ => to_json(content),
237 }
238}
239
240pub(crate) fn normalize_message_contents(messages: &mut [JsonValue], non_text: NormalizeNonText) {
241 for msg in messages {
242 let Some(content) = msg.get("content") else {
243 continue;
244 };
245 if !content.is_string()
246 && !content.is_array()
247 && non_text == NormalizeNonText::LeaveUntouched
248 {
249 continue;
250 }
251 let normalized = extract_visible_text(content);
252 if let Some(obj) = msg.as_object_mut() {
253 obj.insert("content".to_string(), JsonValue::String(normalized));
254 }
255 }
256}
257
258pub(crate) fn encode_arguments_to_dsml(tool_call: &JsonValue) -> Result<String> {
259 let arguments_str = tool_call
260 .get("arguments")
261 .and_then(|a| a.as_str())
262 .context("Missing or invalid 'arguments' field")?;
263
264 let arguments: JsonValue = match serde_json::from_str(arguments_str) {
266 Ok(v) => v,
267 Err(_) => serde_json::json!({ "arguments": arguments_str }),
268 };
269
270 let arguments_obj = arguments
271 .as_object()
272 .context("Arguments must be a JSON object")?;
273
274 let mut params = Vec::new();
275 for (key, value) in arguments_obj {
276 let value_str = if let Some(vs) = value.as_str() {
277 vs.to_string()
278 } else {
279 to_json(value)
280 };
281 params.push(format!(
282 "<{}parameter name=\"{}\" string=\"{}\">{}</{}parameter>",
283 tokens::DSML_TOKEN,
284 key,
285 if value.is_string() { "true" } else { "false" },
286 value_str,
287 tokens::DSML_TOKEN
288 ));
289 }
290
291 Ok(params.join("\n"))
292}
293
294pub(crate) fn task_token(task: &str) -> Option<&'static str> {
295 match task {
296 "action" => Some(tokens::TASK_ACTION),
297 "query" => Some(tokens::TASK_QUERY),
298 "authority" => Some(tokens::TASK_AUTHORITY),
299 "domain" => Some(tokens::TASK_DOMAIN),
300 "title" => Some(tokens::TASK_TITLE),
301 "read_url" => Some(tokens::TASK_READ_URL),
302 _ => None,
303 }
304}
305
306const USER_FIELDS_TO_PRESERVE: [&str; 3] = ["task", "wo_eos", "mask"];
307
308fn preserve_user_fields(target: &mut JsonValue, source: &JsonValue) {
309 if let Some(obj) = target.as_object_mut() {
310 for key in USER_FIELDS_TO_PRESERVE {
311 if let Some(v) = source.get(key) {
312 obj.insert(key.to_string(), v.clone());
313 }
314 }
315 }
316}
317
318pub(crate) fn merge_tool_messages(messages: &[JsonValue]) -> Vec<JsonValue> {
321 let mut merged: Vec<JsonValue> = Vec::with_capacity(messages.len());
322
323 for msg in messages {
324 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
325
326 if role == "tool" {
327 let tool_block = serde_json::json!({
328 "type": "tool_result",
329 "tool_use_id": msg.get("tool_call_id").cloned().unwrap_or_else(|| JsonValue::String(String::new())),
330 "content": msg.get("content").cloned().unwrap_or_else(|| JsonValue::String(String::new())),
331 });
332
333 let can_merge = merged
334 .last()
335 .map(|m| {
336 m.get("role").and_then(|r| r.as_str()) == Some("user")
337 && m.get("content_blocks").is_some()
338 })
339 .unwrap_or(false);
340
341 if can_merge {
342 let last = merged.last_mut().unwrap();
343 if let Some(blocks) = last
344 .as_object_mut()
345 .and_then(|o| o.get_mut("content_blocks"))
346 .and_then(|v| v.as_array_mut())
347 {
348 blocks.push(tool_block);
349 }
350 } else {
351 merged.push(serde_json::json!({
352 "role": "user",
353 "content_blocks": [tool_block],
354 }));
355 }
356 } else if role == "user" {
357 let text = msg
358 .get("content")
359 .and_then(|c| c.as_str())
360 .unwrap_or("")
361 .to_string();
362 let text_block = serde_json::json!({ "type": "text", "text": text });
363
364 let can_merge = merged
365 .last()
366 .map(|m| {
367 m.get("role").and_then(|r| r.as_str()) == Some("user")
368 && m.get("content_blocks").is_some()
369 && m.get("task").map(|v| v.is_null()).unwrap_or(true)
370 })
371 .unwrap_or(false);
372
373 if can_merge {
374 let last = merged.last_mut().unwrap();
375 let appended = last
376 .as_object_mut()
377 .and_then(|o| o.get_mut("content_blocks"))
378 .and_then(|v| v.as_array_mut())
379 .map(|blocks| {
380 blocks.push(text_block);
381 })
382 .is_some();
383 if appended {
384 preserve_user_fields(last, msg);
385 }
386 } else {
387 let mut new_msg = serde_json::json!({
388 "role": "user",
389 "content": text,
390 "content_blocks": [text_block],
391 });
392 preserve_user_fields(&mut new_msg, msg);
393 merged.push(new_msg);
394 }
395 } else {
396 merged.push(msg.clone());
397 }
398 }
399
400 merged
401}
402
403pub(crate) fn sort_tool_results_by_call_order(mut messages: Vec<JsonValue>) -> Vec<JsonValue> {
406 use std::collections::HashMap;
407 let mut last_order: HashMap<String, usize> = HashMap::new();
408
409 for msg in &mut messages {
410 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
411 if role == "assistant" {
412 if let Some(tcs) = msg.get("tool_calls").and_then(|t| t.as_array()) {
413 last_order.clear();
414 for (idx, tc) in tcs.iter().enumerate() {
415 let id = tc
416 .get("id")
417 .and_then(|v| v.as_str())
418 .or_else(|| {
419 tc.get("function")
420 .and_then(|f| f.get("id"))
421 .and_then(|v| v.as_str())
422 })
423 .unwrap_or("");
424 if !id.is_empty() {
425 last_order.insert(id.to_string(), idx);
426 }
427 }
428 }
429 } else if role == "user" && !last_order.is_empty() {
430 let Some(blocks) = msg
431 .as_object_mut()
432 .and_then(|o| o.get_mut("content_blocks"))
433 .and_then(|v| v.as_array_mut())
434 else {
435 continue;
436 };
437
438 let tool_positions: Vec<usize> = blocks
440 .iter()
441 .enumerate()
442 .filter(|(_, b)| b.get("type").and_then(|v| v.as_str()) == Some("tool_result"))
443 .map(|(i, _)| i)
444 .collect();
445
446 if tool_positions.len() > 1 {
447 let start = *tool_positions
448 .first()
449 .expect("tool_positions has length > 1");
450 let end = *tool_positions
451 .last()
452 .expect("tool_positions has length > 1");
453 let is_contiguous = end - start + 1 == tool_positions.len();
454
455 if is_contiguous {
456 blocks[start..=end].sort_by_key(|b| {
458 let id = b.get("tool_use_id").and_then(|v| v.as_str()).unwrap_or("");
459 *last_order.get(id).unwrap_or(&0)
460 });
461 } else {
462 let mut tool_blocks: Vec<JsonValue> = tool_positions
464 .iter()
465 .map(|&i| std::mem::take(&mut blocks[i]))
466 .collect();
467
468 tool_blocks.sort_by_key(|b| {
469 let id = b.get("tool_use_id").and_then(|v| v.as_str()).unwrap_or("");
470 *last_order.get(id).unwrap_or(&0)
471 });
472
473 for (sorted_idx, &pos) in tool_positions.iter().enumerate() {
474 blocks[pos] = std::mem::take(&mut tool_blocks[sorted_idx]);
475 }
476 }
477 }
478 }
479 }
480
481 messages
482}
483
484pub(crate) fn drop_thinking_messages(messages: Vec<JsonValue>) -> Vec<JsonValue> {
486 let last_user_idx = find_last_user_index(&messages);
487 let mut out = Vec::with_capacity(messages.len());
488 const KEEP: &[&str] = &[
489 "user",
490 "system",
491 "tool",
492 "latest_reminder",
493 "direct_search_results",
494 ];
495
496 for (idx, mut msg) in messages.into_iter().enumerate() {
497 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
498 if KEEP.contains(&role) || last_user_idx.is_none_or(|u| idx >= u) {
499 out.push(msg);
500 } else if role == "assistant" {
501 if let Some(obj) = msg.as_object_mut() {
502 obj.remove("reasoning_content");
503 }
504 out.push(msg);
505 }
506 }
508 out
509}
510
511pub(crate) fn resolve_thinking_mode(
512 args: Option<&std::collections::HashMap<String, serde_json::Value>>,
513 default_mode: ThinkingMode,
514) -> ThinkingMode {
515 if let Some(enabled) = crate::thinking_bool_from_args(args) {
516 return if enabled {
517 ThinkingMode::Thinking
518 } else {
519 ThinkingMode::Chat
520 };
521 }
522 if let Some(args) = args
523 && let Some(mode) = args.get("thinking_mode").and_then(|v| v.as_str())
524 {
525 match mode {
526 "chat" => return ThinkingMode::Chat,
527 "thinking" => return ThinkingMode::Thinking,
528 _ => {}
529 }
530 }
531 default_mode
532}
533
534pub(crate) fn inject_tools_and_response_format(
535 messages_array: &mut Vec<JsonValue>,
536 req: &dyn crate::OAIChatLikeRequest,
537) -> Result<()> {
538 let tools_json = req
539 .tools()
540 .map(|t| serde_json::to_value(&t))
541 .transpose()
542 .context("Failed to convert tools to JSON")?;
543
544 let tools_json = match req.tool_choice() {
550 Some(ref tc) if tc.as_str() == Some("none") => None,
551 _ => tools_json,
552 };
553
554 let response_format_json = req
555 .response_format()
556 .map(|rf| serde_json::to_value(&rf))
557 .transpose()
558 .context("Failed to convert response_format to JSON")?;
559
560 if tools_json.is_some() || response_format_json.is_some() {
561 let system_idx = messages_array
562 .iter()
563 .position(|msg| msg.get("role").and_then(|r| r.as_str()) == Some("system"));
564
565 if let Some(idx) = system_idx {
566 if let Some(msg) = messages_array.get_mut(idx)
567 && let Some(obj) = msg.as_object_mut()
568 {
569 if let Some(tools) = tools_json {
570 obj.insert("tools".to_string(), tools);
571 }
572 if let Some(rf) = response_format_json {
573 obj.insert("response_format".to_string(), rf);
574 }
575 }
576 } else {
577 let mut system_msg = serde_json::json!({
578 "role": "system",
579 "content": ""
580 });
581 if let Some(obj) = system_msg.as_object_mut() {
582 if let Some(tools) = tools_json {
583 obj.insert("tools".to_string(), tools);
584 }
585 if let Some(rf) = response_format_json {
586 obj.insert("response_format".to_string(), rf);
587 }
588 }
589 messages_array.insert(0, system_msg);
590 }
591 }
592 Ok(())
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use serde_json::json;
599
600 #[test]
601 fn test_extract_visible_text_from_content_array() {
602 let content = json!([
603 {"type": "text", "text": "who "},
604 {"type": "text", "text": "are "},
605 {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}},
606 {"type": "text", "text": "you?"}
607 ]);
608 assert_eq!(extract_visible_text(&content), "who are you?");
609 }
610
611 #[test]
612 fn test_render_tools_preserves_placeholder_text_inside_tool_schema() {
613 let tools = json!([{
614 "type": "function",
615 "function": {
616 "name": "placeholder_tool",
617 "description": "literal {dsml_token} {thinking_start_token} {thinking_end_token}",
618 "parameters": {"type": "object", "properties": {}}
619 }
620 }]);
621 let rendered = render_tools(
622 "static {dsml_token} {thinking_start_token} {thinking_end_token}\n{tool_schemas}",
623 tools.as_array().unwrap(),
624 );
625
626 assert!(rendered.starts_with(&format!(
627 "static {} {} {}\n",
628 tokens::DSML_TOKEN,
629 tokens::THINKING_START,
630 tokens::THINKING_END
631 )));
632 assert!(
633 rendered.contains("literal {dsml_token} {thinking_start_token} {thinking_end_token}")
634 );
635 }
636}