Skip to content

Commit fc7171b

Browse files
authored
fix(providers): fixed xai response format + tool calls not working when used together (#455)
* fix xai response format + tool calls not working when used together * removed extraneous comments
1 parent f3a4053 commit fc7171b

File tree

1 file changed

+160
-81
lines changed

1 file changed

+160
-81
lines changed

apps/sim/providers/xai/index.ts

Lines changed: 160 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,20 @@ export const xAIProvider: ProviderConfig = {
4444
throw new Error('API key is required for xAI')
4545
}
4646

47-
// Initialize OpenAI client for xAI
4847
const xai = new OpenAI({
4948
apiKey: request.apiKey,
5049
baseURL: 'https://api.x.ai/v1',
5150
})
5251

53-
// Prepare messages
54-
const allMessages = []
52+
logger.info('XAI Provider - Initial request configuration:', {
53+
hasTools: !!request.tools?.length,
54+
toolCount: request.tools?.length || 0,
55+
hasResponseFormat: !!request.responseFormat,
56+
model: request.model || 'grok-3-latest',
57+
streaming: !!request.stream,
58+
})
59+
60+
const allMessages: any[] = []
5561

5662
if (request.systemPrompt) {
5763
allMessages.push({
@@ -83,77 +89,71 @@ export const xAIProvider: ProviderConfig = {
8389
}))
8490
: undefined
8591

86-
// Build the request payload
87-
const payload: any = {
92+
// Log tools and response format conflict detection
93+
if (tools?.length && request.responseFormat) {
94+
logger.warn(
95+
'XAI Provider - Detected both tools and response format. Using tools first, then response format for final response.'
96+
)
97+
}
98+
99+
// Build the base request payload
100+
const basePayload: any = {
88101
model: request.model || 'grok-3-latest',
89102
messages: allMessages,
90103
}
91104

92-
if (request.temperature !== undefined) payload.temperature = request.temperature
93-
if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens
105+
if (request.temperature !== undefined) basePayload.temperature = request.temperature
106+
if (request.maxTokens !== undefined) basePayload.max_tokens = request.maxTokens
94107

95-
if (request.responseFormat) {
96-
payload.response_format = {
97-
type: 'json_schema',
98-
json_schema: {
99-
name: request.responseFormat.name || 'structured_response',
100-
schema: request.responseFormat.schema || request.responseFormat,
101-
strict: request.responseFormat.strict !== false,
102-
},
108+
// Function to create response format configuration
109+
const createResponseFormatPayload = (messages: any[] = allMessages) => {
110+
const payload = {
111+
...basePayload,
112+
messages,
103113
}
104114

105-
if (allMessages.length > 0 && allMessages[0].role === 'system') {
106-
allMessages[0].content = `${allMessages[0].content}\n\nYou MUST respond with a valid JSON object. DO NOT include any other text, explanations, or markdown formatting in your response - ONLY the JSON object.`
107-
} else {
108-
allMessages.unshift({
109-
role: 'system',
110-
content:
111-
'You MUST respond with a valid JSON object. DO NOT include any other text, explanations, or markdown formatting in your response - ONLY the JSON object.',
112-
})
115+
if (request.responseFormat) {
116+
payload.response_format = {
117+
type: 'json_schema',
118+
json_schema: {
119+
name: request.responseFormat.name || 'structured_response',
120+
schema: request.responseFormat.schema || request.responseFormat,
121+
strict: request.responseFormat.strict !== false,
122+
},
123+
}
113124
}
125+
126+
return payload
114127
}
115128

116129
// Handle tools and tool usage control
117130
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
118131

119132
if (tools?.length) {
120133
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'xai')
121-
const { tools: filteredTools, toolChoice } = preparedTools
122-
123-
if (filteredTools?.length && toolChoice) {
124-
payload.tools = filteredTools
125-
payload.tool_choice = toolChoice
126-
127-
logger.info('XAI request configuration:', {
128-
toolCount: filteredTools.length,
129-
toolChoice:
130-
typeof toolChoice === 'string'
131-
? toolChoice
132-
: toolChoice.type === 'function'
133-
? `force:${toolChoice.function.name}`
134-
: toolChoice.type === 'tool'
135-
? `force:${toolChoice.name}`
136-
: toolChoice.type === 'any'
137-
? `force:${toolChoice.any?.name || 'unknown'}`
138-
: 'unknown',
139-
model: request.model || 'grok-3-latest',
140-
})
141-
}
142134
}
143135

144136
// EARLY STREAMING: if caller requested streaming and there are no tools to execute,
145-
// we can directly stream the completion.
137+
// we can directly stream the completion with response format if needed
146138
if (request.stream && (!tools || tools.length === 0)) {
147-
logger.info('Using streaming response for XAI request (no tools)')
139+
logger.info('XAI Provider - Using direct streaming (no tools)')
148140

149141
// Start execution timer for the entire provider execution
150142
const providerStartTime = Date.now()
151143
const providerStartTimeISO = new Date(providerStartTime).toISOString()
152144

153-
const streamResponse = await xai.chat.completions.create({
154-
...payload,
155-
stream: true,
156-
})
145+
// Use response format payload if needed, otherwise use base payload
146+
const streamingPayload = request.responseFormat
147+
? createResponseFormatPayload()
148+
: { ...basePayload, stream: true }
149+
150+
if (!request.responseFormat) {
151+
streamingPayload.stream = true
152+
} else {
153+
streamingPayload.stream = true
154+
}
155+
156+
const streamResponse = await xai.chat.completions.create(streamingPayload)
157157

158158
// Start collecting token usage
159159
const tokenUsage = {
@@ -217,14 +217,29 @@ export const xAIProvider: ProviderConfig = {
217217
// Make the initial API request
218218
const initialCallTime = Date.now()
219219

220+
// For the initial request with tools, we NEVER include response_format
221+
// This is the key fix: tools and response_format cannot be used together with xAI
222+
const initialPayload = { ...basePayload }
223+
220224
// Track the original tool_choice for forced tool tracking
221-
const originalToolChoice = payload.tool_choice
225+
let originalToolChoice: any
222226

223227
// Track forced tools and their usage
224228
const forcedTools = preparedTools?.forcedTools || []
225229
let usedForcedTools: string[] = []
226230

227-
let currentResponse = await xai.chat.completions.create(payload)
231+
if (preparedTools?.tools?.length && preparedTools.toolChoice) {
232+
const { tools: filteredTools, toolChoice } = preparedTools
233+
initialPayload.tools = filteredTools
234+
initialPayload.tool_choice = toolChoice
235+
originalToolChoice = toolChoice
236+
} else if (request.responseFormat) {
237+
// Only add response format if there are no tools
238+
const responseFormatPayload = createResponseFormatPayload()
239+
Object.assign(initialPayload, responseFormatPayload)
240+
}
241+
242+
let currentResponse = await xai.chat.completions.create(initialPayload)
228243
const firstResponseTime = Date.now() - initialCallTime
229244

230245
let content = currentResponse.choices[0]?.message?.content || ''
@@ -278,7 +293,9 @@ export const xAIProvider: ProviderConfig = {
278293
}
279294

280295
// Check if a forced tool was used in the first response
281-
checkForForcedToolUsage(currentResponse, originalToolChoice)
296+
if (originalToolChoice) {
297+
checkForForcedToolUsage(currentResponse, originalToolChoice)
298+
}
282299

283300
try {
284301
while (iterationCount < MAX_ITERATIONS) {
@@ -297,7 +314,10 @@ export const xAIProvider: ProviderConfig = {
297314
const toolArgs = JSON.parse(toolCall.function.arguments)
298315

299316
const tool = request.tools?.find((t) => t.id === toolName)
300-
if (!tool) continue
317+
if (!tool) {
318+
logger.warn('XAI Provider - Tool not found:', { toolName })
319+
continue
320+
}
301321

302322
const toolCallStartTime = Date.now()
303323
const mergedArgs = {
@@ -309,7 +329,13 @@ export const xAIProvider: ProviderConfig = {
309329
const toolCallEndTime = Date.now()
310330
const toolCallDuration = toolCallEndTime - toolCallStartTime
311331

312-
if (!result.success) continue
332+
if (!result.success) {
333+
logger.warn('XAI Provider - Tool execution failed:', {
334+
toolName,
335+
error: result.error,
336+
})
337+
continue
338+
}
313339

314340
// Add to time segments
315341
timeSegments.push({
@@ -351,18 +377,19 @@ export const xAIProvider: ProviderConfig = {
351377
content: JSON.stringify(result.output),
352378
})
353379
} catch (error) {
354-
logger.error('Error processing tool call:', { error })
380+
logger.error('XAI Provider - Error processing tool call:', {
381+
error: error instanceof Error ? error.message : String(error),
382+
toolCall: toolCall.function.name,
383+
})
355384
}
356385
}
357386

358387
// Calculate tool call time for this iteration
359388
const thisToolsTime = Date.now() - toolsStartTime
360389
toolsTime += thisToolsTime
361390

362-
const nextPayload = {
363-
...payload,
364-
messages: currentMessages,
365-
}
391+
// After tool calls, create next payload based on whether we need more tools or final response
392+
let nextPayload: any
366393

367394
// Update tool_choice based on which forced tools have been used
368395
if (
@@ -374,16 +401,41 @@ export const xAIProvider: ProviderConfig = {
374401
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
375402

376403
if (remainingTools.length > 0) {
377-
// Force the next tool
378-
nextPayload.tool_choice = {
379-
type: 'function',
380-
function: { name: remainingTools[0] },
404+
// Force the next tool - continue with tools, no response format
405+
nextPayload = {
406+
...basePayload,
407+
messages: currentMessages,
408+
tools: preparedTools?.tools,
409+
tool_choice: {
410+
type: 'function',
411+
function: { name: remainingTools[0] },
412+
},
381413
}
382-
logger.info(`Forcing next tool: ${remainingTools[0]}`)
383414
} else {
384-
// All forced tools have been used, switch to auto
385-
nextPayload.tool_choice = 'auto'
386-
logger.info('All forced tools have been used, switching to auto tool_choice')
415+
// All forced tools have been used, check if we need response format for final response
416+
if (request.responseFormat) {
417+
nextPayload = createResponseFormatPayload(currentMessages)
418+
} else {
419+
nextPayload = {
420+
...basePayload,
421+
messages: currentMessages,
422+
tool_choice: 'auto',
423+
tools: preparedTools?.tools,
424+
}
425+
}
426+
}
427+
} else {
428+
// Normal tool processing - check if this might be the final response
429+
if (request.responseFormat) {
430+
// Use response format for what might be the final response
431+
nextPayload = createResponseFormatPayload(currentMessages)
432+
} else {
433+
nextPayload = {
434+
...basePayload,
435+
messages: currentMessages,
436+
tools: preparedTools?.tools,
437+
tool_choice: 'auto',
438+
}
387439
}
388440
}
389441

@@ -393,7 +445,9 @@ export const xAIProvider: ProviderConfig = {
393445
currentResponse = await xai.chat.completions.create(nextPayload)
394446

395447
// Check if any forced tools were used in this response
396-
checkForForcedToolUsage(currentResponse, nextPayload.tool_choice)
448+
if (nextPayload.tool_choice && typeof nextPayload.tool_choice === 'object') {
449+
checkForForcedToolUsage(currentResponse, nextPayload.tool_choice)
450+
}
397451

398452
const nextModelEndTime = Date.now()
399453
const thisModelTime = nextModelEndTime - nextModelStartTime
@@ -423,23 +477,35 @@ export const xAIProvider: ProviderConfig = {
423477
iterationCount++
424478
}
425479
} catch (error) {
426-
logger.error('Error in xAI request:', { error })
480+
logger.error('XAI Provider - Error in tool processing loop:', {
481+
error: error instanceof Error ? error.message : String(error),
482+
iterationCount,
483+
})
427484
}
428485

429486
// After all tool processing complete, if streaming was requested and we have messages, use streaming for the final response
430487
if (request.stream && iterationCount > 0) {
431-
logger.info('Using streaming for final XAI response after tool calls')
432-
433-
// When streaming after tool calls with forced tools, make sure tool_choice is set to 'auto'
434-
// This prevents the API from trying to force tool usage again in the final streaming response
435-
const streamingPayload = {
436-
...payload,
437-
messages: currentMessages,
438-
tool_choice: 'auto', // Always use 'auto' for the streaming response after tool calls
439-
stream: true,
488+
// For final streaming response, choose between tools (auto) or response_format (never both)
489+
let finalStreamingPayload: any
490+
491+
if (request.responseFormat) {
492+
// Use response format, no tools
493+
finalStreamingPayload = {
494+
...createResponseFormatPayload(currentMessages),
495+
stream: true,
496+
}
497+
} else {
498+
// Use tools with auto choice
499+
finalStreamingPayload = {
500+
...basePayload,
501+
messages: currentMessages,
502+
tool_choice: 'auto',
503+
tools: preparedTools?.tools,
504+
stream: true,
505+
}
440506
}
441507

442-
const streamResponse = await xai.chat.completions.create(streamingPayload)
508+
const streamResponse = await xai.chat.completions.create(finalStreamingPayload)
443509

444510
// Create a StreamingExecution response with all collected data
445511
const streamingResult = {
@@ -498,6 +564,14 @@ export const xAIProvider: ProviderConfig = {
498564
const providerEndTimeISO = new Date(providerEndTime).toISOString()
499565
const totalDuration = providerEndTime - providerStartTime
500566

567+
logger.info('XAI Provider - Request completed:', {
568+
totalDuration,
569+
iterationCount: iterationCount + 1,
570+
toolCallCount: toolCalls.length,
571+
hasContent: !!content,
572+
contentLength: content?.length || 0,
573+
})
574+
501575
return {
502576
content,
503577
model: request.model,
@@ -521,7 +595,12 @@ export const xAIProvider: ProviderConfig = {
521595
const providerEndTimeISO = new Date(providerEndTime).toISOString()
522596
const totalDuration = providerEndTime - providerStartTime
523597

524-
logger.error('Error in xAI request:', { error, duration: totalDuration })
598+
logger.error('XAI Provider - Request failed:', {
599+
error: error instanceof Error ? error.message : String(error),
600+
duration: totalDuration,
601+
hasTools: !!tools?.length,
602+
hasResponseFormat: !!request.responseFormat,
603+
})
525604

526605
// Create a new error with timing information
527606
const enhancedError = new Error(error instanceof Error ? error.message : String(error))

0 commit comments

Comments
 (0)