@@ -38,6 +38,7 @@ function getSessionToken(): string {
3838
3939export class RooHandler extends BaseOpenAiCompatibleProvider < string > {
4040 private fetcherBaseURL : string
41+ private currentReasoningDetails : any [ ] = [ ]
4142
4243 constructor ( options : ApiHandlerOptions ) {
4344 const sessionToken = getSessionToken ( )
@@ -116,12 +117,19 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
116117 }
117118 }
118119
120+ getReasoningDetails ( ) : any [ ] | undefined {
121+ return this . currentReasoningDetails . length > 0 ? this . currentReasoningDetails : undefined
122+ }
123+
119124 override async * createMessage (
120125 systemPrompt : string ,
121126 messages : Anthropic . Messages . MessageParam [ ] ,
122127 metadata ?: ApiHandlerCreateMessageMetadata ,
123128 ) : ApiStream {
124129 try {
130+ // Reset reasoning_details accumulator for this request
131+ this . currentReasoningDetails = [ ]
132+
125133 const headers : Record < string , string > = {
126134 "X-Roo-App-Version" : Package . version ,
127135 }
@@ -133,21 +141,97 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
133141 const stream = await this . createStream ( systemPrompt , messages , metadata , { headers } )
134142
135143 let lastUsage : RooUsage | undefined = undefined
144+ // Accumulator for reasoning_details: accumulate text by type-index key
145+ const reasoningDetailsAccumulator = new Map <
146+ string ,
147+ {
148+ type : string
149+ text ?: string
150+ summary ?: string
151+ data ?: string
152+ id ?: string | null
153+ format ?: string
154+ signature ?: string
155+ index : number
156+ }
157+ > ( )
136158
137159 for await ( const chunk of stream ) {
138160 const delta = chunk . choices [ 0 ] ?. delta
139161
140162 if ( delta ) {
141- // Check for reasoning content (similar to OpenRouter)
142- if ( "reasoning" in delta && delta . reasoning && typeof delta . reasoning === "string" ) {
163+ // Handle reasoning_details array format (used by Gemini 3, Claude, OpenAI o-series, etc.)
164+ // See: https://openrouter.ai/docs/use-cases/reasoning-tokens#preserving-reasoning-blocks
165+ // Priority: Check for reasoning_details first, as it's the newer format
166+ const deltaWithReasoning = delta as typeof delta & {
167+ reasoning_details ?: Array < {
168+ type : string
169+ text ?: string
170+ summary ?: string
171+ data ?: string
172+ id ?: string | null
173+ format ?: string
174+ signature ?: string
175+ index ?: number
176+ } >
177+ }
178+
179+ if ( deltaWithReasoning . reasoning_details && Array . isArray ( deltaWithReasoning . reasoning_details ) ) {
180+ for ( const detail of deltaWithReasoning . reasoning_details ) {
181+ const index = detail . index ?? 0
182+ const key = `${ detail . type } -${ index } `
183+ const existing = reasoningDetailsAccumulator . get ( key )
184+
185+ if ( existing ) {
186+ // Accumulate text/summary/data for existing reasoning detail
187+ if ( detail . text !== undefined ) {
188+ existing . text = ( existing . text || "" ) + detail . text
189+ }
190+ if ( detail . summary !== undefined ) {
191+ existing . summary = ( existing . summary || "" ) + detail . summary
192+ }
193+ if ( detail . data !== undefined ) {
194+ existing . data = ( existing . data || "" ) + detail . data
195+ }
196+ // Update other fields if provided
197+ if ( detail . id !== undefined ) existing . id = detail . id
198+ if ( detail . format !== undefined ) existing . format = detail . format
199+ if ( detail . signature !== undefined ) existing . signature = detail . signature
200+ } else {
201+ // Start new reasoning detail accumulation
202+ reasoningDetailsAccumulator . set ( key , {
203+ type : detail . type ,
204+ text : detail . text ,
205+ summary : detail . summary ,
206+ data : detail . data ,
207+ id : detail . id ,
208+ format : detail . format ,
209+ signature : detail . signature ,
210+ index,
211+ } )
212+ }
213+
214+ // Yield text for display (still fragmented for live streaming)
215+ let reasoningText : string | undefined
216+ if ( detail . type === "reasoning.text" && typeof detail . text === "string" ) {
217+ reasoningText = detail . text
218+ } else if ( detail . type === "reasoning.summary" && typeof detail . summary === "string" ) {
219+ reasoningText = detail . summary
220+ }
221+ // Note: reasoning.encrypted types are intentionally skipped as they contain redacted content
222+
223+ if ( reasoningText ) {
224+ yield { type : "reasoning" , text : reasoningText }
225+ }
226+ }
227+ } else if ( "reasoning" in delta && delta . reasoning && typeof delta . reasoning === "string" ) {
228+ // Handle legacy reasoning format - only if reasoning_details is not present
143229 yield {
144230 type : "reasoning" ,
145231 text : delta . reasoning ,
146232 }
147- }
148-
149- // Also check for reasoning_content for backward compatibility
150- if ( "reasoning_content" in delta && typeof delta . reasoning_content === "string" ) {
233+ } else if ( "reasoning_content" in delta && typeof delta . reasoning_content === "string" ) {
234+ // Also check for reasoning_content for backward compatibility
151235 yield {
152236 type : "reasoning" ,
153237 text : delta . reasoning_content ,
@@ -180,6 +264,11 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
180264 }
181265 }
182266
267+ // After streaming completes, store the accumulated reasoning_details
268+ if ( reasoningDetailsAccumulator . size > 0 ) {
269+ this . currentReasoningDetails = Array . from ( reasoningDetailsAccumulator . values ( ) )
270+ }
271+
183272 if ( lastUsage ) {
184273 // Check if the current model is marked as free
185274 const model = this . getModel ( )
0 commit comments