1use anyhow::{Context, Result};
12use serde_json::Value as JsonValue;
13
14use super::common::{
15 NormalizeNonText, RESPONSE_FORMAT_TEMPLATE, TOOL_CALL_TEMPLATE, TOOL_OUTPUT_TEMPLATE,
16 TOOLS_SYSTEM_TEMPLATE, encode_arguments_to_dsml, find_last_user_index,
17 normalize_message_contents, render_tools, to_json,
18};
19
20pub use super::common::{ThinkingMode, tokens};
21
22fn render_message(
24 index: usize,
25 messages: &[JsonValue],
26 thinking_mode: ThinkingMode,
27 last_user_idx: Option<usize>,
28) -> Result<String> {
29 let msg = &messages[index];
30 let role = msg
31 .get("role")
32 .and_then(|r| r.as_str())
33 .context("Missing 'role' field")?;
34
35 let mut prompt = String::new();
36
37 match role {
38 "system" => {
39 let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
40 prompt.push_str(content);
41
42 if let Some(tools) = msg.get("tools").and_then(|t| t.as_array()) {
43 prompt.push_str("\n\n");
44 prompt.push_str(&render_tools(TOOLS_SYSTEM_TEMPLATE, tools));
45 }
46
47 if let Some(response_format) = msg.get("response_format") {
48 prompt.push_str("\n\n");
49 prompt.push_str(
50 &RESPONSE_FORMAT_TEMPLATE.replace("{schema}", &to_json(response_format)),
51 );
52 }
53 }
54
55 "user" => {
56 let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
57 prompt.push_str(tokens::USER_START);
58 prompt.push_str(content);
59 prompt.push_str(tokens::ASSISTANT_START);
60
61 if Some(index) == last_user_idx && thinking_mode == ThinkingMode::Thinking {
62 prompt.push_str(tokens::THINKING_START);
63 } else {
64 prompt.push_str(tokens::THINKING_END);
65 }
66 }
67
68 "developer" => {
69 let content = msg
70 .get("content")
71 .and_then(|c| c.as_str())
72 .context("Developer role requires content")?;
73
74 let mut content_developer = String::new();
75
76 if let Some(tools) = msg.get("tools").and_then(|t| t.as_array()) {
77 content_developer.push_str("\n\n");
78 content_developer.push_str(&render_tools(TOOLS_SYSTEM_TEMPLATE, tools));
79 }
80
81 if let Some(response_format) = msg.get("response_format") {
82 content_developer.push_str("\n\n");
83 content_developer.push_str(
84 &RESPONSE_FORMAT_TEMPLATE.replace("{schema}", &to_json(response_format)),
85 );
86 }
87
88 content_developer.push_str(&format!("\n\n# The user's message is: {}", content));
89
90 prompt.push_str(tokens::USER_START);
91 prompt.push_str(&content_developer);
92 prompt.push_str(tokens::ASSISTANT_START);
93
94 if Some(index) == last_user_idx && thinking_mode == ThinkingMode::Thinking {
95 prompt.push_str(tokens::THINKING_START);
96 } else {
97 prompt.push_str(tokens::THINKING_END);
98 }
99 }
100
101 "assistant" => {
102 if thinking_mode == ThinkingMode::Thinking
110 && last_user_idx.is_some_and(|idx| index > idx)
111 {
112 let reasoning = msg.get("reasoning_content").and_then(|v| match v {
113 serde_json::Value::String(s) => {
114 if s.is_empty() {
115 None
116 } else {
117 Some(s.clone())
118 }
119 }
120 serde_json::Value::Array(arr) => {
121 let joined = arr
122 .iter()
123 .filter_map(|v| v.as_str())
124 .filter(|s| !s.is_empty())
125 .collect::<Vec<_>>()
126 .join("\n");
127 if joined.is_empty() {
128 None
129 } else {
130 Some(joined)
131 }
132 }
133 _ => None,
134 });
135
136 if let Some(reasoning) = reasoning {
137 prompt.push_str(&reasoning);
139 prompt.push_str(tokens::THINKING_END);
140 }
141 }
142
143 if let Some(content) = msg.get("content").and_then(|c| c.as_str()) {
145 prompt.push_str(content);
146 }
147
148 if let Some(tool_calls) = msg.get("tool_calls").and_then(|t| t.as_array())
150 && !tool_calls.is_empty()
151 {
152 prompt.push_str("\n\n");
153 prompt.push_str(&format!("<{}function_calls>\n", tokens::DSML_TOKEN));
154
155 for tool_call in tool_calls {
156 let name = tool_call
157 .get("function")
158 .and_then(|f| f.get("name"))
159 .and_then(|n| n.as_str())
160 .context("Missing tool call name")?;
161
162 let arguments = encode_arguments_to_dsml(
163 tool_call.get("function").context("Missing function")?,
164 )?;
165
166 let invoke = TOOL_CALL_TEMPLATE
167 .replace("{dsml_token}", tokens::DSML_TOKEN)
168 .replace("{name}", name)
169 .replace("{arguments}", &arguments);
170
171 prompt.push_str(&invoke);
172 prompt.push('\n');
173 }
174
175 prompt.push_str(&format!("</{}function_calls>", tokens::DSML_TOKEN));
176 }
177
178 prompt.push_str(tokens::EOS);
179 }
180
181 "tool" => {
182 let mut prev_assistant_idx = None;
184 let mut tool_count = 0;
185
186 for i in (0..index).rev() {
187 let prev_role = messages[i].get("role").and_then(|r| r.as_str());
188 if prev_role == Some("tool") {
189 tool_count += 1;
190 } else if prev_role == Some("assistant") {
191 prev_assistant_idx = Some(i);
192 break;
193 }
194 }
195
196 let tool_call_order = tool_count + 1;
197
198 if tool_call_order == 1 {
200 prompt.push_str("\n\n<function_results>");
201 }
202
203 let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
205 prompt.push_str(&TOOL_OUTPUT_TEMPLATE.replace("{content}", content));
206
207 if let Some(prev_idx) = prev_assistant_idx {
209 let tool_calls_count = messages[prev_idx]
210 .get("tool_calls")
211 .and_then(|t| t.as_array())
212 .map(|a| a.len())
213 .unwrap_or(0);
214
215 if tool_call_order == tool_calls_count {
216 prompt.push_str("\n</function_results>");
217
218 if last_user_idx.is_some_and(|idx| index >= idx)
219 && thinking_mode == ThinkingMode::Thinking
220 {
221 prompt.push_str("\n\n");
222 prompt.push_str(tokens::THINKING_START);
223 } else {
224 prompt.push_str("\n\n");
225 prompt.push_str(tokens::THINKING_END);
226 }
227 }
228 }
229 }
230
231 _ => anyhow::bail!("Unknown role: {}", role),
232 }
233
234 Ok(prompt)
235}
236
237pub fn encode_messages(
247 messages: &[JsonValue],
248 thinking_mode: ThinkingMode,
249 add_bos_token: bool,
250) -> Result<String> {
251 let mut prompt = String::new();
252
253 if add_bos_token {
254 prompt.push_str(tokens::BOS);
255 }
256
257 let last_user_idx = find_last_user_index(messages);
258
259 for (index, _) in messages.iter().enumerate() {
260 let msg_prompt = render_message(index, messages, thinking_mode, last_user_idx)?;
261 prompt.push_str(&msg_prompt);
262 }
263
264 Ok(prompt)
265}
266
267#[derive(Debug)]
271pub struct DeepSeekV32Formatter {
272 thinking_mode: ThinkingMode,
273}
274
275impl DeepSeekV32Formatter {
276 pub fn new(thinking_mode: ThinkingMode) -> Self {
277 Self { thinking_mode }
278 }
279
280 pub fn new_thinking() -> Self {
282 Self::new(ThinkingMode::Thinking)
283 }
284
285 pub fn new_chat() -> Self {
287 Self::new(ThinkingMode::Chat)
288 }
289}
290
291impl crate::OAIPromptFormatter for DeepSeekV32Formatter {
292 fn supports_add_generation_prompt(&self) -> bool {
293 true
294 }
295
296 fn render(&self, req: &dyn crate::OAIChatLikeRequest) -> Result<String> {
297 let thinking_mode =
298 super::common::resolve_thinking_mode(req.chat_template_args(), self.thinking_mode);
299
300 let messages_value = req.messages();
302
303 let messages_json =
305 serde_json::to_value(&messages_value).context("Failed to convert messages to JSON")?;
306
307 let mut messages_array = messages_json
308 .as_array()
309 .context("Messages is not an array")?
310 .clone();
311
312 normalize_message_contents(&mut messages_array, NormalizeNonText::SerializeJson);
315
316 super::common::inject_tools_and_response_format(&mut messages_array, req)?;
319
320 encode_messages(
322 &messages_array,
323 thinking_mode,
324 true, )
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use serde_json::json;
333
334 #[test]
335 fn test_simple_conversation() {
336 let messages = json!([
337 {"role": "system", "content": "You are a helpful assistant."},
338 {"role": "user", "content": "Hello!"}
339 ]);
340
341 let result =
342 encode_messages(messages.as_array().unwrap(), ThinkingMode::Thinking, true).unwrap();
343
344 assert!(result.starts_with(tokens::BOS));
345 assert!(result.contains("You are a helpful assistant."));
346 assert!(result.contains(tokens::USER_START));
347 assert!(result.contains("Hello!"));
348 assert!(result.contains(tokens::ASSISTANT_START));
349 assert!(result.contains(tokens::THINKING_START));
350 }
351
352 #[test]
353 fn test_formatter_handles_user_content_array() {
354 use crate::OAIPromptFormatter;
355
356 let request = MockRequest::new(json!([
357 {"role": "user", "content": [
358 {"type": "text", "text": "who are you?"}
359 ]}
360 ]));
361
362 let formatter = DeepSeekV32Formatter::new_thinking();
363 let result = formatter.render(&request).unwrap();
364
365 assert!(result.contains("who are you?"));
366 assert!(result.contains(tokens::USER_START));
367 assert!(result.contains(tokens::ASSISTANT_START));
368 }
369
370 #[test]
371 fn test_formatter_serializes_non_text_content() {
372 use crate::OAIPromptFormatter;
373
374 let request = MockRequest::new(json!([
375 {"role": "user", "content": {"foo": "bar"}}
376 ]));
377
378 let formatter = DeepSeekV32Formatter::new_thinking();
379 let result = formatter.render(&request).unwrap();
380
381 assert!(result.contains(r#"{"foo": "bar"}"#));
382 }
383
384 #[test]
385 fn test_tools_rendering() {
386 let messages = json!([
387 {
388 "role": "system",
389 "content": "You are helpful.",
390 "tools": [{
391 "type": "function",
392 "function": {
393 "name": "get_weather",
394 "description": "Get weather",
395 "parameters": {
396 "type": "object",
397 "properties": {
398 "location": {"type": "string"}
399 }
400 }
401 }
402 }]
403 },
404 {"role": "user", "content": "What's the weather?"}
405 ]);
406
407 let result =
408 encode_messages(messages.as_array().unwrap(), ThinkingMode::Thinking, true).unwrap();
409
410 assert!(result.contains("## Tools"));
411 assert!(result.contains("get_weather"));
412 assert!(result.contains("<functions>"));
413 }
414
415 struct MockRequest {
417 messages: JsonValue,
418 tools: Option<JsonValue>,
419 response_format: Option<JsonValue>,
420 chat_template_args: Option<std::collections::HashMap<String, JsonValue>>,
421 }
422
423 impl MockRequest {
424 fn new(messages: JsonValue) -> Self {
425 Self {
426 messages,
427 tools: None,
428 response_format: None,
429 chat_template_args: None,
430 }
431 }
432
433 fn with_tools(mut self, tools: JsonValue) -> Self {
434 self.tools = Some(tools);
435 self
436 }
437
438 fn with_response_format(mut self, response_format: JsonValue) -> Self {
439 self.response_format = Some(response_format);
440 self
441 }
442
443 fn with_chat_template_args(
444 mut self,
445 args: std::collections::HashMap<String, JsonValue>,
446 ) -> Self {
447 self.chat_template_args = Some(args);
448 self
449 }
450 }
451
452 impl crate::OAIChatLikeRequest for MockRequest {
453 fn model(&self) -> String {
454 "deepseek-v3.2".to_string()
455 }
456
457 fn messages(&self) -> minijinja::value::Value {
458 minijinja::value::Value::from_serialize(&self.messages)
459 }
460
461 fn tools(&self) -> Option<minijinja::value::Value> {
462 self.tools
463 .as_ref()
464 .map(minijinja::value::Value::from_serialize)
465 }
466
467 fn response_format(&self) -> Option<minijinja::value::Value> {
468 self.response_format
469 .as_ref()
470 .map(minijinja::value::Value::from_serialize)
471 }
472
473 fn should_add_generation_prompt(&self) -> bool {
474 true
475 }
476
477 fn chat_template_args(
478 &self,
479 ) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
480 self.chat_template_args.as_ref()
481 }
482 }
483
484 #[test]
485 fn test_formatter_injects_tools_into_existing_system_message() {
486 use crate::OAIPromptFormatter;
487
488 let tools = json!([{
489 "type": "function",
490 "function": {
491 "name": "get_weather",
492 "description": "Get current weather",
493 "parameters": {
494 "type": "object",
495 "properties": {
496 "location": {"type": "string", "description": "City name"},
497 "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
498 },
499 "required": ["location"]
500 }
501 }
502 }]);
503
504 let request = MockRequest::new(json!([
505 {"role": "system", "content": "You are a helpful assistant."},
506 {"role": "user", "content": "What's the weather in Moscow?"}
507 ]))
508 .with_tools(tools);
509
510 let formatter = DeepSeekV32Formatter::new_thinking();
511 let result = formatter.render(&request).unwrap();
512
513 assert!(
515 result.contains("## Tools"),
516 "Should contain Tools section header"
517 );
518 assert!(
519 result.contains("get_weather"),
520 "Should contain function name"
521 );
522 assert!(
523 result.contains("<functions>"),
524 "Should contain functions block"
525 );
526 assert!(
527 result.contains("</functions>"),
528 "Should contain closing functions tag"
529 );
530 assert!(
531 result.contains("You are a helpful assistant."),
532 "Should preserve original system content"
533 );
534 assert!(
535 result.contains(&format!("<{}function_calls>", tokens::DSML_TOKEN)),
536 "Should contain DSML format instructions"
537 );
538 }
539
540 #[test]
541 fn test_formatter_creates_system_message_for_tools_when_missing() {
542 use crate::OAIPromptFormatter;
543
544 let tools = json!([{
545 "type": "function",
546 "function": {
547 "name": "get_current_time",
548 "description": "Get current time in a timezone",
549 "parameters": {
550 "type": "object",
551 "properties": {
552 "timezone": {"type": "string"}
553 },
554 "required": ["timezone"]
555 }
556 }
557 }]);
558
559 let request = MockRequest::new(json!([
561 {"role": "user", "content": "What time is it in Tokyo?"}
562 ]))
563 .with_tools(tools);
564
565 let formatter = DeepSeekV32Formatter::new_thinking();
566 let result = formatter.render(&request).unwrap();
567
568 assert!(
570 result.contains("## Tools"),
571 "Should contain Tools section even without explicit system message"
572 );
573 assert!(
574 result.contains("get_current_time"),
575 "Should contain function name"
576 );
577 assert!(
578 result.contains("<functions>"),
579 "Should contain functions block"
580 );
581 }
582
583 #[test]
584 fn test_formatter_without_tools_does_not_add_tools_section() {
585 use crate::OAIPromptFormatter;
586
587 let request = MockRequest::new(json!([
588 {"role": "system", "content": "You are a helpful assistant."},
589 {"role": "user", "content": "Hello!"}
590 ]));
591
592 let formatter = DeepSeekV32Formatter::new_thinking();
593 let result = formatter.render(&request).unwrap();
594
595 assert!(
597 !result.contains("## Tools"),
598 "Should not contain Tools section when no tools provided"
599 );
600 assert!(
601 !result.contains("<functions>"),
602 "Should not contain functions block when no tools provided"
603 );
604 assert!(
605 result.contains("You are a helpful assistant."),
606 "Should preserve system content"
607 );
608 }
609
610 #[test]
611 fn test_formatter_with_multiple_tools() {
612 use crate::OAIPromptFormatter;
613
614 let tools = json!([
615 {
616 "type": "function",
617 "function": {
618 "name": "get_weather",
619 "description": "Get current weather",
620 "parameters": {
621 "type": "object",
622 "properties": {
623 "location": {"type": "string"}
624 }
625 }
626 }
627 },
628 {
629 "type": "function",
630 "function": {
631 "name": "get_current_time",
632 "description": "Get current time",
633 "parameters": {
634 "type": "object",
635 "properties": {
636 "timezone": {"type": "string"}
637 }
638 }
639 }
640 }
641 ]);
642
643 let request = MockRequest::new(json!([
644 {"role": "system", "content": "You are helpful."},
645 {"role": "user", "content": "Weather and time in Moscow?"}
646 ]))
647 .with_tools(tools);
648
649 let formatter = DeepSeekV32Formatter::new_thinking();
650 let result = formatter.render(&request).unwrap();
651
652 assert!(
654 result.contains("get_weather"),
655 "Should contain first function"
656 );
657 assert!(
658 result.contains("get_current_time"),
659 "Should contain second function"
660 );
661 }
662
663 #[test]
666 fn test_formatter_injects_response_format_into_existing_system_message() {
667 use crate::OAIPromptFormatter;
668
669 let response_format = json!({
670 "type": "json_schema",
671 "json_schema": {
672 "name": "city_info",
673 "strict": true,
674 "schema": {
675 "type": "object",
676 "properties": {
677 "city": {"type": "string"},
678 "country": {"type": "string"},
679 "population": {"type": "number"}
680 },
681 "required": ["city", "country", "population"]
682 }
683 }
684 });
685
686 let request = MockRequest::new(json!([
687 {"role": "system", "content": "You are a helpful assistant."},
688 {"role": "user", "content": "Tell me about Moscow."}
689 ]))
690 .with_response_format(response_format);
691
692 let formatter = DeepSeekV32Formatter::new_thinking();
693 let result = formatter.render(&request).unwrap();
694
695 assert!(
697 result.contains("## Response Format:"),
698 "Should contain Response Format section header"
699 );
700 assert!(
701 result.contains("json_schema"),
702 "Should contain json_schema type"
703 );
704 assert!(result.contains("city_info"), "Should contain schema name");
705 assert!(
706 result.contains("You are a helpful assistant."),
707 "Should preserve original system content"
708 );
709 }
710
711 #[test]
712 fn test_formatter_creates_system_message_for_response_format_when_missing() {
713 use crate::OAIPromptFormatter;
714
715 let response_format = json!({
716 "type": "json_schema",
717 "json_schema": {
718 "name": "weather_response",
719 "schema": {
720 "type": "object",
721 "properties": {
722 "temperature": {"type": "number"},
723 "conditions": {"type": "string"}
724 }
725 }
726 }
727 });
728
729 let request = MockRequest::new(json!([
731 {"role": "user", "content": "What's the weather?"}
732 ]))
733 .with_response_format(response_format);
734
735 let formatter = DeepSeekV32Formatter::new_thinking();
736 let result = formatter.render(&request).unwrap();
737
738 assert!(
740 result.contains("## Response Format:"),
741 "Should contain Response Format section even without explicit system message"
742 );
743 assert!(
744 result.contains("weather_response"),
745 "Should contain schema name"
746 );
747 }
748
749 #[test]
750 fn test_formatter_with_both_tools_and_response_format() {
751 use crate::OAIPromptFormatter;
752
753 let tools = json!([{
754 "type": "function",
755 "function": {
756 "name": "search_database",
757 "description": "Search the database",
758 "parameters": {
759 "type": "object",
760 "properties": {
761 "query": {"type": "string"}
762 }
763 }
764 }
765 }]);
766
767 let response_format = json!({
768 "type": "json_schema",
769 "json_schema": {
770 "name": "search_result",
771 "schema": {
772 "type": "object",
773 "properties": {
774 "results": {"type": "array"},
775 "total_count": {"type": "number"}
776 }
777 }
778 }
779 });
780
781 let request = MockRequest::new(json!([
782 {"role": "system", "content": "You are a search assistant."},
783 {"role": "user", "content": "Find documents about Rust."}
784 ]))
785 .with_tools(tools)
786 .with_response_format(response_format);
787
788 let formatter = DeepSeekV32Formatter::new_thinking();
789 let result = formatter.render(&request).unwrap();
790
791 assert!(result.contains("## Tools"), "Should contain Tools section");
793 assert!(
794 result.contains("search_database"),
795 "Should contain function name"
796 );
797 assert!(
798 result.contains("## Response Format:"),
799 "Should contain Response Format section"
800 );
801 assert!(
802 result.contains("search_result"),
803 "Should contain schema name"
804 );
805 assert!(
806 result.contains("You are a search assistant."),
807 "Should preserve original system content"
808 );
809 }
810
811 #[test]
812 fn test_formatter_without_response_format_does_not_add_response_format_section() {
813 use crate::OAIPromptFormatter;
814
815 let request = MockRequest::new(json!([
816 {"role": "system", "content": "You are a helpful assistant."},
817 {"role": "user", "content": "Hello!"}
818 ]));
819
820 let formatter = DeepSeekV32Formatter::new_thinking();
821 let result = formatter.render(&request).unwrap();
822
823 assert!(
825 !result.contains("## Response Format:"),
826 "Should not contain Response Format section when not provided"
827 );
828 }
829
830 #[test]
833 fn test_chat_mode_via_thinking_false() {
834 use crate::OAIPromptFormatter;
835
836 let args = std::collections::HashMap::from([("thinking".to_string(), json!(false))]);
837
838 let request = MockRequest::new(json!([
839 {"role": "system", "content": "You are a helpful assistant."},
840 {"role": "user", "content": "Hello!"}
841 ]))
842 .with_chat_template_args(args);
843
844 let formatter = DeepSeekV32Formatter::new_thinking();
845 let result = formatter.render(&request).unwrap();
846
847 assert!(
850 result.ends_with(&format!(
851 "{}{}",
852 tokens::ASSISTANT_START,
853 tokens::THINKING_END
854 )),
855 "Chat mode should end with </think> after Assistant token, got: ...{}",
856 &result[result.len().saturating_sub(80)..],
857 );
858 assert!(
859 !result.ends_with(&format!(
860 "{}{}",
861 tokens::ASSISTANT_START,
862 tokens::THINKING_START
863 )),
864 "Chat mode should NOT end with <think>",
865 );
866 }
867
868 #[test]
869 fn test_explicit_thinking_true_via_args() {
870 use crate::OAIPromptFormatter;
871
872 let args = std::collections::HashMap::from([("thinking".to_string(), json!(true))]);
873
874 let request = MockRequest::new(json!([
875 {"role": "system", "content": "You are a helpful assistant."},
876 {"role": "user", "content": "Hello!"}
877 ]))
878 .with_chat_template_args(args);
879
880 let formatter = DeepSeekV32Formatter::new_thinking();
881 let result = formatter.render(&request).unwrap();
882
883 assert!(
884 result.ends_with(&format!(
885 "{}{}",
886 tokens::ASSISTANT_START,
887 tokens::THINKING_START
888 )),
889 "Thinking mode should end with <think> after Assistant token",
890 );
891 }
892
893 #[test]
894 fn test_chat_mode_via_thinking_mode_string() {
895 use crate::OAIPromptFormatter;
896
897 let args = std::collections::HashMap::from([("thinking_mode".to_string(), json!("chat"))]);
898
899 let request = MockRequest::new(json!([
900 {"role": "system", "content": "You are a helpful assistant."},
901 {"role": "user", "content": "Hello!"}
902 ]))
903 .with_chat_template_args(args);
904
905 let formatter = DeepSeekV32Formatter::new_thinking();
906 let result = formatter.render(&request).unwrap();
907
908 assert!(
909 result.ends_with(&format!(
910 "{}{}",
911 tokens::ASSISTANT_START,
912 tokens::THINKING_END
913 )),
914 "thinking_mode='chat' should produce chat mode (ends with </think>)",
915 );
916 }
917
918 #[test]
919 fn test_thinking_mode_string_thinking() {
920 use crate::OAIPromptFormatter;
921
922 let args =
923 std::collections::HashMap::from([("thinking_mode".to_string(), json!("thinking"))]);
924
925 let request = MockRequest::new(json!([
926 {"role": "system", "content": "You are a helpful assistant."},
927 {"role": "user", "content": "Hello!"}
928 ]))
929 .with_chat_template_args(args);
930
931 let formatter = DeepSeekV32Formatter::new_thinking();
932 let result = formatter.render(&request).unwrap();
933
934 assert!(
935 result.ends_with(&format!(
936 "{}{}",
937 tokens::ASSISTANT_START,
938 tokens::THINKING_START
939 )),
940 "thinking_mode='thinking' should produce thinking mode (ends with <think>)",
941 );
942 }
943
944 #[test]
945 fn test_default_thinking_mode_without_args() {
946 use crate::OAIPromptFormatter;
947
948 let request = MockRequest::new(json!([
949 {"role": "system", "content": "You are a helpful assistant."},
950 {"role": "user", "content": "Hello!"}
951 ]));
952
953 let formatter = DeepSeekV32Formatter::new_thinking();
955 let result = formatter.render(&request).unwrap();
956
957 assert!(
958 result.ends_with(&format!(
959 "{}{}",
960 tokens::ASSISTANT_START,
961 tokens::THINKING_START
962 )),
963 "Default (new_thinking) should produce thinking mode",
964 );
965
966 let formatter_chat = DeepSeekV32Formatter::new_chat();
968 let result_chat = formatter_chat.render(&request).unwrap();
969
970 assert!(
971 result_chat.ends_with(&format!(
972 "{}{}",
973 tokens::ASSISTANT_START,
974 tokens::THINKING_END
975 )),
976 "Default (new_chat) should produce chat mode",
977 );
978 }
979
980 #[test]
981 fn test_thinking_false_overrides_default_thinking() {
982 use crate::OAIPromptFormatter;
983
984 let args = std::collections::HashMap::from([("thinking".to_string(), json!(false))]);
985
986 let request = MockRequest::new(json!([
987 {"role": "system", "content": "You are a helpful assistant."},
988 {"role": "user", "content": "Hello!"}
989 ]))
990 .with_chat_template_args(args);
991
992 let formatter = DeepSeekV32Formatter::new_thinking();
994 let result = formatter.render(&request).unwrap();
995
996 assert!(
997 result.ends_with(&format!(
998 "{}{}",
999 tokens::ASSISTANT_START,
1000 tokens::THINKING_END
1001 )),
1002 "Per-request thinking=false should override new_thinking() default",
1003 );
1004 }
1005
1006 #[test]
1007 fn test_thinking_true_overrides_default_chat() {
1008 use crate::OAIPromptFormatter;
1009
1010 let args = std::collections::HashMap::from([("thinking".to_string(), json!(true))]);
1011
1012 let request = MockRequest::new(json!([
1013 {"role": "system", "content": "You are a helpful assistant."},
1014 {"role": "user", "content": "Hello!"}
1015 ]))
1016 .with_chat_template_args(args);
1017
1018 let formatter = DeepSeekV32Formatter::new_chat();
1020 let result = formatter.render(&request).unwrap();
1021
1022 assert!(
1023 result.ends_with(&format!(
1024 "{}{}",
1025 tokens::ASSISTANT_START,
1026 tokens::THINKING_START
1027 )),
1028 "Per-request thinking=true should override new_chat() default",
1029 );
1030 }
1031
1032 #[test]
1033 fn test_thinking_bool_takes_precedence_over_thinking_mode_string() {
1034 use crate::OAIPromptFormatter;
1035
1036 let args = std::collections::HashMap::from([
1037 ("thinking".to_string(), json!(false)),
1038 ("thinking_mode".to_string(), json!("thinking")),
1039 ]);
1040
1041 let request = MockRequest::new(json!([
1042 {"role": "system", "content": "You are a helpful assistant."},
1043 {"role": "user", "content": "Hello!"}
1044 ]))
1045 .with_chat_template_args(args);
1046
1047 let formatter = DeepSeekV32Formatter::new_thinking();
1048 let result = formatter.render(&request).unwrap();
1049
1050 assert!(
1052 result.ends_with(&format!(
1053 "{}{}",
1054 tokens::ASSISTANT_START,
1055 tokens::THINKING_END
1056 )),
1057 "Boolean 'thinking' key should take precedence over 'thinking_mode' string",
1058 );
1059 }
1060}