@@ -29,23 +29,75 @@ vi.mock("openai", () => {
2929 }
3030 }
3131
32+ // Check if this is a reasoning_content test by looking at model
33+ const isReasonerModel = options . model ?. includes ( "deepseek-reasoner" )
34+ const isToolCallTest = options . tools ?. length > 0
35+
3236 // Return async iterator for streaming
3337 return {
3438 [ Symbol . asyncIterator ] : async function * ( ) {
35- yield {
36- choices : [
37- {
38- delta : { content : "Test response" } ,
39- index : 0 ,
40- } ,
41- ] ,
42- usage : null ,
39+ // For reasoner models, emit reasoning_content first
40+ if ( isReasonerModel ) {
41+ yield {
42+ choices : [
43+ {
44+ delta : { reasoning_content : "Let me think about this..." } ,
45+ index : 0 ,
46+ } ,
47+ ] ,
48+ usage : null ,
49+ }
50+ yield {
51+ choices : [
52+ {
53+ delta : { reasoning_content : " I'll analyze step by step." } ,
54+ index : 0 ,
55+ } ,
56+ ] ,
57+ usage : null ,
58+ }
4359 }
60+
61+ // For tool call tests with reasoner, emit tool call
62+ if ( isReasonerModel && isToolCallTest ) {
63+ yield {
64+ choices : [
65+ {
66+ delta : {
67+ tool_calls : [
68+ {
69+ index : 0 ,
70+ id : "call_123" ,
71+ function : {
72+ name : "get_weather" ,
73+ arguments : '{"location":"SF"}' ,
74+ } ,
75+ } ,
76+ ] ,
77+ } ,
78+ index : 0 ,
79+ } ,
80+ ] ,
81+ usage : null ,
82+ }
83+ } else {
84+ yield {
85+ choices : [
86+ {
87+ delta : { content : "Test response" } ,
88+ index : 0 ,
89+ } ,
90+ ] ,
91+ usage : null ,
92+ }
93+ }
94+
4495 yield {
4596 choices : [
4697 {
4798 delta : { } ,
4899 index : 0 ,
100+ finish_reason : isToolCallTest ? "tool_calls" : "stop" ,
49101 } ,
50102 ] ,
51103 usage : {
@@ -70,7 +122,7 @@ vi.mock("openai", () => {
70122import OpenAI from "openai"
71123import type { Anthropic } from "@anthropic-ai/sdk"
72124
73- import { deepSeekDefaultModelId } from "@roo-code/types"
125+ import { deepSeekDefaultModelId , type ModelInfo } from "@roo-code/types"
74126
75127import type { ApiHandlerOptions } from "../../../shared/api"
76128
@@ -174,6 +226,27 @@ describe("DeepSeekHandler", () => {
174226 expect ( model . info . supportsPromptCache ) . toBe ( true )
175227 } )
176228
229+ it ( "should have preserveReasoning enabled for deepseek-reasoner to support interleaved thinking" , ( ) => {
230+ // This is critical for DeepSeek's interleaved thinking mode with tool calls.
231+ // See: https://api-docs.deepseek.com/guides/thinking_mode
232+ // The reasoning_content needs to be passed back during tool call continuation
233+ // within the same turn for the model to continue reasoning properly.
234+ const handlerWithReasoner = new DeepSeekHandler ( {
235+ ...mockOptions ,
236+ apiModelId : "deepseek-reasoner" ,
237+ } )
238+ const model = handlerWithReasoner . getModel ( )
239+ // Cast to ModelInfo to access preserveReasoning which is an optional property
240+ expect ( ( model . info as ModelInfo ) . preserveReasoning ) . toBe ( true )
241+ } )
242+
243+ it ( "should NOT have preserveReasoning enabled for deepseek-chat" , ( ) => {
244+ // deepseek-chat doesn't use thinking mode, so no need to preserve reasoning
245+ const model = handler . getModel ( )
246+ // Cast to ModelInfo to access preserveReasoning which is an optional property
247+ expect ( ( model . info as ModelInfo ) . preserveReasoning ) . toBeUndefined ( )
248+ } )
249+
177250 it ( "should return provided model ID with default model info if model does not exist" , ( ) => {
178251 const handlerWithInvalidModel = new DeepSeekHandler ( {
179252 ...mockOptions ,
@@ -317,4 +390,108 @@ describe("DeepSeekHandler", () => {
317390 expect ( result . cacheReadTokens ) . toBeUndefined ( )
318391 } )
319392 } )
393+
394+ describe ( "interleaved thinking mode" , ( ) => {
395+ const systemPrompt = "You are a helpful assistant."
396+ const messages : Anthropic . Messages . MessageParam [ ] = [
397+ {
398+ role : "user" ,
399+ content : [
400+ {
401+ type : "text" as const ,
402+ text : "Hello!" ,
403+ } ,
404+ ] ,
405+ } ,
406+ ]
407+
408+ it ( "should handle reasoning_content in streaming responses for deepseek-reasoner" , async ( ) => {
409+ const reasonerHandler = new DeepSeekHandler ( {
410+ ...mockOptions ,
411+ apiModelId : "deepseek-reasoner" ,
412+ } )
413+
414+ const stream = reasonerHandler . createMessage ( systemPrompt , messages )
415+ const chunks : any [ ] = [ ]
416+ for await ( const chunk of stream ) {
417+ chunks . push ( chunk )
418+ }
419+
420+ // Should have reasoning chunks
421+ const reasoningChunks = chunks . filter ( ( chunk ) => chunk . type === "reasoning" )
422+ expect ( reasoningChunks . length ) . toBeGreaterThan ( 0 )
423+ expect ( reasoningChunks [ 0 ] . text ) . toBe ( "Let me think about this..." )
424+ expect ( reasoningChunks [ 1 ] . text ) . toBe ( " I'll analyze step by step." )
425+ } )
426+
427+ it ( "should pass thinking parameter for deepseek-reasoner model" , async ( ) => {
428+ const reasonerHandler = new DeepSeekHandler ( {
429+ ...mockOptions ,
430+ apiModelId : "deepseek-reasoner" ,
431+ } )
432+
433+ const stream = reasonerHandler . createMessage ( systemPrompt , messages )
434+ for await ( const _chunk of stream ) {
435+ // Consume the stream
436+ }
437+
438+ // Verify that the thinking parameter was passed to the API
439+ // Note: mockCreate receives two arguments - request options and path options
440+ expect ( mockCreate ) . toHaveBeenCalledWith (
441+ expect . objectContaining ( {
442+ thinking : { type : "enabled" } ,
443+ } ) ,
444+ { } , // Empty path options for non-Azure URLs
445+ )
446+ } )
447+
448+ it ( "should NOT pass thinking parameter for deepseek-chat model" , async ( ) => {
449+ const chatHandler = new DeepSeekHandler ( {
450+ ...mockOptions ,
451+ apiModelId : "deepseek-chat" ,
452+ } )
453+
454+ const stream = chatHandler . createMessage ( systemPrompt , messages )
455+ for await ( const _chunk of stream ) {
456+ // Consume the stream
457+ }
458+
459+ // Verify that the thinking parameter was NOT passed to the API
460+ const callArgs = mockCreate . mock . calls [ 0 ] [ 0 ]
461+ expect ( callArgs . thinking ) . toBeUndefined ( )
462+ } )
463+
464+ it ( "should handle tool calls with reasoning_content" , async ( ) => {
465+ const reasonerHandler = new DeepSeekHandler ( {
466+ ...mockOptions ,
467+ apiModelId : "deepseek-reasoner" ,
468+ } )
469+
470+ const tools : any [ ] = [
471+ {
472+ type : "function" ,
473+ function : {
474+ name : "get_weather" ,
475+ description : "Get weather" ,
476+ parameters : { type : "object" , properties : { } } ,
477+ } ,
478+ } ,
479+ ]
480+
481+ const stream = reasonerHandler . createMessage ( systemPrompt , messages , { taskId : "test" , tools } )
482+ const chunks : any [ ] = [ ]
483+ for await ( const chunk of stream ) {
484+ chunks . push ( chunk )
485+ }
486+
487+ // Should have reasoning chunks
488+ const reasoningChunks = chunks . filter ( ( chunk ) => chunk . type === "reasoning" )
489+ expect ( reasoningChunks . length ) . toBeGreaterThan ( 0 )
490+
491+ // Should have tool call chunks
492+ const toolCallChunks = chunks . filter ( ( chunk ) => chunk . type === "tool_call_partial" )
493+ expect ( toolCallChunks . length ) . toBeGreaterThan ( 0 )
494+ expect ( toolCallChunks [ 0 ] . name ) . toBe ( "get_weather" )
495+ } )
496+ } )
320497} )
0 commit comments