@@ -549,3 +549,125 @@ describe("POST /v1/messages (thinking blocks non-streaming)", () => {
549549 expect ( body . content [ 0 ] . type ) . toBe ( "text" ) ;
550550 } ) ;
551551} ) ;
552+
553+ // ─── Chat Completions: reasoning_content (OpenRouter format) ────────────────
554+
555+ interface ChatCompletionChunk {
556+ id : string ;
557+ object : string ;
558+ created : number ;
559+ model : string ;
560+ choices : {
561+ index : number ;
562+ delta : { role ?: string ; content ?: string | null ; reasoning_content ?: string } ;
563+ finish_reason : string | null ;
564+ } [ ] ;
565+ }
566+
567+ function parseChatCompletionSSEChunks ( body : string ) : ChatCompletionChunk [ ] {
568+ const chunks : ChatCompletionChunk [ ] = [ ] ;
569+ for ( const line of body . split ( "\n" ) ) {
570+ if ( line . startsWith ( "data: " ) && line . slice ( 6 ) . trim ( ) !== "[DONE]" ) {
571+ chunks . push ( JSON . parse ( line . slice ( 6 ) ) as ChatCompletionChunk ) ;
572+ }
573+ }
574+ return chunks ;
575+ }
576+
577+ describe ( "POST /v1/chat/completions (reasoning_content streaming)" , ( ) => {
578+ it ( "emits reasoning_content deltas before content deltas" , async ( ) => {
579+ instance = await createServer ( allFixtures ) ;
580+ const res = await post ( `${ instance . url } /v1/chat/completions` , {
581+ model : "gpt-4" ,
582+ messages : [ { role : "user" , content : "think" } ] ,
583+ stream : true ,
584+ } ) ;
585+
586+ expect ( res . status ) . toBe ( 200 ) ;
587+ const chunks = parseChatCompletionSSEChunks ( res . body ) ;
588+
589+ const reasoningChunks = chunks . filter ( ( c ) => c . choices [ 0 ] ?. delta . reasoning_content ) ;
590+ const contentChunks = chunks . filter (
591+ ( c ) => c . choices [ 0 ] ?. delta . content && c . choices [ 0 ] . delta . content . length > 0 ,
592+ ) ;
593+
594+ expect ( reasoningChunks . length ) . toBeGreaterThan ( 0 ) ;
595+ expect ( contentChunks . length ) . toBeGreaterThan ( 0 ) ;
596+
597+ // All reasoning chunks appear before all content chunks
598+ const lastReasoningIdx = chunks . lastIndexOf ( reasoningChunks [ reasoningChunks . length - 1 ] ) ;
599+ const firstContentIdx = chunks . indexOf ( contentChunks [ 0 ] ) ;
600+ expect ( lastReasoningIdx ) . toBeLessThan ( firstContentIdx ) ;
601+ } ) ;
602+
603+ it ( "reasoning_content deltas reconstruct full reasoning text" , async ( ) => {
604+ instance = await createServer ( allFixtures ) ;
605+ const res = await post ( `${ instance . url } /v1/chat/completions` , {
606+ model : "gpt-4" ,
607+ messages : [ { role : "user" , content : "think" } ] ,
608+ stream : true ,
609+ } ) ;
610+
611+ const chunks = parseChatCompletionSSEChunks ( res . body ) ;
612+ const reasoning = chunks . map ( ( c ) => c . choices [ 0 ] ?. delta . reasoning_content ?? "" ) . join ( "" ) ;
613+ expect ( reasoning ) . toBe ( "Let me think step by step about this problem." ) ;
614+ } ) ;
615+
616+ it ( "content deltas still reconstruct full text" , async ( ) => {
617+ instance = await createServer ( allFixtures ) ;
618+ const res = await post ( `${ instance . url } /v1/chat/completions` , {
619+ model : "gpt-4" ,
620+ messages : [ { role : "user" , content : "think" } ] ,
621+ stream : true ,
622+ } ) ;
623+
624+ const chunks = parseChatCompletionSSEChunks ( res . body ) ;
625+ const content = chunks . map ( ( c ) => c . choices [ 0 ] ?. delta . content ?? "" ) . join ( "" ) ;
626+ expect ( content ) . toBe ( "The answer is 42." ) ;
627+ } ) ;
628+
629+ it ( "no reasoning_content when reasoning is absent" , async ( ) => {
630+ instance = await createServer ( allFixtures ) ;
631+ const res = await post ( `${ instance . url } /v1/chat/completions` , {
632+ model : "gpt-4" ,
633+ messages : [ { role : "user" , content : "plain" } ] ,
634+ stream : true ,
635+ } ) ;
636+
637+ const chunks = parseChatCompletionSSEChunks ( res . body ) ;
638+ const reasoningChunks = chunks . filter ( ( c ) => c . choices [ 0 ] ?. delta . reasoning_content ) ;
639+ expect ( reasoningChunks ) . toHaveLength ( 0 ) ;
640+ } ) ;
641+ } ) ;
642+
643+ describe ( "POST /v1/chat/completions (reasoning_content non-streaming)" , ( ) => {
644+ it ( "includes reasoning_content in non-streaming response" , async ( ) => {
645+ instance = await createServer ( allFixtures ) ;
646+ const res = await post ( `${ instance . url } /v1/chat/completions` , {
647+ model : "gpt-4" ,
648+ messages : [ { role : "user" , content : "think" } ] ,
649+ stream : false ,
650+ } ) ;
651+
652+ expect ( res . status ) . toBe ( 200 ) ;
653+ const body = JSON . parse ( res . body ) ;
654+ expect ( body . object ) . toBe ( "chat.completion" ) ;
655+ expect ( body . choices [ 0 ] . message . content ) . toBe ( "The answer is 42." ) ;
656+ expect ( body . choices [ 0 ] . message . reasoning_content ) . toBe (
657+ "Let me think step by step about this problem." ,
658+ ) ;
659+ } ) ;
660+
661+ it ( "no reasoning_content when reasoning is absent" , async ( ) => {
662+ instance = await createServer ( allFixtures ) ;
663+ const res = await post ( `${ instance . url } /v1/chat/completions` , {
664+ model : "gpt-4" ,
665+ messages : [ { role : "user" , content : "plain" } ] ,
666+ stream : false ,
667+ } ) ;
668+
669+ const body = JSON . parse ( res . body ) ;
670+ expect ( body . choices [ 0 ] . message . content ) . toBe ( "Just plain text." ) ;
671+ expect ( body . choices [ 0 ] . message . reasoning_content ) . toBeUndefined ( ) ;
672+ } ) ;
673+ } ) ;
0 commit comments