From e05aec7d9ce917f77573c19463012f6128c688cc Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 22 Dec 2025 18:56:20 -0700 Subject: [PATCH] fix: improve reasoning_details accumulation and serialization - Use id as key for merging reasoning detail chunks when available - Ensures reasoning.summary and reasoning.encrypted chunks with same id merge correctly - Preserve original type when encrypted data chunks arrive - Place reasoning_details before tool_calls in output to match provider-expected order - Strip internal id field from reasoning_details in serialized output --- src/api/providers/roo.ts | 7 ++++++- src/api/transform/openai-format.ts | 22 +++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts index 83eab87ef7e..b4a626832b4 100644 --- a/src/api/providers/roo.ts +++ b/src/api/providers/roo.ts @@ -179,7 +179,10 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { if (deltaWithReasoning.reasoning_details && Array.isArray(deltaWithReasoning.reasoning_details)) { for (const detail of deltaWithReasoning.reasoning_details) { const index = detail.index ?? 0 - const key = `${detail.type}-${index}` + // Use id as key when available to merge chunks that share the same reasoning block id + // This ensures that reasoning.summary and reasoning.encrypted chunks with the same id + // are merged into a single object, matching the provider's expected format + const key = detail.id ?? `${detail.type}-${index}` const existing = reasoningDetailsAccumulator.get(key) if (existing) { @@ -194,6 +197,8 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { existing.data = (existing.data || "") + detail.data } // Update other fields if provided + // Note: Don't update type - keep original type (e.g., reasoning.summary) + // even when encrypted data chunks arrive with type reasoning.encrypted if (detail.id !== undefined) existing.id = detail.id if (detail.format !== undefined) existing.format = detail.format if (detail.signature !== undefined) existing.signature = detail.signature diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index 56d6441c48b..7ca4ddb993c 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -150,16 +150,28 @@ export function convertToOpenAiMessages( // Check if the message has reasoning_details (used by Gemini 3, etc.) const messageWithDetails = anthropicMessage as any - const baseMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = { + + // Build message with reasoning_details BEFORE tool_calls to preserve + // the order expected by providers like Roo. Property order matters + // when sending messages back to some APIs. + const baseMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam & { reasoning_details?: any[] } = { role: "assistant", content, - // Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty - tool_calls: tool_calls.length > 0 ? tool_calls : undefined, } - // Preserve reasoning_details if present (will be processed by provider if needed) + // Add reasoning_details first (before tool_calls) to preserve provider-expected order + // Strip the id field from each reasoning detail as it's only used internally for accumulation if (messageWithDetails.reasoning_details && Array.isArray(messageWithDetails.reasoning_details)) { - ;(baseMessage as any).reasoning_details = messageWithDetails.reasoning_details + baseMessage.reasoning_details = messageWithDetails.reasoning_details.map((detail: any) => { + const { id, ...rest } = detail + return rest + }) + } + + // Add tool_calls after reasoning_details + // Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty + if (tool_calls.length > 0) { + baseMessage.tool_calls = tool_calls } openAiMessages.push(baseMessage)