fix: Handle malformed reasoning blocks from x-ai/grok models

- Add sanitization for x-ai model reasoning blocks to prevent interference with tool parsing
- Remove XML-like tags and diff markers from reasoning content
- Add validation to skip corrupted tool calls from x-ai models
- Clean reasoning artifacts from diff content before processing
- Handle interleaved thinking blocks in content stream

This fixes issue #9239 where x-ai/grok-code-fast-1 model was causing 'Edit Unsuccessful' errors due to malformed reasoning blocks interfering with the diff parsing logic.
This commit is contained in:
Roo Code 2025-11-13 19:58:37 +00:00
parent 5e6e601b0a
commit 6f99b2edde
3 changed files with 158 additions and 12 deletions

View file

@ -128,6 +128,12 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
let lastUsage: RooUsage | undefined = undefined
// Accumulate tool calls by index - similar to how reasoning accumulates
const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
// 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<string> {
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<string> {
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("<think>") ||
args.includes("</think>") ||
args.includes("<reasoning>") ||
args.includes("</reasoning>")
) {
// 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("<think>") || textContent.includes("</think>")) {
// Extract and handle the reasoning part separately
const thinkMatch = textContent.match(/<think>(.*?)<\/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>.*?<\/think>/s, "")
}
}
}
// Only yield text if there's content after cleaning
if (textContent.trim()) {
yield {
type: "text",
text: textContent,
}
}
}
}

View file

@ -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("<think>") ||
diffContent.includes("</think>") ||
diffContent.includes("<reasoning>") ||
diffContent.includes("</reasoning>")
) {
console.warn("[ApplyDiffTool] Cleaning reasoning artifacts from x-ai model diff content", {
modelId,
hasThinkTags: diffContent.includes("<think>"),
hasReasoningTags: diffContent.includes("<reasoning>"),
})
// Remove thinking/reasoning blocks but preserve the actual diff markers
diffContent = diffContent
.replace(/<think>.*?<\/think>/gs, "")
.replace(/<reasoning>.*?<\/reasoning>/gs, "")
// Also clean up orphaned closing tags that might interfere
.replace(/<\/think>/g, "")
.replace(/<\/reasoning>/g, "")
}
}
try {
if (!relPath) {
task.consecutiveMistakeCount++

View file

@ -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>.*?<\/think>/gs, "")
.replace(/<reasoning>.*?<\/reasoning>/gs, "")
// Check for corrupted XML structure due to reasoning blocks
if (cleanedXml.includes("</think>") && !cleanedXml.includes("<think>")) {
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 &amp; 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("<think>") || diffContent.includes("</think>")) {
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>.*?<\/think>/gs, "")
}
}
// Only add to operations if we have valid content
if (diffContent) {
operationsMap[filePath].diff.push({