@@ -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 ( / < \/ ? a p p l y _ d i f f [ ^ > ] * > / g, "" )
151+ . replace ( / < \/ ? S E A R C H [ ^ > ] * > / g, "" )
152+ . replace ( / < \/ ? R E P L A C E [ ^ > ] * > / g, "" )
153+ . replace ( / < < < < < < < S E A R C H / g, "[SEARCH]" )
154+ . replace ( / = = = = = = = / g, "[SEPARATOR]" )
155+ . replace ( / > > > > > > > R E P L A C E / 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 ( / < \/ ? a p p l y _ d i f f [ ^ > ] * > / g, "" )
172+ . replace ( / < \/ ? S E A R C H [ ^ > ] * > / g, "" )
173+ . replace ( / < \/ ? R E P L A C E [ ^ > ] * > / g, "" )
174+ . replace ( / < < < < < < < S E A R C H / g, "[SEARCH]" )
175+ . replace ( / = = = = = = = / g, "[SEPARATOR]" )
176+ . replace ( / > > > > > > > R E P L A C E / 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 ( / < t h i n k > ( .* ?) < \/ t h i n k > / 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 ( / < t h i n k > .* ?< \/ t h i n k > / 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 }
0 commit comments