@@ -249,3 +249,209 @@ describe('anthropicMessagesToOpenAI', () => {
249249 )
250250 } )
251251} )
252+
253+ describe ( 'DeepSeek thinking mode (enableThinking)' , ( ) => {
254+ test ( 'preserves thinking block as reasoning_content when enabled' , ( ) => {
255+ const result = anthropicMessagesToOpenAI (
256+ [ makeUserMsg ( 'question' ) , makeAssistantMsg ( [
257+ { type : 'thinking' as const , thinking : 'Let me reason about this...' } ,
258+ { type : 'text' , text : 'The answer is 42.' } ,
259+ ] ) ] ,
260+ [ ] as any ,
261+ { enableThinking : true } ,
262+ )
263+ // Should have: user, assistant with reasoning_content
264+ expect ( result ) . toHaveLength ( 2 )
265+ expect ( result [ 0 ] . role ) . toBe ( 'user' )
266+ const assistant = result [ 1 ] as any
267+ expect ( assistant . role ) . toBe ( 'assistant' )
268+ expect ( assistant . content ) . toBe ( 'The answer is 42.' )
269+ expect ( assistant . reasoning_content ) . toBe ( 'Let me reason about this...' )
270+ } )
271+
272+ test ( 'drops thinking block when enableThinking is false (default)' , ( ) => {
273+ const result = anthropicMessagesToOpenAI (
274+ [ makeAssistantMsg ( [
275+ { type : 'thinking' as const , thinking : 'internal thoughts...' } ,
276+ { type : 'text' , text : 'visible response' } ,
277+ ] ) ] ,
278+ [ ] as any ,
279+ )
280+ const assistant = result [ 0 ] as any
281+ expect ( assistant . content ) . toBe ( 'visible response' )
282+ expect ( assistant . reasoning_content ) . toBeUndefined ( )
283+ } )
284+
285+ test ( 'preserves reasoning_content with tool_calls in same turn' , ( ) => {
286+ const result = anthropicMessagesToOpenAI (
287+ [
288+ makeUserMsg ( 'what is the weather?' ) ,
289+ makeAssistantMsg ( [
290+ { type : 'thinking' as const , thinking : 'I need to call the weather tool.' } ,
291+ { type : 'text' , text : '' } ,
292+ {
293+ type : 'tool_use' as const ,
294+ id : 'toolu_001' ,
295+ name : 'get_weather' ,
296+ input : { location : 'Hangzhou' } ,
297+ } ,
298+ ] ) ,
299+ makeUserMsg ( [
300+ {
301+ type : 'tool_result' as const ,
302+ tool_use_id : 'toolu_001' ,
303+ content : 'Cloudy 7~13°C' ,
304+ } ,
305+ ] ) ,
306+ ] ,
307+ [ ] as any ,
308+ { enableThinking : true } ,
309+ )
310+
311+ // Find the assistant message
312+ const assistants = result . filter ( m => m . role === 'assistant' )
313+ expect ( assistants . length ) . toBe ( 1 )
314+ const assistant = assistants [ 0 ] as any
315+ expect ( assistant . reasoning_content ) . toBe ( 'I need to call the weather tool.' )
316+ expect ( assistant . tool_calls ) . toBeDefined ( )
317+ expect ( assistant . tool_calls [ 0 ] . function . name ) . toBe ( 'get_weather' )
318+ } )
319+
320+ test ( 'strips reasoning_content from previous turns' , ( ) => {
321+ const result = anthropicMessagesToOpenAI (
322+ [
323+ // Turn 1: user → assistant (with thinking)
324+ makeUserMsg ( 'question 1' ) ,
325+ makeAssistantMsg ( [
326+ { type : 'thinking' as const , thinking : 'Turn 1 reasoning...' } ,
327+ { type : 'text' , text : 'Turn 1 answer' } ,
328+ ] ) ,
329+ // Turn 2: new user message → previous reasoning should be stripped
330+ makeUserMsg ( 'question 2' ) ,
331+ makeAssistantMsg ( [
332+ { type : 'thinking' as const , thinking : 'Turn 2 reasoning...' } ,
333+ { type : 'text' , text : 'Turn 2 answer' } ,
334+ ] ) ,
335+ ] ,
336+ [ ] as any ,
337+ { enableThinking : true } ,
338+ )
339+
340+ const assistants = result . filter ( m => m . role === 'assistant' )
341+ // Turn 1 assistant: reasoning should be stripped (previous turn)
342+ expect ( ( assistants [ 0 ] as any ) . reasoning_content ) . toBeUndefined ( )
343+ expect ( ( assistants [ 0 ] as any ) . content ) . toBe ( 'Turn 1 answer' )
344+ // Turn 2 assistant: reasoning should be preserved (current turn)
345+ expect ( ( assistants [ 1 ] as any ) . reasoning_content ) . toBe ( 'Turn 2 reasoning...' )
346+ expect ( ( assistants [ 1 ] as any ) . content ) . toBe ( 'Turn 2 answer' )
347+ } )
348+
349+ test ( 'preserves reasoning_content in multi-iteration tool call within same turn' , ( ) => {
350+ // Simulates a full DeepSeek tool call iteration:
351+ // user → assistant(thinking+tool_call) → tool_result → assistant(thinking+tool_call) → tool_result → assistant(thinking+text)
352+ const result = anthropicMessagesToOpenAI (
353+ [
354+ makeUserMsg ( "tomorrow's weather in Hangzhou" ) ,
355+ // Iteration 1: thinking + tool call
356+ makeAssistantMsg ( [
357+ { type : 'thinking' as const , thinking : 'I need the date first.' } ,
358+ {
359+ type : 'tool_use' as const ,
360+ id : 'toolu_001' ,
361+ name : 'get_date' ,
362+ input : { } ,
363+ } ,
364+ ] ) ,
365+ makeUserMsg ( [
366+ {
367+ type : 'tool_result' as const ,
368+ tool_use_id : 'toolu_001' ,
369+ content : '2026-04-08' ,
370+ } ,
371+ ] ) ,
372+ // Iteration 2: thinking + tool call
373+ makeAssistantMsg ( [
374+ { type : 'thinking' as const , thinking : 'Now I can get the weather.' } ,
375+ {
376+ type : 'tool_use' as const ,
377+ id : 'toolu_002' ,
378+ name : 'get_weather' ,
379+ input : { location : 'Hangzhou' , date : '2026-04-08' } ,
380+ } ,
381+ ] ) ,
382+ makeUserMsg ( [
383+ {
384+ type : 'tool_result' as const ,
385+ tool_use_id : 'toolu_002' ,
386+ content : 'Cloudy 7~13°C' ,
387+ } ,
388+ ] ) ,
389+ // Iteration 3: thinking + final answer
390+ makeAssistantMsg ( [
391+ { type : 'thinking' as const , thinking : 'I have the info now.' } ,
392+ { type : 'text' , text : 'Tomorrow will be cloudy, 7-13°C.' } ,
393+ ] ) ,
394+ ] ,
395+ [ ] as any ,
396+ { enableThinking : true } ,
397+ )
398+
399+ // All 3 assistant messages are in the current turn (after last user msg is the last tool_result,
400+ // but the "last user message" boundary logic finds the last user-typed message).
401+ // Actually, tool_result messages are also UserMessage type, so the last user message
402+ // is the one with tool_result for toolu_002. All assistant messages after that should have reasoning.
403+ const assistants = result . filter ( m => m . role === 'assistant' )
404+ expect ( assistants . length ) . toBe ( 3 )
405+ // All iterations within the same turn preserve reasoning
406+ expect ( ( assistants [ 0 ] as any ) . reasoning_content ) . toBe ( 'I need the date first.' )
407+ expect ( ( assistants [ 1 ] as any ) . reasoning_content ) . toBe ( 'Now I can get the weather.' )
408+ expect ( ( assistants [ 2 ] as any ) . reasoning_content ) . toBe ( 'I have the info now.' )
409+ } )
410+
411+ test ( 'handles multiple thinking blocks in single assistant message' , ( ) => {
412+ const result = anthropicMessagesToOpenAI (
413+ [ makeUserMsg ( 'question' ) , makeAssistantMsg ( [
414+ { type : 'thinking' as const , thinking : 'First thought.' } ,
415+ { type : 'thinking' as const , thinking : 'Second thought.' } ,
416+ { type : 'text' , text : 'Final answer.' } ,
417+ ] ) ] ,
418+ [ ] as any ,
419+ { enableThinking : true } ,
420+ )
421+ const assistant = result . filter ( m => m . role === 'assistant' ) [ 0 ] as any
422+ expect ( assistant . reasoning_content ) . toBe ( 'First thought.\nSecond thought.' )
423+ } )
424+
425+ test ( 'skips empty thinking blocks' , ( ) => {
426+ const result = anthropicMessagesToOpenAI (
427+ [ makeUserMsg ( 'question' ) , makeAssistantMsg ( [
428+ { type : 'thinking' as const , thinking : '' } ,
429+ { type : 'text' , text : 'Answer.' } ,
430+ ] ) ] ,
431+ [ ] as any ,
432+ { enableThinking : true } ,
433+ )
434+ const assistant = result . filter ( m => m . role === 'assistant' ) [ 0 ] as any
435+ expect ( assistant . reasoning_content ) . toBeUndefined ( )
436+ } )
437+
438+ test ( 'sets content to null when only thinking and tool_calls present' , ( ) => {
439+ const result = anthropicMessagesToOpenAI (
440+ [ makeUserMsg ( 'question' ) , makeAssistantMsg ( [
441+ { type : 'thinking' as const , thinking : 'Reasoning only.' } ,
442+ {
443+ type : 'tool_use' as const ,
444+ id : 'toolu_001' ,
445+ name : 'bash' ,
446+ input : { command : 'ls' } ,
447+ } ,
448+ ] ) ] ,
449+ [ ] as any ,
450+ { enableThinking : true } ,
451+ )
452+ const assistant = result . filter ( m => m . role === 'assistant' ) [ 0 ] as any
453+ expect ( assistant . content ) . toBeNull ( )
454+ expect ( assistant . reasoning_content ) . toBe ( 'Reasoning only.' )
455+ expect ( assistant . tool_calls ) . toHaveLength ( 1 )
456+ } )
457+ } )
0 commit comments