@@ -431,6 +431,96 @@ describe("parseOpenAIStream", () => {
431431 }
432432 } ) ;
433433
434+ it ( "应解析单个 chunk 内的 <think>...</think> 标签" , async ( ) => {
435+ const reader = createMockReader ( [
436+ 'data: {"choices":[{"delta":{"content":"before<think>reasoning</think>after"}}]}\n\n' ,
437+ "data: [DONE]\n\n" ,
438+ ] ) ;
439+
440+ const events : ChatStreamEvent [ ] = [ ] ;
441+ const controller = new AbortController ( ) ;
442+
443+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
444+
445+ expect ( events ) . toEqual ( [
446+ { type : "content_delta" , delta : "before" } ,
447+ { type : "thinking_delta" , delta : "reasoning" } ,
448+ { type : "content_delta" , delta : "after" } ,
449+ { type : "done" } ,
450+ ] ) ;
451+ } ) ;
452+
453+ it ( "应处理 <think> 标签被 SSE chunk 拆开的情况" , async ( ) => {
454+ // 标签跨 chunk:chunk1 以 "<th" 结尾,chunk2 以 "ink>" 开头
455+ const reader = createMockReader ( [
456+ 'data: {"choices":[{"delta":{"content":"before<th"}}]}\n\n' ,
457+ 'data: {"choices":[{"delta":{"content":"ink>thought</think>after"}}]}\n\n' ,
458+ "data: [DONE]\n\n" ,
459+ ] ) ;
460+
461+ const events : ChatStreamEvent [ ] = [ ] ;
462+ const controller = new AbortController ( ) ;
463+
464+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
465+
466+ // 拼接所有 content_delta 与 thinking_delta 以验证内容未泄露标签片段
467+ const contentParts = events . filter ( ( e ) => e . type === "content_delta" ) . map ( ( e : any ) => e . delta ) ;
468+ const thinkingParts = events . filter ( ( e ) => e . type === "thinking_delta" ) . map ( ( e : any ) => e . delta ) ;
469+ expect ( contentParts . join ( "" ) ) . toBe ( "beforeafter" ) ;
470+ expect ( thinkingParts . join ( "" ) ) . toBe ( "thought" ) ;
471+ } ) ;
472+
473+ it ( "应处理 </think> 标签被 SSE chunk 拆开的情况" , async ( ) => {
474+ // 结束标签跨 chunk:chunk1 末尾是 "</thi",chunk2 开头是 "nk>"
475+ const reader = createMockReader ( [
476+ 'data: {"choices":[{"delta":{"content":"<think>thinking</thi"}}]}\n\n' ,
477+ 'data: {"choices":[{"delta":{"content":"nk>normal"}}]}\n\n' ,
478+ "data: [DONE]\n\n" ,
479+ ] ) ;
480+
481+ const events : ChatStreamEvent [ ] = [ ] ;
482+ const controller = new AbortController ( ) ;
483+
484+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
485+
486+ const contentParts = events . filter ( ( e ) => e . type === "content_delta" ) . map ( ( e : any ) => e . delta ) ;
487+ const thinkingParts = events . filter ( ( e ) => e . type === "thinking_delta" ) . map ( ( e : any ) => e . delta ) ;
488+ expect ( contentParts . join ( "" ) ) . toBe ( "normal" ) ;
489+ expect ( thinkingParts . join ( "" ) ) . toBe ( "thinking" ) ;
490+ } ) ;
491+
492+ it ( "应处理 <think> 标签逐字符跨 chunk 到达" , async ( ) => {
493+ // 每个字符独立到达,模拟 token 级别拆分
494+ const chunks = "before<think>reasoning</think>after"
495+ . split ( "" )
496+ . map ( ( ch ) => `data: {"choices":[{"delta":{"content":${ JSON . stringify ( ch ) } }}]}\n\n` ) ;
497+ chunks . push ( "data: [DONE]\n\n" ) ;
498+ const reader = createMockReader ( chunks ) ;
499+
500+ const events : ChatStreamEvent [ ] = [ ] ;
501+ const controller = new AbortController ( ) ;
502+
503+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
504+
505+ const contentParts = events . filter ( ( e ) => e . type === "content_delta" ) . map ( ( e : any ) => e . delta ) ;
506+ const thinkingParts = events . filter ( ( e ) => e . type === "thinking_delta" ) . map ( ( e : any ) => e . delta ) ;
507+ expect ( contentParts . join ( "" ) ) . toBe ( "beforeafter" ) ;
508+ expect ( thinkingParts . join ( "" ) ) . toBe ( "reasoning" ) ;
509+ } ) ;
510+
511+ it ( "流结束时仍停留在标签残片则原样作为 content 输出" , async ( ) => {
512+ // 看起来像 <think> 的残片,但后续再也没有到达 -> 按内容输出
513+ const reader = createMockReader ( [ 'data: {"choices":[{"delta":{"content":"hello <th"}}]}\n\n' , "data: [DONE]\n\n" ] ) ;
514+
515+ const events : ChatStreamEvent [ ] = [ ] ;
516+ const controller = new AbortController ( ) ;
517+
518+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
519+
520+ const contentParts = events . filter ( ( e ) => e . type === "content_delta" ) . map ( ( e : any ) => e . delta ) ;
521+ expect ( contentParts . join ( "" ) ) . toBe ( "hello <th" ) ;
522+ } ) ;
523+
434524 it ( "reasoning_content 后跟 tool_calls 应都正确解析" , async ( ) => {
435525 const reader = createMockReader ( [
436526 'data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":"分析页面"}}]}\n\n' ,
0 commit comments