@@ -146,7 +146,9 @@ 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+ // IMPORTANT: We do not synthesize/add any reasoning blocks from the top-level `reasoning` field.
151+ // The top-level `reasoning` is treated as display-only.
150152 const reasoningDetailsAccumulator = new Map <
151153 string ,
152154 {
@@ -161,6 +163,15 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
161163 }
162164 > ( )
163165
166+ // Track whether we've seen reasoning_details in this stream
167+ // When reasoning_details are present, the top-level reasoning field is redundant
168+ // and should be ignored for storage (but may still be used for display)
169+ let hasReasoningDetails = false
170+ // Track whether we've yielded any displayable text from reasoning_details
171+ // This prevents double-display when both reasoning_details (with summary) and
172+ // top-level reasoning field contain the same text
173+ let hasYieldedReasoningFromDetails = false
174+
164175 for await ( const chunk of stream ) {
165176 const delta = chunk . choices [ 0 ] ?. delta
166177 const finishReason = chunk . choices [ 0 ] ?. finish_reason
@@ -183,6 +194,7 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
183194 }
184195
185196 if ( deltaWithReasoning . reasoning_details && Array . isArray ( deltaWithReasoning . reasoning_details ) ) {
197+ hasReasoningDetails = true
186198 for ( const detail of deltaWithReasoning . reasoning_details ) {
187199 const index = detail . index ?? 0
188200 // Use id as key when available to merge chunks that share the same reasoning block id
@@ -223,29 +235,47 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
223235 }
224236
225237 // Yield text for display (still fragmented for live streaming)
238+ // Only reasoning.text and reasoning.summary have displayable content
239+ // reasoning.encrypted is intentionally skipped as it contains redacted content
226240 let reasoningText : string | undefined
227241 if ( detail . type === "reasoning.text" && typeof detail . text === "string" ) {
228242 reasoningText = detail . text
229243 } else if ( detail . type === "reasoning.summary" && typeof detail . summary === "string" ) {
230244 reasoningText = detail . summary
231245 }
232- // Note: reasoning.encrypted types are intentionally skipped as they contain redacted content
233246
234247 if ( reasoningText ) {
248+ hasYieldedReasoningFromDetails = true
235249 yield { type : "reasoning" , text : reasoningText }
236250 }
237251 }
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 ,
252+ }
253+
254+ // Handle top-level reasoning field for UI display
255+ // IMPORTANT: The top-level `reasoning` field is ONLY for display purposes
256+ // when reasoning_details exists. It should NOT be stored or sent back - only
257+ // the original `reasoning_details` array received from the API should be stored.
258+ //
259+ // Display rules:
260+ // - If we've already yielded text from reasoning_details, DON'T yield top-level reasoning
261+ // (they contain the same text, would cause double display)
262+ // - If reasoning_details only has encrypted content (no displayable text), then yield
263+ // top-level reasoning for display
264+ // - If no reasoning_details exist, yield for display
265+ //
266+ // Storage rules:
267+ // - Never store top-level `reasoning` / `reasoning_content`. Only store what the API returns
268+ // in `reasoning_details`.
269+ if ( "reasoning" in delta && delta . reasoning && typeof delta . reasoning === "string" ) {
270+ // Only yield for display if we haven't already yielded from reasoning_details
271+ if ( ! hasYieldedReasoningFromDetails ) {
272+ yield { type : "reasoning" , text : delta . reasoning }
243273 }
244274 } else if ( "reasoning_content" in delta && typeof delta . reasoning_content === "string" ) {
245275 // Also check for reasoning_content for backward compatibility
246- yield {
247- type : "reasoning" ,
248- text : delta . reasoning_content ,
276+ // Same rules apply: yield for display only if not already yielded from details
277+ if ( ! hasYieldedReasoningFromDetails ) {
278+ yield { type : "reasoning" , text : delta . reasoning_content }
249279 }
250280 }
251281
@@ -282,7 +312,7 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
282312 }
283313 }
284314
285- // After streaming completes, store the accumulated reasoning_details
315+ // After streaming completes, store ONLY the reasoning_details we received from the API.
286316 if ( reasoningDetailsAccumulator . size > 0 ) {
287317 this . currentReasoningDetails = Array . from ( reasoningDetailsAccumulator . values ( ) )
288318 }
0 commit comments