1use 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 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";
97
98pub(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";
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum ThinkingMode {
103 Chat,
104 Thinking,
105}
106
107impl ThinkingMode {
108 pub fn as_str(&self) -> &'static str {
109 match self {
110 ThinkingMode::Chat => "chat",
111 ThinkingMode::Thinking => "thinking",
112 }
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum ReasoningEffort {
119 Max,
120 High,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub(crate) enum NormalizeNonText {
125 SerializeJson,
126 LeaveUntouched,
127}
128
129pub(crate) fn to_json(value: &JsonValue) -> String {
133 use serde::Serialize;
134 use serde_json::ser::Formatter;
135 use std::io;
136
137 struct PythonFormatter;
138
139 impl Formatter for PythonFormatter {
140 fn begin_array_value<W: ?Sized + io::Write>(
141 &mut self,
142 writer: &mut W,
143 first: bool,
144 ) -> io::Result<()> {
145 if first {
146 Ok(())
147 } else {
148 writer.write_all(b", ")
149 }
150 }
151
152 fn begin_object_key<W: ?Sized + io::Write>(
153 &mut self,
154 writer: &mut W,
155 first: bool,
156 ) -> io::Result<()> {
157 if first {
158 Ok(())
159 } else {
160 writer.write_all(b", ")
161 }
162 }
163
164 fn begin_object_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
165 writer.write_all(b": ")
166 }
167 }
168
169 let mut buf = Vec::with_capacity(64);
171 let mut ser = serde_json::Serializer::with_formatter(&mut buf, PythonFormatter);
172 value
173 .serialize(&mut ser)
174 .expect("JsonValue serialization to Vec<u8> is infallible");
175 String::from_utf8(buf).expect("serde_json output is always valid UTF-8")
176}
177
178pub(crate) fn render_tools(template: &str, tools: &[JsonValue]) -> String {
179 let tools_json: Vec<String> = tools
180 .iter()
181 .filter_map(|tool| tool.get("function"))
182 .map(to_json)
183 .collect();
184
185 template
188 .replace("{dsml_token}", tokens::DSML_TOKEN)
189 .replace("{thinking_start_token}", tokens::THINKING_START)
190 .replace("{thinking_end_token}", tokens::THINKING_END)
191 .replace("{tool_schemas}", &tools_json.join("\n"))
192}
193
194pub(crate) fn find_last_user_index(messages: &[JsonValue]) -> Option<usize> {
195 messages
196 .iter()
197 .enumerate()
198 .rev()
199 .find(|(_, msg)| {
200 msg.get("role")
201 .and_then(|r| r.as_str())
202 .map(|r| r == "user" || r == "developer")
203 .unwrap_or(false)
204 })
205 .map(|(idx, _)| idx)
206}
207
208pub(crate) fn extract_visible_text(content: &JsonValue) -> String {
209 match content {
210 JsonValue::String(text) => text.clone(),
211 JsonValue::Array(items) => items
212 .iter()
213 .filter_map(|item| {
214 if let Some(text) = item.as_str() {
215 return Some(text.to_string());
216 }
217 let item_type = item.get("type").and_then(|v| v.as_str());
218 if item_type == Some("text") {
219 return item
220 .get("text")
221 .and_then(|v| v.as_str())
222 .map(|text| text.to_string());
223 }
224 tracing::warn!(
225 chunk_type = item_type.unwrap_or("unknown"),
226 "DeepSeek formatter dropped non-text content chunk while normalizing message content",
227 );
228 None
229 })
230 .collect::<String>(),
231 _ => to_json(content),
232 }
233}
234
235pub(crate) fn normalize_message_contents(messages: &mut [JsonValue], non_text: NormalizeNonText) {
236 for msg in messages {
237 let Some(content) = msg.get("content") else {
238 continue;
239 };
240 if content.is_string() {
242 continue;
243 }
244 if !content.is_array() && non_text == NormalizeNonText::LeaveUntouched {
245 continue;
246 }
247 let normalized = extract_visible_text(content);
248 if let Some(obj) = msg.as_object_mut() {
249 obj.insert("content".to_string(), JsonValue::String(normalized));
250 }
251 }
252}
253
254pub(crate) fn encode_arguments_to_dsml(tool_call: &JsonValue) -> Result<String> {
255 let arguments_str = tool_call
256 .get("arguments")
257 .and_then(|a| a.as_str())
258 .context("Missing or invalid 'arguments' field")?;
259
260 let arguments: JsonValue = match serde_json::from_str(arguments_str) {
262 Ok(v) => v,
263 Err(_) => serde_json::json!({ "arguments": arguments_str }),
264 };
265
266 let arguments_obj = arguments
267 .as_object()
268 .context("Arguments must be a JSON object")?;
269
270 let mut params = Vec::new();
271 for (key, value) in arguments_obj {
272 let value_str = if let Some(vs) = value.as_str() {
273 vs.to_string()
274 } else {
275 to_json(value)
276 };
277 params.push(format!(
278 "<{}parameter name=\"{}\" string=\"{}\">{}</{}parameter>",
279 tokens::DSML_TOKEN,
280 key,
281 if value.is_string() { "true" } else { "false" },
282 value_str,
283 tokens::DSML_TOKEN
284 ));
285 }
286
287 Ok(params.join("\n"))
288}
289
290pub(crate) fn task_token(task: &str) -> Option<&'static str> {
291 match task {
292 "action" => Some(tokens::TASK_ACTION),
293 "query" => Some(tokens::TASK_QUERY),
294 "authority" => Some(tokens::TASK_AUTHORITY),
295 "domain" => Some(tokens::TASK_DOMAIN),
296 "title" => Some(tokens::TASK_TITLE),
297 "read_url" => Some(tokens::TASK_READ_URL),
298 _ => None,
299 }
300}
301
302const USER_FIELDS_TO_PRESERVE: [&str; 3] = ["task", "wo_eos", "mask"];
303
304fn preserve_user_fields(target: &mut JsonValue, source: &JsonValue) {
305 if let Some(obj) = target.as_object_mut() {
306 for key in USER_FIELDS_TO_PRESERVE {
307 if let Some(v) = source.get(key) {
308 obj.insert(key.to_string(), v.clone());
309 }
310 }
311 }
312}
313
314pub(crate) fn merge_tool_messages(messages: Vec<JsonValue>) -> Vec<JsonValue> {
319 let mut merged: Vec<JsonValue> = Vec::with_capacity(messages.len());
320
321 for mut msg in messages {
322 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
323
324 if role == "tool" {
325 let obj = msg
326 .as_object_mut()
327 .expect("a message with a role is an object");
328 let tool_block = JsonValue::Object(serde_json::Map::from_iter([
329 ("type".into(), JsonValue::String("tool_result".into())),
330 (
331 "tool_use_id".into(),
332 obj.remove("tool_call_id")
333 .unwrap_or_else(|| JsonValue::String(String::new())),
334 ),
335 (
336 "content".into(),
337 obj.remove("content")
338 .unwrap_or_else(|| JsonValue::String(String::new())),
339 ),
340 ]));
341
342 let can_merge = merged
343 .last()
344 .map(|m| {
345 m.get("role").and_then(|r| r.as_str()) == Some("user")
346 && m.get("content_blocks").is_some()
347 })
348 .unwrap_or(false);
349
350 if can_merge {
351 let last = merged.last_mut().unwrap();
352 if let Some(blocks) = last
353 .as_object_mut()
354 .and_then(|o| o.get_mut("content_blocks"))
355 .and_then(|v| v.as_array_mut())
356 {
357 blocks.push(tool_block);
358 }
359 } else {
360 merged.push(JsonValue::Object(serde_json::Map::from_iter([
361 ("role".into(), JsonValue::String("user".into())),
362 ("content_blocks".into(), JsonValue::Array(vec![tool_block])),
363 ])));
364 }
365 } else if role == "user" {
366 let text = msg
367 .as_object_mut()
368 .expect("a message with a role is an object")
369 .remove("content")
370 .filter(JsonValue::is_string)
371 .unwrap_or_else(|| JsonValue::String(String::new()));
372 let text_block = JsonValue::Object(serde_json::Map::from_iter([
373 ("type".into(), JsonValue::String("text".into())),
374 ("text".into(), text),
375 ]));
376
377 let can_merge = merged
378 .last()
379 .map(|m| {
380 m.get("role").and_then(|r| r.as_str()) == Some("user")
381 && m.get("content_blocks").is_some()
382 && m.get("task").map(|v| v.is_null()).unwrap_or(true)
383 })
384 .unwrap_or(false);
385
386 if can_merge {
387 let last = merged.last_mut().unwrap();
388 let appended = last
389 .as_object_mut()
390 .and_then(|o| o.get_mut("content_blocks"))
391 .and_then(|v| v.as_array_mut())
392 .map(|blocks| {
393 blocks.push(text_block);
394 })
395 .is_some();
396 if appended {
397 preserve_user_fields(last, &msg);
398 }
399 } else {
400 let mut new_msg = JsonValue::Object(serde_json::Map::from_iter([
403 ("role".into(), JsonValue::String("user".into())),
404 ("content_blocks".into(), JsonValue::Array(vec![text_block])),
405 ]));
406 preserve_user_fields(&mut new_msg, &msg);
407 merged.push(new_msg);
408 }
409 } else {
410 merged.push(msg);
411 }
412 }
413
414 merged
415}
416
417pub(crate) fn sort_tool_results_by_call_order(mut messages: Vec<JsonValue>) -> Vec<JsonValue> {
420 use std::collections::HashMap;
421 let mut last_order: HashMap<String, usize> = HashMap::new();
422
423 for msg in &mut messages {
424 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
425 if role == "assistant" {
426 if let Some(tcs) = msg.get("tool_calls").and_then(|t| t.as_array()) {
427 last_order.clear();
428 for (idx, tc) in tcs.iter().enumerate() {
429 let id = tc
430 .get("id")
431 .and_then(|v| v.as_str())
432 .or_else(|| {
433 tc.get("function")
434 .and_then(|f| f.get("id"))
435 .and_then(|v| v.as_str())
436 })
437 .unwrap_or("");
438 if !id.is_empty() {
439 last_order.insert(id.to_string(), idx);
440 }
441 }
442 }
443 } else if role == "user" && !last_order.is_empty() {
444 let Some(blocks) = msg
445 .as_object_mut()
446 .and_then(|o| o.get_mut("content_blocks"))
447 .and_then(|v| v.as_array_mut())
448 else {
449 continue;
450 };
451
452 let tool_positions: Vec<usize> = blocks
454 .iter()
455 .enumerate()
456 .filter(|(_, b)| b.get("type").and_then(|v| v.as_str()) == Some("tool_result"))
457 .map(|(i, _)| i)
458 .collect();
459
460 if tool_positions.len() > 1 {
461 let start = *tool_positions
462 .first()
463 .expect("tool_positions has length > 1");
464 let end = *tool_positions
465 .last()
466 .expect("tool_positions has length > 1");
467 let is_contiguous = end - start + 1 == tool_positions.len();
468
469 if is_contiguous {
470 blocks[start..=end].sort_by_key(|b| {
472 let id = b.get("tool_use_id").and_then(|v| v.as_str()).unwrap_or("");
473 *last_order.get(id).unwrap_or(&0)
474 });
475 } else {
476 let mut tool_blocks: Vec<JsonValue> = tool_positions
478 .iter()
479 .map(|&i| std::mem::take(&mut blocks[i]))
480 .collect();
481
482 tool_blocks.sort_by_key(|b| {
483 let id = b.get("tool_use_id").and_then(|v| v.as_str()).unwrap_or("");
484 *last_order.get(id).unwrap_or(&0)
485 });
486
487 for (sorted_idx, &pos) in tool_positions.iter().enumerate() {
488 blocks[pos] = std::mem::take(&mut tool_blocks[sorted_idx]);
489 }
490 }
491 }
492 }
493 }
494
495 messages
496}
497
498pub(crate) fn drop_thinking_messages(messages: Vec<JsonValue>) -> Vec<JsonValue> {
500 let last_user_idx = find_last_user_index(&messages);
501 let mut out = Vec::with_capacity(messages.len());
502 const KEEP: &[&str] = &[
503 "user",
504 "system",
505 "tool",
506 "latest_reminder",
507 "direct_search_results",
508 ];
509
510 for (idx, mut msg) in messages.into_iter().enumerate() {
511 let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
512 if KEEP.contains(&role) || last_user_idx.is_none_or(|u| idx >= u) {
513 out.push(msg);
514 } else if role == "assistant" {
515 if let Some(obj) = msg.as_object_mut() {
516 obj.remove("reasoning_content");
517 }
518 out.push(msg);
519 }
520 }
522 out
523}
524
525pub(crate) fn resolve_thinking_mode(
526 args: Option<&std::collections::HashMap<String, serde_json::Value>>,
527 default_mode: ThinkingMode,
528) -> ThinkingMode {
529 if let Some(enabled) = crate::thinking_bool_from_args(args) {
530 return if enabled {
531 ThinkingMode::Thinking
532 } else {
533 ThinkingMode::Chat
534 };
535 }
536 if let Some(args) = args
537 && let Some(mode) = args.get("thinking_mode").and_then(|v| v.as_str())
538 {
539 match mode {
540 "chat" => return ThinkingMode::Chat,
541 "thinking" => return ThinkingMode::Thinking,
542 _ => {}
543 }
544 }
545 default_mode
546}
547
548pub(crate) fn inject_tools_and_response_format(
549 messages_array: &mut Vec<JsonValue>,
550 req: &dyn crate::OAIChatLikeRequest,
551) -> Result<()> {
552 let tools_json = req
553 .tools()
554 .map(|t| serde_json::to_value(&t))
555 .transpose()
556 .context("Failed to convert tools to JSON")?;
557
558 let tools_json = match req.tool_choice() {
564 Some(ref tc) if tc.as_str() == Some("none") => None,
565 _ => tools_json,
566 };
567
568 let response_format_json = req
569 .response_format()
570 .map(|rf| serde_json::to_value(&rf))
571 .transpose()
572 .context("Failed to convert response_format to JSON")?;
573
574 if tools_json.is_some() || response_format_json.is_some() {
575 let system_idx = messages_array
576 .iter()
577 .position(|msg| msg.get("role").and_then(|r| r.as_str()) == Some("system"));
578
579 if let Some(idx) = system_idx {
580 if let Some(msg) = messages_array.get_mut(idx)
581 && let Some(obj) = msg.as_object_mut()
582 {
583 if let Some(tools) = tools_json {
584 obj.insert("tools".to_string(), tools);
585 }
586 if let Some(rf) = response_format_json {
587 obj.insert("response_format".to_string(), rf);
588 }
589 }
590 } else {
591 let mut system_msg = serde_json::json!({
592 "role": "system",
593 "content": ""
594 });
595 if let Some(obj) = system_msg.as_object_mut() {
596 if let Some(tools) = tools_json {
597 obj.insert("tools".to_string(), tools);
598 }
599 if let Some(rf) = response_format_json {
600 obj.insert("response_format".to_string(), rf);
601 }
602 }
603 messages_array.insert(0, system_msg);
604 }
605 }
606 Ok(())
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612 use serde_json::json;
613
614 #[test]
615 fn merged_user_blocks_preserve_metadata_and_order() {
616 let merged = merge_tool_messages(vec![
617 serde_json::json!({"role": "tool", "tool_call_id": "c1", "content": "result"}),
618 serde_json::json!({"role": "user", "content": "one", "wo_eos": true, "mask": [1, 0]}),
619 serde_json::json!({"role": "user", "content": "two", "task": "action"}),
620 serde_json::json!({"role": "user", "content": "separate"}),
621 ]);
622 assert_eq!(merged.len(), 2);
623 assert!(
624 merged
625 .iter()
626 .all(|message| message.get("content").is_none())
627 );
628 assert_eq!(
629 merged[0]["content_blocks"],
630 serde_json::json!([
631 {"type": "tool_result", "tool_use_id": "c1", "content": "result"},
632 {"type": "text", "text": "one"},
633 {"type": "text", "text": "two"}
634 ])
635 );
636 assert_eq!(merged[0]["task"], "action");
637 assert_eq!(merged[0]["wo_eos"], true);
638 assert_eq!(merged[0]["mask"], serde_json::json!([1, 0]));
639 assert_eq!(merged[1]["content_blocks"][0]["text"], "separate");
640 }
641
642 #[test]
643 fn test_extract_visible_text_from_content_array() {
644 let content = json!([
645 {"type": "text", "text": "who "},
646 {"type": "text", "text": "are "},
647 {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}},
648 {"type": "text", "text": "you?"}
649 ]);
650 assert_eq!(extract_visible_text(&content), "who are you?");
651 }
652
653 #[test]
654 fn test_render_tools_preserves_placeholder_text_inside_tool_schema() {
655 let tools = json!([{
656 "type": "function",
657 "function": {
658 "name": "placeholder_tool",
659 "description": "literal {dsml_token} {thinking_start_token} {thinking_end_token}",
660 "parameters": {"type": "object", "properties": {}}
661 }
662 }]);
663 let rendered = render_tools(
664 "static {dsml_token} {thinking_start_token} {thinking_end_token}\n{tool_schemas}",
665 tools.as_array().unwrap(),
666 );
667
668 assert!(rendered.starts_with(&format!(
669 "static {} {} {}\n",
670 tokens::DSML_TOKEN,
671 tokens::THINKING_START,
672 tokens::THINKING_END
673 )));
674 assert!(
675 rendered.contains("literal {dsml_token} {thinking_start_token} {thinking_end_token}")
676 );
677 }
678}