@@ -1124,48 +1124,52 @@ async function processOpenAICompletionsStream(
11241124 // (`[{type:"thinking", thinking:"..."}, {type:"text", text:"..."}]`)
11251125 // instead of a string. JS string concatenation on the array (`"" + arr`)
11261126 // produced literal `"[object Object]"` tokens in the assembled text and a
1127- // matching corrupted `text_delta` event. Unpack typed blocks into text +
1128- // reasoning deltas, route reasoning blocks through the existing thinking
1129- // append path, and only emit a text delta if real text content arrives.
1130- // Plain string content keeps the original fast path.
1127+ // matching corrupted `text_delta` event. Walk the typed blocks in order
1128+ // and route each one through the existing text / thinking append paths
1129+ // so transcript block chronology and stream event order match the
1130+ // provider's original ordering. Plain string content keeps the original
1131+ // fast path. Unrecognized non-array shapes (including arrays whose blocks
1132+ // are all unsupported) fall through so reasoning_* and tool_calls in the
1133+ // same chunk are still processed.
11311134 const unpacked = unpackOpenAICompletionsContent ( choice . delta . content ) ;
1132- if ( unpacked . thinkingDelta . length > 0 ) {
1133- const reasoningDelta = {
1134- signature : "content_thinking" ,
1135- text : unpacked . thinkingDelta ,
1136- } ;
1137- if ( currentBlock ?. type === "toolCall" ) {
1138- if ( ! pendingThinkingDelta ) {
1139- pendingThinkingDelta = { ...reasoningDelta } ;
1135+ for ( const delta of unpacked . deltas ) {
1136+ if ( delta . kind === "thinking" ) {
1137+ const reasoningDelta = {
1138+ signature : "content_thinking" ,
1139+ text : delta . value ,
1140+ } ;
1141+ if ( currentBlock ?. type === "toolCall" ) {
1142+ if ( ! pendingThinkingDelta ) {
1143+ pendingThinkingDelta = { ...reasoningDelta } ;
1144+ } else {
1145+ pendingThinkingDelta . text += reasoningDelta . text ;
1146+ }
11401147 } else {
1141- pendingThinkingDelta . text += reasoningDelta . text ;
1148+ appendThinkingDelta ( reasoningDelta ) ;
11421149 }
1143- } else {
1144- appendThinkingDelta ( reasoningDelta ) ;
1150+ continue ;
11451151 }
1146- }
1147- if ( unpacked . textDelta . length > 0 ) {
11481152 flushPendingThinkingDelta ( ) ;
11491153 if ( ! currentBlock || currentBlock . type !== "text" ) {
11501154 finishCurrentBlock ( ) ;
11511155 currentBlock = { type : "text" , text : "" } ;
11521156 output . content . push ( currentBlock ) ;
11531157 stream . push ( { type : "text_start" , contentIndex : blockIndex ( ) , partial : output } ) ;
11541158 }
1155- currentBlock . text += unpacked . textDelta ;
1159+ currentBlock . text += delta . value ;
11561160 stream . push ( {
11571161 type : "text_delta" ,
11581162 contentIndex : blockIndex ( ) ,
1159- delta : unpacked . textDelta ,
1163+ delta : delta . value ,
11601164 partial : output ,
11611165 } ) ;
11621166 }
11631167 if ( unpacked . recognized ) {
11641168 continue ;
11651169 }
1166- // Unrecognized truthy non-string shape: fall through to the reasoning /
1167- // tool_calls branches below rather than coercing the value into the
1168- // assembled text.
1170+ // Unrecognized truthy non-string / no-supported-block shape: fall through
1171+ // to the reasoning / tool_calls branches below rather than coercing the
1172+ // value into the assembled text.
11691173 }
11701174 const reasoningDelta = getCompletionsReasoningDelta ( choice . delta as Record < string , unknown > ) ;
11711175 if ( reasoningDelta ) {
@@ -1233,53 +1237,73 @@ async function processOpenAICompletionsStream(
12331237// (e.g. `[{type:"thinking", thinking:"..."}, {type:"text", text:"..."}]`,
12341238// observed for Mistral with reasoning enabled where reasoning content
12351239// arrives inside `delta.content` instead of a top-level reasoning field).
1236- // `recognized` is true for both the string fast path and the typed-block array
1237- // shape. When false (e.g. a plain object or unexpected primitive), the caller
1238- // falls through to the reasoning/tool_calls branches instead of coercing the
1239- // value into assembled text.
1240+ // Deltas are returned in the original block order (not coalesced by type) so a
1241+ // `[{type:"text",…},{type:"thinking",…}]` array does not silently flip into
1242+ // thinking-then-text on the consumer side.
1243+ // `recognized` is true for the string fast path and for arrays that yielded at
1244+ // least one supported typed block. Empty arrays or arrays whose blocks are all
1245+ // unsupported shapes return `recognized: false` so reasoning_* and tool_calls
1246+ // fields in the same chunk are still processed by the loop below.
1247+ type OpenAICompletionsContentDelta =
1248+ | { kind : "text" ; value : string }
1249+ | { kind : "thinking" ; value : string } ;
1250+
12401251function unpackOpenAICompletionsContent ( rawContent : unknown ) : {
1241- textDelta : string ;
1242- thinkingDelta : string ;
1252+ deltas : OpenAICompletionsContentDelta [ ] ;
12431253 recognized : boolean ;
12441254} {
12451255 if ( typeof rawContent === "string" ) {
1246- return { textDelta : rawContent , thinkingDelta : "" , recognized : true } ;
1256+ return {
1257+ deltas : rawContent . length > 0 ? [ { kind : "text" , value : rawContent } ] : [ ] ,
1258+ recognized : true ,
1259+ } ;
12471260 }
12481261 if ( ! Array . isArray ( rawContent ) ) {
1249- return { textDelta : "" , thinkingDelta : "" , recognized : false } ;
1262+ return { deltas : [ ] , recognized : false } ;
12501263 }
1251- let textDelta = "" ;
1252- let thinkingDelta = "" ;
1264+ const deltas : OpenAICompletionsContentDelta [ ] = [ ] ;
1265+ let sawSupportedBlock = false ;
12531266 for ( const part of rawContent ) {
12541267 if ( ! part || typeof part !== "object" ) {
12551268 continue ;
12561269 }
12571270 const block = part as { type ?: unknown ; text ?: unknown ; thinking ?: unknown } ;
12581271 if ( block . type === "text" && typeof block . text === "string" ) {
1259- textDelta += block . text ;
1272+ sawSupportedBlock = true ;
1273+ if ( block . text . length > 0 ) {
1274+ deltas . push ( { kind : "text" , value : block . text } ) ;
1275+ }
12601276 continue ;
12611277 }
12621278 if ( block . type === "thinking" ) {
12631279 // Mistral reasoning blocks observed in two shapes: `.thinking` as a
12641280 // string, or `.thinking` as a nested array of `{type:"text", text}` parts.
12651281 if ( typeof block . thinking === "string" ) {
1266- thinkingDelta += block . thinking ;
1282+ sawSupportedBlock = true ;
1283+ if ( block . thinking . length > 0 ) {
1284+ deltas . push ( { kind : "thinking" , value : block . thinking } ) ;
1285+ }
12671286 continue ;
12681287 }
12691288 if ( Array . isArray ( block . thinking ) ) {
1289+ let thinkingValue = "" ;
12701290 for ( const sub of block . thinking ) {
12711291 if ( ! sub || typeof sub !== "object" ) {
12721292 continue ;
12731293 }
1274- const subText = ( sub as { text ?: unknown } ) . text ;
1275- if ( typeof subText === "string" ) {
1276- thinkingDelta += subText ;
1294+ const subBlock = sub as { type ?: unknown ; text ?: unknown } ;
1295+ if ( subBlock . type === "text" && typeof subBlock . text === "string" ) {
1296+ thinkingValue += subBlock . text ;
12771297 }
12781298 }
1299+ sawSupportedBlock = true ;
1300+ if ( thinkingValue . length > 0 ) {
1301+ deltas . push ( { kind : "thinking" , value : thinkingValue } ) ;
1302+ }
12791303 }
12801304 }
12811305 }
1282- return { textDelta , thinkingDelta , recognized : true } ;
1306+ return { deltas , recognized : sawSupportedBlock } ;
12831307}
12841308
12851309function getCompletionsReasoningDelta ( delta : Record < string , unknown > ) : {
0 commit comments