Skip to content

Commit ec551b1

Browse files
feat: add reasoning_details support to Roo provider (#9796)
- Add currentReasoningDetails accumulator to track reasoning details - Add getReasoningDetails() method to expose accumulated details - Handle reasoning_details array format in streaming responses - Accumulate reasoning details by type-index key - Support reasoning.text, reasoning.summary, and reasoning.encrypted types - Maintain backward compatibility with legacy reasoning format - Follows same pattern as OpenRouter provider Co-authored-by: Roo Code <[email protected]>
1 parent ce22901 commit ec551b1

File tree

1 file changed

+95
-6
lines changed

1 file changed

+95
-6
lines changed

src/api/providers/roo.ts

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function getSessionToken(): string {
3838

3939
export 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

Comments
 (0)