Skip to content

Commit 6f99b2e

Browse files
committed
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.
1 parent 5e6e601 commit 6f99b2e

File tree

3 files changed

+158
-12
lines changed

3 files changed

+158
-12
lines changed

src/api/providers/roo.ts

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
128128
let lastUsage: RooUsage | undefined = undefined
129129
// Accumulate tool calls by index - similar to how reasoning accumulates
130130
const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
131+
// Track if we're currently processing reasoning to prevent interference with tool parsing
132+
let isProcessingReasoning = false
133+
134+
// Check if this is an x-ai model that might have malformed reasoning blocks
135+
const modelId = this.options.apiModelId || ""
136+
const isXAIModel = modelId.includes("x-ai/") || modelId.includes("grok")
131137

132138
for await (const chunk of stream) {
133139
const delta = chunk.choices[0]?.delta
@@ -136,46 +142,128 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
136142
if (delta) {
137143
// Check for reasoning content (similar to OpenRouter)
138144
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
145+
// For x-ai models, sanitize reasoning text to prevent XML-like content from interfering
146+
let reasoningText = delta.reasoning
147+
if (isXAIModel) {
148+
// Remove any XML-like tags that might interfere with tool parsing
149+
reasoningText = reasoningText
150+
.replace(/<\/?apply_diff[^>]*>/g, "")
151+
.replace(/<\/?SEARCH[^>]*>/g, "")
152+
.replace(/<\/?REPLACE[^>]*>/g, "")
153+
.replace(/<<<<<<< SEARCH/g, "[SEARCH]")
154+
.replace(/=======/g, "[SEPARATOR]")
155+
.replace(/>>>>>>> REPLACE/g, "[REPLACE]")
156+
}
157+
isProcessingReasoning = true
139158
yield {
140159
type: "reasoning",
141-
text: delta.reasoning,
160+
text: reasoningText,
142161
}
162+
isProcessingReasoning = false
143163
}
144164

145165
// Also check for reasoning_content for backward compatibility
146166
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
167+
// Apply same sanitization for x-ai models
168+
let reasoningText = delta.reasoning_content
169+
if (isXAIModel) {
170+
reasoningText = reasoningText
171+
.replace(/<\/?apply_diff[^>]*>/g, "")
172+
.replace(/<\/?SEARCH[^>]*>/g, "")
173+
.replace(/<\/?REPLACE[^>]*>/g, "")
174+
.replace(/<<<<<<< SEARCH/g, "[SEARCH]")
175+
.replace(/=======/g, "[SEPARATOR]")
176+
.replace(/>>>>>>> REPLACE/g, "[REPLACE]")
177+
}
178+
isProcessingReasoning = true
147179
yield {
148180
type: "reasoning",
149-
text: delta.reasoning_content,
181+
text: reasoningText,
150182
}
183+
isProcessingReasoning = false
151184
}
152185

