@@ -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+ }
59+ }
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+ }
4393 }
94+
4495 yield {
4596 choices : [
4697 {
4798 delta : { } ,
4899 index : 0 ,
100+ finish_reason : isToolCallTest ? "tool_calls" : "stop" ,
49101 } ,
50102 ] ,
51103 usage : {
@@ -317,4 +369,155 @@ describe("DeepSeekHandler", () => {
317369 expect ( result . cacheReadTokens ) . toBeUndefined ( )
318370 } )
319371 } )
372+
373+ describe ( "interleaved thinking mode" , ( ) => {
374+ const systemPrompt = "You are a helpful assistant."
375+ const messages : Anthropic . Messages . MessageParam [ ] = [
376+ {
377+ role : "user" ,
378+ content : [
379+ {
380+ type : "text" as const ,
381+ text : "Hello!" ,
382+ } ,
383+ ] ,
384+ } ,
385+ ]
386+
387+ it ( "should handle reasoning_content in streaming responses for deepseek-reasoner" , async ( ) => {
388+ const reasonerHandler = new DeepSeekHandler ( {
389+ ...mockOptions ,
390+ apiModelId : "deepseek-reasoner" ,
391+ } )
392+
393+ const stream = reasonerHandler . createMessage ( systemPrompt , messages )
394+ const chunks : any [ ] = [ ]
395+ for await ( const chunk of stream ) {
396+ chunks . push ( chunk )
397+ }
398+
399+ // Should have reasoning chunks
400+ const reasoningChunks = chunks . filter ( ( chunk ) => chunk . type === "reasoning" )
401+ expect ( reasoningChunks . length ) . toBeGreaterThan ( 0 )
402+ expect ( reasoningChunks [ 0 ] . text ) . toBe ( "Let me think about this..." )
403+ expect ( reasoningChunks [ 1 ] . text ) . toBe ( " I'll analyze step by step." )
404+ } )
405+
406+ it ( "should accumulate reasoning content via getReasoningContent()" , async ( ) => {
407+ const reasonerHandler = new DeepSeekHandler ( {
408+ ...mockOptions ,
409+ apiModelId : "deepseek-reasoner" ,
410+ } )
411+
412+ // Before any API call, reasoning content should be undefined
413+ expect ( reasonerHandler . getReasoningContent ( ) ) . toBeUndefined ( )
414+
415+ const stream = reasonerHandler . createMessage ( systemPrompt , messages )
416+ for await ( const _chunk of stream ) {
417+ // Consume the stream
418+ }
419+
420+ // After streaming, reasoning content should be accumulated
421+ const reasoningContent = reasonerHandler . getReasoningContent ( )
422+ expect ( reasoningContent ) . toBe ( "Let me think about this... I'll analyze step by step." )
423+ } )
424+
425+ it ( "should pass thinking parameter for deepseek-reasoner model" , async ( ) => {
426+ const reasonerHandler = new DeepSeekHandler ( {
427+ ...mockOptions ,
428+ apiModelId : "deepseek-reasoner" ,
429+ } )
430+
431+ const stream = reasonerHandler . createMessage ( systemPrompt , messages )
432+ for await ( const _chunk of stream ) {
433+ // Consume the stream
434+ }
435+
436+ // Verify that the thinking parameter was passed to the API
437+ expect ( mockCreate ) . toHaveBeenCalledWith (
438+ expect . objectContaining ( {
439+ thinking : { type : "enabled" } ,
440+ } ) ,
441+ )
442+ } )
443+
444+ it ( "should NOT pass thinking parameter for deepseek-chat model" , async ( ) => {
445+ const chatHandler = new DeepSeekHandler ( {
446+ ...mockOptions ,
447+ apiModelId : "deepseek-chat" ,
448+ } )
449+
450+ const stream = chatHandler . createMessage ( systemPrompt , messages )
451+ for await ( const _chunk of stream ) {
452+ // Consume the stream
453+ }
454+
455+ // Verify that the thinking parameter was NOT passed to the API
456+ const callArgs = mockCreate . mock . calls [ 0 ] [ 0 ]
457+ expect ( callArgs . thinking ) . toBeUndefined ( )
458+ } )
459+
460+ it ( "should handle tool calls with reasoning_content" , async ( ) => {
461+ const reasonerHandler = new DeepSeekHandler ( {
462+ ...mockOptions ,
463+ apiModelId : "deepseek-reasoner" ,
464+ } )
465+
466+ const tools : any [ ] = [
467+ {
468+ type : "function" ,
469+ function : {
470+ name : "get_weather" ,
471+ description : "Get weather" ,
472+ parameters : { type : "object" , properties : { } } ,
473+ } ,
474+ } ,
475+ ]
476+
477+ const stream = reasonerHandler . createMessage ( systemPrompt , messages , { taskId : "test" , tools } )
478+ const chunks : any [ ] = [ ]
479+ for await ( const chunk of stream ) {
480+ chunks . push ( chunk )
481+ }
482+
483+ // Should have reasoning chunks
484+ const reasoningChunks = chunks . filter ( ( chunk ) => chunk . type === "reasoning" )
485+ expect ( reasoningChunks . length ) . toBeGreaterThan ( 0 )
486+
487+ // Should have tool call chunks
488+ const toolCallChunks = chunks . filter ( ( chunk ) => chunk . type === "tool_call_partial" )
489+ expect ( toolCallChunks . length ) . toBeGreaterThan ( 0 )
490+ expect ( toolCallChunks [ 0 ] . name ) . toBe ( "get_weather" )
491+
492+ // Reasoning content should be accumulated for potential continuation
493+ const reasoningContent = reasonerHandler . getReasoningContent ( )
494+ expect ( reasoningContent ) . toBeDefined ( )
495+ } )
496+
497+ it ( "should reset reasoning content for each new request" , async ( ) => {
498+ const reasonerHandler = new DeepSeekHandler ( {
499+ ...mockOptions ,
500+ apiModelId : "deepseek-reasoner" ,
501+ } )
502+
503+ // First request
504+ const stream1 = reasonerHandler . createMessage ( systemPrompt , messages )
505+ for await ( const _chunk of stream1 ) {
506+ // Consume the stream
507+ }
508+
509+ const reasoningContent1 = reasonerHandler . getReasoningContent ( )
510+ expect ( reasoningContent1 ) . toBeDefined ( )
511+
512+ // Second request should reset the reasoning content
513+ const stream2 = reasonerHandler . createMessage ( systemPrompt , messages )
514+ for await ( const _chunk of stream2 ) {
515+ // Consume the stream
516+ }
517+
518+ // The reasoning content should be fresh from the second request
519+ const reasoningContent2 = reasonerHandler . getReasoningContent ( )
520+ expect ( reasoningContent2 ) . toBe ( "Let me think about this... I'll analyze step by step." )
521+ } )
522+ } )
320523} )
0 commit comments