diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts index 77769aab26..fee01d5f20 100644 --- a/src/api/providers/roo.ts +++ b/src/api/providers/roo.ts @@ -128,6 +128,12 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { let lastUsage: RooUsage | undefined = undefined // Accumulate tool calls by index - similar to how reasoning accumulates const toolCallAccumulator = new Map() + // Track if we're currently processing reasoning to prevent interference with tool parsing + let isProcessingReasoning = false + + // Check if this is an x-ai model that might have malformed reasoning blocks + const modelId = this.options.apiModelId || "" + const isXAIModel = modelId.includes("x-ai/") || modelId.includes("grok") for await (const chunk of stream) { const delta = chunk.choices[0]?.delta @@ -136,22 +142,49 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { if (delta) { // Check for reasoning content (similar to OpenRouter) if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") { + // For x-ai models, sanitize reasoning text to prevent XML-like content from interfering + let reasoningText = delta.reasoning + if (isXAIModel) { + // Remove any XML-like tags that might interfere with tool parsing + reasoningText = reasoningText + .replace(/<\/?apply_diff[^>]*>/g, "") + .replace(/<\/?SEARCH[^>]*>/g, "") + .replace(/<\/?REPLACE[^>]*>/g, "") + .replace(/<<<<<<< SEARCH/g, "[SEARCH]") + .replace(/=======/g, "[SEPARATOR]") + .replace(/>>>>>>> REPLACE/g, "[REPLACE]") + } + isProcessingReasoning = true yield { type: "reasoning", - text: delta.reasoning, + text: reasoningText, } + isProcessingReasoning = false } // Also check for reasoning_content for backward compatibility if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") { + // Apply same sanitization for x-ai models + let reasoningText = delta.reasoning_content + if (isXAIModel) { + reasoningText = reasoningText + .replace(/<\/?apply_diff[^>]*>/g, "") + .replace(/<\/?SEARCH[^>]*>/g, "") + .replace(/<\/?REPLACE[^>]*>/g, "") + .replace(/<<<<<<< SEARCH/g, "[SEARCH]") + .replace(/=======/g, "[SEPARATOR]") + .replace(/>>>>>>> REPLACE/g, "[REPLACE]") + } + isProcessingReasoning = true yield { type: "reasoning", - text: delta.reasoning_content, + text: reasoningText, } + isProcessingReasoning = false } - // Check for tool calls in delta - if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) { + // Check for tool calls in delta - but skip if we're processing reasoning to avoid interference + if (!isProcessingReasoning && "tool_calls" in delta && Array.isArray(delta.tool_calls)) { for (const toolCall of delta.tool_calls) { const index = toolCall.index const existing = toolCallAccumulator.get(index) @@ -159,23 +192,78 @@ export class RooHandler extends BaseOpenAiCompatibleProvider { if (existing) { // Accumulate arguments for existing tool call if (toolCall.function?.arguments) { - existing.arguments += toolCall.function.arguments + // For x-ai models, validate the arguments don't contain reasoning artifacts + let args = toolCall.function.arguments + if (isXAIModel && args) { + // Check if the arguments contain reasoning block artifacts + if ( + args.includes("") || + args.includes("") || + args.includes("") || + args.includes("") + ) { + // Skip this chunk as it's likely corrupted reasoning content + console.warn( + "[RooHandler] Skipping corrupted tool call arguments from x-ai model", + { + modelId, + corruptedContent: args.substring(0, 100), + }, + ) + continue + } + } + existing.arguments += args } } else { // Start new tool call accumulation + const toolName = toolCall.function?.name || "" + const toolArgs = toolCall.function?.arguments || "" + + // Validate tool name isn't corrupted by reasoning content + if (isXAIModel && (toolName.includes("think") || toolName.includes("reasoning"))) { + console.warn("[RooHandler] Skipping corrupted tool call from x-ai model", { + modelId, + corruptedName: toolName, + }) + continue + } + toolCallAccumulator.set(index, { id: toolCall.id || "", - name: toolCall.function?.name || "", - arguments: toolCall.function?.arguments || "", + name: toolName, + arguments: toolArgs, }) } } } if (delta.content) { - yield { - type: "text", - text: delta.content, + // For x-ai models, check if content contains interleaved reasoning markers + let textContent = delta.content + if (isXAIModel) { + // Check for common reasoning block markers that shouldn't be in regular content + if (textContent.includes("") || textContent.includes("")) { + // Extract and handle the reasoning part separately + const thinkMatch = textContent.match(/(.*?)<\/think>/s) + if (thinkMatch) { + // Emit the reasoning part + yield { + type: "reasoning", + text: thinkMatch[1], + } + // Remove the thinking block from the text + textContent = textContent.replace(/.*?<\/think>/s, "") + } + } + } + + // Only yield text if there's content after cleaning + if (textContent.trim()) { + yield { + type: "text", + text: textContent, + } } } } diff --git a/src/core/tools/ApplyDiffTool.ts b/src/core/tools/ApplyDiffTool.ts index ef6623ed93..1ce7b9869c 100644 --- a/src/core/tools/ApplyDiffTool.ts +++ b/src/core/tools/ApplyDiffTool.ts @@ -35,10 +35,38 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { const { askApproval, handleError, pushToolResult } = callbacks let { path: relPath, diff: diffContent } = params - if (diffContent && !task.api.getModel().id.includes("claude")) { + // Check if this is an x-ai model that might have reasoning artifacts + const modelId = task.api.getModel().id + const isXAIModel = modelId.includes("x-ai/") || modelId.includes("grok") + + if (diffContent && !modelId.includes("claude")) { diffContent = unescapeHtmlEntities(diffContent) } + // Clean up reasoning artifacts from x-ai models + if (isXAIModel && diffContent) { + // Check for reasoning block markers that shouldn't be in diff content + if ( + diffContent.includes("") || + diffContent.includes("") || + diffContent.includes("") || + diffContent.includes("") + ) { + console.warn("[ApplyDiffTool] Cleaning reasoning artifacts from x-ai model diff content", { + modelId, + hasThinkTags: diffContent.includes(""), + hasReasoningTags: diffContent.includes(""), + }) + // Remove thinking/reasoning blocks but preserve the actual diff markers + diffContent = diffContent + .replace(/.*?<\/think>/gs, "") + .replace(/.*?<\/reasoning>/gs, "") + // Also clean up orphaned closing tags that might interfere + .replace(/<\/think>/g, "") + .replace(/<\/reasoning>/g, "") + } + } + try { if (!relPath) { task.consecutiveMistakeCount++ diff --git a/src/core/tools/MultiApplyDiffTool.ts b/src/core/tools/MultiApplyDiffTool.ts index 4a41db7697..128c5c1172 100644 --- a/src/core/tools/MultiApplyDiffTool.ts +++ b/src/core/tools/MultiApplyDiffTool.ts @@ -127,10 +127,28 @@ export async function applyDiffTool( if (argsXmlTag) { // Parse file entries from XML (new way) try { + // Check if this might be from an x-ai model and contains reasoning artifacts + const isXAIModel = cline.api.getModel().id.includes("x-ai/") || cline.api.getModel().id.includes("grok") + let cleanedXml = argsXmlTag + + if (isXAIModel) { + // Clean up common reasoning artifacts that might have leaked into the XML + // Remove thinking tags that might interfere with parsing + cleanedXml = cleanedXml + .replace(/.*?<\/think>/gs, "") + .replace(/.*?<\/reasoning>/gs, "") + + // Check for corrupted XML structure due to reasoning blocks + if (cleanedXml.includes("") && !cleanedXml.includes("")) { + console.warn("[MultiApplyDiffTool] Detected orphaned reasoning closing tag in x-ai model response") + cleanedXml = cleanedXml.replace(/<\/think>/g, "") + } + } + // IMPORTANT: We use parseXmlForDiff here instead of parseXml to prevent HTML entity decoding // This ensures exact character matching when comparing parsed content against original file content // Without this, special characters like & would be decoded to & causing diff mismatches - const parsed = parseXmlForDiff(argsXmlTag, ["file.diff.content"]) as ParsedXmlResult + const parsed = parseXmlForDiff(cleanedXml, ["file.diff.content"]) as ParsedXmlResult const files = Array.isArray(parsed.file) ? parsed.file : [parsed.file].filter(Boolean) for (const file of files) { @@ -158,6 +176,18 @@ export async function applyDiffTool( diffContent = typeof diff.content === "string" ? diff.content : "" startLine = diff.start_line ? parseInt(diff.start_line) : undefined + // For x-ai models, validate diff content doesn't contain reasoning artifacts + if (isXAIModel && diffContent) { + // Check for reasoning block markers that shouldn't be in diff content + if (diffContent.includes("") || diffContent.includes("")) { + console.warn( + "[MultiApplyDiffTool] Cleaning reasoning artifacts from x-ai model diff content", + ) + // Remove thinking blocks but preserve the actual diff markers + diffContent = diffContent.replace(/.*?<\/think>/gs, "") + } + } + // Only add to operations if we have valid content if (diffContent) { operationsMap[filePath].diff.push({