@@ -146,7 +146,8 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
146146 const stream = await this . createStream ( systemPrompt , messages , metadata , { headers } )
147147
148148 let lastUsage : RooUsage | undefined = undefined
149- // Accumulator for reasoning_details: accumulate text by type-index key
149+ // Accumulator for reasoning_details FROM the API.
150+ // We preserve the original shape of reasoning_details to prevent malformed responses.
150151 const reasoningDetailsAccumulator = new Map <
151152 string ,
152153 {
@@ -161,6 +162,11 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
161162 }
162163 > ( )
163164
165+ // Track whether we've yielded displayable text from reasoning_details.
166+ // When reasoning_details has displayable content (reasoning.text or reasoning.summary),
167+ // we skip yielding the top-level reasoning field to avoid duplicate display.
168+ let hasYieldedReasoningFromDetails = false
169+
164170 for await ( const chunk of stream ) {
165171 const delta = chunk . choices [ 0 ] ?. delta
166172 const finishReason = chunk . choices [ 0 ] ?. finish_reason
@@ -223,29 +229,32 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
223229 }
224230
225231 // Yield text for display (still fragmented for live streaming)
232+ // Only reasoning.text and reasoning.summary have displayable content
233+ // reasoning.encrypted is intentionally skipped as it contains redacted content
226234 let reasoningText : string | undefined
227235 if ( detail . type === "reasoning.text" && typeof detail . text === "string" ) {
228236 reasoningText = detail . text
229237 } else if ( detail . type === "reasoning.summary" && typeof detail . summary === "string" ) {
230238 reasoningText = detail . summary
231239 }
232- // Note: reasoning.encrypted types are intentionally skipped as they contain redacted content
233240
234241 if ( reasoningText ) {
242+ hasYieldedReasoningFromDetails = true
235243 yield { type : "reasoning" , text : reasoningText }
236244 }
237245 }
238- } else if ( "reasoning" in delta && delta . reasoning && typeof delta . reasoning === "string" ) {
239- // Handle legacy reasoning format - only if reasoning_details is not present
240- yield {
241- type : "reasoning" ,
242- text : delta . reasoning ,
246+ }
247+
248+ // Handle top-level reasoning field for UI display.
249+ // Skip if we've already yielded from reasoning_details to avoid duplicate display.
250+ if ( "reasoning" in delta && delta . reasoning && typeof delta . reasoning === "string" ) {
251+ if ( ! hasYieldedReasoningFromDetails ) {
252+ yield { type : "reasoning" , text : delta . reasoning }
243253 }
244254 } else if ( "reasoning_content" in delta && typeof delta . reasoning_content === "string" ) {
245255 // Also check for reasoning_content for backward compatibility
246- yield {
247- type : "reasoning" ,
248- text : delta . reasoning_content ,
256+ if ( ! hasYieldedReasoningFromDetails ) {
257+ yield { type : "reasoning" , text : delta . reasoning_content }
249258 }
250259 }
251260
@@ -282,7 +291,7 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
282291 }
283292 }
284293
285- // After streaming completes, store the accumulated reasoning_details
294+ // After streaming completes, store ONLY the reasoning_details we received from the API.
286295 if ( reasoningDetailsAccumulator . size > 0 ) {
287296 this . currentReasoningDetails = Array . from ( reasoningDetailsAccumulator . values ( ) )
288297 }
0 commit comments