@@ -52,6 +52,7 @@ struct Delta {
5252 role : Option < String > ,
5353 tool_calls : Option < Vec < DeltaToolCall > > ,
5454 reasoning_details : Option < Vec < Value > > ,
55+ reasoning_content : Option < String > ,
5556}
5657
5758#[ derive( Serialize , Deserialize , Debug ) ]
@@ -80,6 +81,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
8081 let mut output = Vec :: new ( ) ;
8182 let mut content_array = Vec :: new ( ) ;
8283 let mut text_array = Vec :: new ( ) ;
84+ let mut reasoning_text: Option < String > = None ;
8385
8486 for content in & message. content {
8587 match content {
@@ -112,6 +114,9 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
112114 MessageContent :: SystemNotification ( _) => {
113115 continue ;
114116 }
117+ MessageContent :: Reasoning ( r) => {
118+ reasoning_text = Some ( r. text . clone ( ) ) ;
119+ }
115120 MessageContent :: ToolRequest ( request) => match & request. tool_call {
116121 Ok ( tool_call) => {
117122 let sanitized_name = sanitize_function_name ( & tool_call. name ) ;
@@ -277,6 +282,17 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
277282 converted[ "content" ] = json ! ( null) ;
278283 }
279284
285+ // DeepSeek requires reasoning_content field when tool_calls are present
286+ // Set it to the captured reasoning text, or empty string if not present
287+ if converted. get ( "tool_calls" ) . is_some ( ) {
288+ let reasoning = reasoning_text. unwrap_or_default ( ) ;
289+ converted[ "reasoning_content" ] = json ! ( reasoning) ;
290+ } else if let Some ( reasoning) = reasoning_text {
291+ if !reasoning. is_empty ( ) {
292+ converted[ "reasoning_content" ] = json ! ( reasoning) ;
293+ }
294+ }
295+
280296 if converted. get ( "content" ) . is_some ( ) || converted. get ( "tool_calls" ) . is_some ( ) {
281297 output. insert ( 0 , converted) ;
282298 }
@@ -330,6 +346,15 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
330346
331347 let mut content = Vec :: new ( ) ;
332348
349+ // Capture reasoning_content if present (for DeepSeek reasoning models)
350+ if let Some ( reasoning_content) = original. get ( "reasoning_content" ) {
351+ if let Some ( reasoning_str) = reasoning_content. as_str ( ) {
352+ if !reasoning_str. is_empty ( ) {
353+ content. push ( MessageContent :: reasoning ( reasoning_str) ) ;
354+ }
355+ }
356+ }
357+
333358 if let Some ( text) = original. get ( "content" ) {
334359 if let Some ( text_str) = text. as_str ( ) {
335360 content. push ( MessageContent :: text ( text_str) ) ;
@@ -678,23 +703,44 @@ where
678703 Some ( msg) ,
679704 usage,
680705 )
681- } else if chunk. choices[ 0 ] . delta. content. is_some( ) {
682- let text = chunk. choices[ 0 ] . delta. content. as_ref( ) . unwrap( ) ;
683- let mut msg = Message :: new(
684- Role :: Assistant ,
685- chrono:: Utc :: now( ) . timestamp( ) ,
686- vec![ MessageContent :: text( text) ] ,
687- ) ;
706+ } else if chunk. choices[ 0 ] . delta. content. is_some( ) || chunk. choices[ 0 ] . delta. reasoning_content. is_some( ) {
707+ let mut content = Vec :: new( ) ;
688708
689- // Add ID if present
690- if let Some ( id) = chunk. id {
691- msg = msg. with_id( id) ;
709+ if let Some ( reasoning) = & chunk. choices[ 0 ] . delta. reasoning_content {
710+ if !reasoning. is_empty( ) {
711+ content. push( MessageContent :: reasoning( reasoning) ) ;
712+ }
692713 }
693714
694- yield (
695- Some ( msg) ,
696- usage,
697- )
715+ if let Some ( text) = & chunk. choices[ 0 ] . delta. content {
716+ if !text. is_empty( ) {
717+ content. push( MessageContent :: text( text) ) ;
718+ }
719+ }
720+
721+ if !content. is_empty( ) {
722+ let mut msg = Message :: new(
723+ Role :: Assistant ,
724+ chrono:: Utc :: now( ) . timestamp( ) ,
725+ content,
726+ ) ;
727+
728+ // Add ID if present
729+ if let Some ( id) = chunk. id {
730+ msg = msg. with_id( id) ;
731+ }
732+
733+ yield (
734+ Some ( msg) ,
735+ if chunk. choices[ 0 ] . finish_reason. is_some( ) {
736+ usage
737+ } else {
738+ None
739+ } ,
740+ )
741+ } else if usage. is_some( ) {
742+ yield ( None , usage)
743+ }
698744 } else if usage. is_some( ) {
699745 yield ( None , usage)
700746 }
@@ -1834,4 +1880,82 @@ data: [DONE]"#;
18341880
18351881 panic ! ( "Expected tool call message with nested extra_content metadata" ) ;
18361882 }
1883+
1884+ #[ test]
1885+ fn test_response_to_message_with_reasoning_content ( ) -> anyhow:: Result < ( ) > {
1886+ // Test capturing reasoning_content from DeepSeek reasoning models
1887+ let response = json ! ( {
1888+ "choices" : [ {
1889+ "role" : "assistant" ,
1890+ "message" : {
1891+ "reasoning_content" : "Let me think about this step by step..." ,
1892+ "content" : "The answer is 9.11 is greater than 9.8"
1893+ }
1894+ } ] ,
1895+ "usage" : {
1896+ "input_tokens" : 10 ,
1897+ "output_tokens" : 25 ,
1898+ "total_tokens" : 35
1899+ }
1900+ } ) ;
1901+
1902+ let message = response_to_message ( & response) ?;
1903+ assert_eq ! ( message. content. len( ) , 2 ) ;
1904+
1905+ // First should be reasoning content
1906+ if let MessageContent :: Reasoning ( reasoning) = & message. content [ 0 ] {
1907+ assert_eq ! ( reasoning. text, "Let me think about this step by step..." ) ;
1908+ } else {
1909+ panic ! ( "Expected Reasoning content" ) ;
1910+ }
1911+
1912+ // Second should be text content
1913+ if let MessageContent :: Text ( text) = & message. content [ 1 ] {
1914+ assert_eq ! ( text. text, "The answer is 9.11 is greater than 9.8" ) ;
1915+ } else {
1916+ panic ! ( "Expected Text content" ) ;
1917+ }
1918+
1919+ Ok ( ( ) )
1920+ }
1921+
1922+ #[ test]
1923+ fn test_format_messages_with_reasoning_content ( ) -> anyhow:: Result < ( ) > {
1924+ // Test that reasoning_content is properly included in formatted messages
1925+ let mut message = Message :: assistant ( )
1926+ . with_content ( MessageContent :: reasoning ( "Thinking through the problem..." ) )
1927+ . with_text ( "The result is 42" ) ;
1928+
1929+ // Add a tool call to test that reasoning_content works with tool calls
1930+ message = message. with_tool_request (
1931+ "tool1" ,
1932+ Ok ( rmcp:: model:: CallToolRequestParams {
1933+ meta : None ,
1934+ task : None ,
1935+ name : "test_tool" . into ( ) ,
1936+ arguments : Some ( rmcp:: object!( { "param" : "value" } ) ) ,
1937+ } ) ,
1938+ ) ;
1939+
1940+ let spec = format_messages ( & [ message] , & ImageFormat :: OpenAi ) ;
1941+
1942+ assert_eq ! ( spec. len( ) , 1 ) ;
1943+ assert_eq ! ( spec[ 0 ] [ "role" ] , "assistant" ) ;
1944+
1945+ // Should have reasoning_content field
1946+ assert ! ( spec[ 0 ] . get( "reasoning_content" ) . is_some( ) ) ;
1947+ assert_eq ! (
1948+ spec[ 0 ] [ "reasoning_content" ] ,
1949+ "Thinking through the problem..."
1950+ ) ;
1951+
1952+ // Should have content
1953+ assert_eq ! ( spec[ 0 ] [ "content" ] , "The result is 42" ) ;
1954+
1955+ // Should have tool_calls
1956+ assert ! ( spec[ 0 ] [ "tool_calls" ] . is_array( ) ) ;
1957+ assert_eq ! ( spec[ 0 ] [ "tool_calls" ] [ 0 ] [ "function" ] [ "name" ] , "test_tool" ) ;
1958+
1959+ Ok ( ( ) )
1960+ }
18371961}
0 commit comments