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