153-
// Check for tool calls in delta
154-
if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) {
186+
// Check for tool calls in delta - but skip if we're processing reasoning to avoid interference
187+
if (!isProcessingReasoning && "tool_calls" in delta && Array.isArray(delta.tool_calls)) {
155188
for (const toolCall of delta.tool_calls) {
156189
const index = toolCall.index
157190
const existing = toolCallAccumulator.get(index)
158191

159192
if (existing) {
160193
// Accumulate arguments for existing tool call
161194
if (toolCall.function?.arguments) {
162-
existing.arguments += toolCall.function.arguments
195+
// For x-ai models, validate the arguments don't contain reasoning artifacts
196+
let args = toolCall.function.arguments
197+
if (isXAIModel && args) {
198+
// Check if the arguments contain reasoning block artifacts
199+
if (
200+
args.includes("<think>") ||
201+
args.includes("</think>") ||
202+
args.includes("<reasoning>") ||
203+
args.includes("</reasoning>")
204+
) {
205+
// Skip this chunk as it's likely corrupted reasoning content
206+
console.warn(
207+
"[RooHandler] Skipping corrupted tool call arguments from x-ai model",
208+
{
209+
modelId,
210+
corruptedContent: args.substring(0, 100),
211+
},
212+
)
213+
continue
214+
}
215+
}
216+
existing.arguments += args
163217
}
164218
} else {
165219
// Start new tool call accumulation
220+
const toolName = toolCall.function?.name || ""
221+
const toolArgs = toolCall.function?.arguments || ""
222+
223+
// Validate tool name isn't corrupted by reasoning content
224+
if (isXAIModel && (toolName.includes("think") || toolName.includes("reasoning"))) {
225+
console.warn("[RooHandler] Skipping corrupted tool call from x-ai model", {
226+
modelId,
227+
corruptedName: toolName,
228+
})
229+
continue
230+
}
231+
166232
toolCallAccumulator.set(index, {
167233
id: toolCall.id || "",
168-
name: toolCall.function?.name || "",
169-
arguments: toolCall.function?.arguments || "",
234+
name: toolName,
235+
arguments: toolArgs,
170236
})
171237
}
172238
}
173239
}
174240

175241
if (delta.content) {
176-
yield {
177-
type: "text",
178-
text: delta.content,
242+
// For x-ai models, check if content contains interleaved reasoning markers
243+
let textContent = delta.content
244+
if (isXAIModel) {
245+
// Check for common reasoning block markers that shouldn't be in regular content
246+
if (textContent.includes("<think>") || textContent.includes("</think>")) {
247+
// Extract and handle the reasoning part separately
248+
const thinkMatch = textContent.match(/<think>(.*?)<\/think>/s)
249+
if (thinkMatch) {
250+
// Emit the reasoning part
251+
yield {
252+
type: "reasoning",
253+
text: thinkMatch[1],
254+
}
255+
// Remove the thinking block from the text
256+
textContent = textContent.replace(/<think>.*?<\/think>/s, "")
257+
}
258+
}
259+
}
260+
261+
// Only yield text if there's content after cleaning
262+
if (textContent.trim()) {
263+
yield {
264+
type: "text",
265+
text: textContent,
266+
}
179267
}
180268
}
181269
}

src/core/tools/ApplyDiffTool.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,38 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
3535
const { askApproval, handleError, pushToolResult } = callbacks
3636
let { path: relPath, diff: diffContent } = params
3737

38-
if (diffContent && !task.api.getModel().id.includes("claude")) {
38+
// Check if this is an x-ai model that might have reasoning artifacts
39+
const modelId = task.api.getModel().id
40+
const isXAIModel = modelId.includes("x-ai/") || modelId.includes("grok")
41+
42+
if (diffContent && !modelId.includes("claude")) {
3943
diffContent = unescapeHtmlEntities(diffContent)
4044
}
4145

46+
// Clean up reasoning artifacts from x-ai models
47+
if (isXAIModel && diffContent) {
48+
// Check for reasoning block markers that shouldn't be in diff content
49+
if (
50+
diffContent.includes("<think>") ||
51+
diffContent.includes("</think>") ||
52+
diffContent.includes("<reasoning>") ||
53+
diffContent.includes("</reasoning>")
54+
) {
55+
console.warn("[ApplyDiffTool] Cleaning reasoning artifacts from x-ai model diff content", {
56+
modelId,
57+
hasThinkTags: diffContent.includes("<think>"),
58+
hasReasoningTags: diffContent.includes("<reasoning>"),
59+
})
60+
// Remove thinking/reasoning blocks but preserve the actual diff markers
61+
diffContent = diffContent
62+
.replace(/<think>.*?<\/think>/gs, "")
63+
.replace(/<reasoning>.*?<\/reasoning>/gs, "")
64+
// Also clean up orphaned closing tags that might interfere
65+
.replace(/<\/think>/g, "")
66+
.replace(/<\/reasoning>/g, "")
67+
}
68+
}
69+
4270
try {
4371
if (!relPath) {
4472
task.consecutiveMistakeCount++

src/core/tools/MultiApplyDiffTool.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,28 @@ export async function applyDiffTool(
127127
if (argsXmlTag) {
128128
// Parse file entries from XML (new way)
129129
try {
130+
// Check if this might be from an x-ai model and contains reasoning artifacts
131+
const isXAIModel = cline.api.getModel().id.includes("x-ai/") || cline.api.getModel().id.includes("grok")
132+
let cleanedXml = argsXmlTag
133+
134+
if (isXAIModel) {
135+
// Clean up common reasoning artifacts that might have leaked into the XML
136+
// Remove thinking tags that might interfere with parsing
137+
cleanedXml = cleanedXml
138+
.replace(/<think>.*?<\/think>/gs, "")
139+
.replace(/<reasoning>.*?<\/reasoning>/gs, "")
140+
141+
// Check for corrupted XML structure due to reasoning blocks
142+
if (cleanedXml.includes("</think>") && !cleanedXml.includes("<think>")) {
143+
console.warn("[MultiApplyDiffTool] Detected orphaned reasoning closing tag in x-ai model response")
144+
cleanedXml = cleanedXml.replace(/<\/think>/g, "")
145+
}
146+
}
147+
130148
// IMPORTANT: We use parseXmlForDiff here instead of parseXml to prevent HTML entity decoding
131149
// This ensures exact character matching when comparing parsed content against original file content
132150
// Without this, special characters like & would be decoded to &amp; causing diff mismatches
133-
const parsed = parseXmlForDiff(argsXmlTag, ["file.diff.content"]) as ParsedXmlResult
151+
const parsed = parseXmlForDiff(cleanedXml, ["file.diff.content"]) as ParsedXmlResult
134152
const files = Array.isArray(parsed.file) ? parsed.file : [parsed.file].filter(Boolean)
135153

136154
for (const file of files) {
@@ -158,6 +176,18 @@ export async function applyDiffTool(
158176
diffContent = typeof diff.content === "string" ? diff.content : ""
159177
startLine = diff.start_line ? parseInt(diff.start_line) : undefined
160178

179+
// For x-ai models, validate diff content doesn't contain reasoning artifacts
180+
if (isXAIModel && diffContent) {
181+
// Check for reasoning block markers that shouldn't be in diff content
182+
if (diffContent.includes("<think>") || diffContent.includes("</think>")) {
183+
console.warn(
184+
"[MultiApplyDiffTool] Cleaning reasoning artifacts from x-ai model diff content",
185+
)
186+
// Remove thinking blocks but preserve the actual diff markers
187+
diffContent = diffContent.replace(/<think>.*?<\/think>/gs, "")
188+
}
189+
}
190+
161191
// Only add to operations if we have valid content
162192
if (diffContent) {
163193
operationsMap[filePath].diff.push({

0 commit comments

Comments
 (0)