@@ -1084,6 +1084,157 @@ describe("convertToOpenAiMessages", () => {
10841084 expect ( assistantMessage . reasoning_details [ 2 ] . data ) . toBe ( "encrypted_data" )
10851085 } )
10861086 } )
1087+
1088+ describe ( "reasoning_content round-trip for DeepSeek / Z.ai thinking mode" , ( ) => {
1089+ it ( "should pass through top-level reasoning_content on assistant messages" , ( ) => {
1090+ const anthropicMessages = [
1091+ {
1092+ role : "assistant" as const ,
1093+ content : "Here is my answer." ,
1094+ reasoning_content : "Let me think about this carefully..." ,
1095+ } ,
1096+ ] as any as Anthropic . Messages . MessageParam [ ]
1097+
1098+ const result = convertToOpenAiMessages ( anthropicMessages )
1099+
1100+ expect ( result ) . toHaveLength ( 1 )
1101+ expect ( ( result [ 0 ] as any ) . reasoning_content ) . toBe ( "Let me think about this carefully..." )
1102+ } )
1103+
1104+ it ( "should extract reasoning_content from reasoning content block" , ( ) => {
1105+ // buildCleanConversationHistory stores reasoning as a content block when preserveReasoning=true
1106+ const anthropicMessages = [
1107+ {
1108+ role : "assistant" as const ,
1109+ content : [
1110+ { type : "reasoning" , text : "Let me think..." , summary : [ ] } ,
1111+ { type : "text" , text : "My answer." } ,
1112+ ] ,
1113+ } ,
1114+ ] as any as Anthropic . Messages . MessageParam [ ]
1115+
1116+ const result = convertToOpenAiMessages ( anthropicMessages )
1117+
1118+ expect ( result ) . toHaveLength ( 1 )
1119+ const msg = result [ 0 ] as any
1120+ expect ( msg . reasoning_content ) . toBe ( "Let me think..." )
1121+ expect ( msg . content ) . toBe ( "My answer." )
1122+ } )
1123+
1124+ it ( "should extract reasoning_content from reasoning block alongside tool calls" , ( ) => {
1125+ // The critical case: DeepSeek thinking + tool call in the same turn.
1126+ // Without reasoning_content on the second request, DeepSeek returns 400:
1127+ // "The reasoning_content in the thinking mode must be passed back to the API."
1128+ const anthropicMessages = [
1129+ {
1130+ role : "assistant" as const ,
1131+ content : [
1132+ { type : "reasoning" , text : "I need to read a file." , summary : [ ] } ,
1133+ {
1134+ type : "tool_use" ,
1135+ id : "call_abc" ,
1136+ name : "read_file" ,
1137+ input : { path : "foo.txt" } ,
1138+ } ,
1139+ ] ,
1140+ } ,
1141+ ] as any as Anthropic . Messages . MessageParam [ ]
1142+
1143+ const result = convertToOpenAiMessages ( anthropicMessages )
1144+
1145+ expect ( result ) . toHaveLength ( 1 )
1146+ const msg = result [ 0 ] as any
1147+ expect ( msg . reasoning_content ) . toBe ( "I need to read a file." )
1148+ expect ( msg . tool_calls ) . toHaveLength ( 1 )
1149+ expect ( msg . tool_calls [ 0 ] . id ) . toBe ( "call_abc" )
1150+ } )
1151+
1152+ it ( "should accumulate multiple reasoning blocks in order, separated by a tool call" , ( ) => {
1153+ // DeepSeek / Z.ai interleaved thinking can emit more than one reasoning block per
1154+ // turn. A regression that overwrites (instead of accumulates) would silently drop
1155+ // all but the last block.
1156+ const anthropicMessages = [
1157+ {
1158+ role : "assistant" as const ,
1159+ content : [
1160+ { type : "reasoning" , text : "First, I should check the file." , summary : [ ] } ,
1161+ {
1162+ type : "tool_use" ,
1163+ id : "call_abc" ,
1164+ name : "read_file" ,
1165+ input : { path : "foo.txt" } ,
1166+ } ,
1167+ { type : "reasoning" , text : "Now I know what to do next." , summary : [ ] } ,
1168+ ] ,
1169+ } ,
1170+ ] as any as Anthropic . Messages . MessageParam [ ]
1171+
1172+ const result = convertToOpenAiMessages ( anthropicMessages )
1173+
1174+ expect ( result ) . toHaveLength ( 1 )
1175+ const msg = result [ 0 ] as any
1176+ expect ( msg . reasoning_content ) . toBe ( "First, I should check the file.Now I know what to do next." )
1177+ expect ( msg . tool_calls ) . toHaveLength ( 1 )
1178+ expect ( msg . tool_calls [ 0 ] . id ) . toBe ( "call_abc" )
1179+ } )
1180+
1181+ it ( "should prefer top-level reasoning_content over content block" , ( ) => {
1182+ const anthropicMessages = [
1183+ {
1184+ role : "assistant" as const ,
1185+ content : [
1186+ { type : "reasoning" , text : "block reasoning" , summary : [ ] } ,
1187+ { type : "text" , text : "answer" } ,
1188+ ] ,
1189+ reasoning_content : "top-level reasoning" ,
1190+ } ,
1191+ ] as any as Anthropic . Messages . MessageParam [ ]
1192+
1193+ const result = convertToOpenAiMessages ( anthropicMessages )
1194+
1195+ expect ( ( result [ 0 ] as any ) . reasoning_content ) . toBe ( "top-level reasoning" )
1196+ } )
1197+
1198+ it ( "should not set reasoning_content when there is none" , ( ) => {
1199+ const anthropicMessages : Anthropic . Messages . MessageParam [ ] = [
1200+ {
1201+ role : "assistant" ,
1202+ content : [
1203+ {
1204+ type : "tool_use" ,
1205+ id : "call_abc" ,
1206+ name : "read_file" ,
1207+ input : { path : "foo.txt" } ,
1208+ } ,
1209+ ] ,
1210+ } ,
1211+ ]
1212+
1213+ const result = convertToOpenAiMessages ( anthropicMessages )
1214+
1215+ expect ( result ) . toHaveLength ( 1 )
1216+ expect ( ( result [ 0 ] as any ) . reasoning_content ) . toBeUndefined ( )
1217+ } )
1218+
1219+ it ( "should ignore non-object content parts without crashing (defensive guard)" , ( ) => {
1220+ // getReasoningBlockText guards against non-object parts (e.g. a stray
1221+ // string in the content array). Such parts are not reasoning blocks and
1222+ // must be skipped rather than crashing or being misread as reasoning.
1223+ const anthropicMessages = [
1224+ {
1225+ role : "assistant" as const ,
1226+ content : [ "not-a-block" as any , { type : "text" , text : "answer" } ] ,
1227+ } ,
1228+ ] as any as Anthropic . Messages . MessageParam [ ]
1229+
1230+ const result = convertToOpenAiMessages ( anthropicMessages )
1231+
1232+ expect ( result ) . toHaveLength ( 1 )
1233+ const msg = result [ 0 ] as any
1234+ expect ( msg . content ) . toBe ( "answer" )
1235+ expect ( msg . reasoning_content ) . toBeUndefined ( )
1236+ } )
1237+ } )
10871238} )
10881239
10891240describe ( "consolidateReasoningDetails" , ( ) => {
0 commit comments