Skip to content

Conversation

@vercel-ai-sdk
Copy link
Contributor

@vercel-ai-sdk vercel-ai-sdk bot commented Nov 25, 2025

This is an automated backport of #10523 to the release-v5.0 branch. FYI @baturyilmaz

… type (#10523)

Add missing schema validation for xAI Responses API server-side tools:
- Add custom_tool_call type to outputItemSchema
- Make toolCallSchema fields optional for in_progress states
- Add input field for custom_tool_call (vs arguments)
- Add action field for in_progress tool execution states
- Add 12 streaming event types for tool lifecycle:
  - web_search_call: in_progress, searching, completed
  - x_search_call: in_progress, searching, completed
  - code_execution_call: in_progress, executing, completed
  - code_interpreter_call: in_progress, executing, completed

Fixes validation errors ('Invalid JSON response', 'No matching
discriminator') when using xai.responses() with xai.tools.webSearch(),
xai.tools.xSearch(), or xai.tools.codeExecution().

## Background

The xAI Responses API with server-side tools (`web_search`, `x_search`,
`code_execution`) was failing with validation errors when using the
Vercel AI SDK:

```
AI_TypeValidationError: Invalid JSON response
Error: No matching discriminator for output[].type
```

**Root cause**: The xAI API returns response formats that were not
included in the SDK's Zod validation schemas:

1. **`custom_tool_call` type** - Server-side tool calls use this type
instead of the standard tool call types
2. **Streaming progress events** - Events like
`response.web_search_call.in_progress`,
`response.web_search_call.searching`,
`response.web_search_call.completed` were not recognized
3. **Optional fields during execution** - During `in_progress` state,
fields like `name`, `arguments`, `call_id` are undefined
4. **Different field names** - `custom_tool_call` uses `input` field
instead of `arguments`

## Summary

Updated `packages/xai/src/responses/xai-responses-api.ts` to support the
complete xAI Responses API format:

### 1. Added `custom_tool_call` Type Support

**Type definition** (`XaiResponsesToolCall`):
```typescript
export type XaiResponsesToolCall = {
  type:
    | 'function_call'
    | 'web_search_call'
    | 'x_search_call'
    | 'code_interpreter_call'
    | 'custom_tool_call';  // ✅ Added
  id: string;
  call_id?: string;        // ✅ Made optional
  name?: string;           // ✅ Made optional
  arguments?: string;      // ✅ Made optional
  input?: string;          // ✅ Added for custom_tool_call
  status: string;
  action?: any;            // ✅ Added for in_progress state
};
```

**Schema** (`outputItemSchema`):
```typescript
z.object({
  type: z.literal('custom_tool_call'),
  ...toolCallSchema.shape,
}),
```

### 2. Made Tool Call Fields Optional

Updated `toolCallSchema` to handle in-progress states where fields are
undefined:
```typescript
const toolCallSchema = z.object({
  name: z.string().optional(),      // Was required
  arguments: z.string().optional(), // Was required
  input: z.string().optional(),     // ✅ New (for custom_tool_call)
  call_id: z.string().optional(),   // Was required
  id: z.string(),
  status: z.string(),
  action: z.any().optional(),       // ✅ New (for in_progress state)
});
```

### 3. Added 12 Streaming Event Types

Added to `xaiResponsesChunkSchema` for complete tool execution
lifecycle:

**Web Search:**
- `response.web_search_call.in_progress`
- `response.web_search_call.searching`
- `response.web_search_call.completed`

**X Search:**
- `response.x_search_call.in_progress`
- `response.x_search_call.searching`
- `response.x_search_call.completed`

**Code Execution:**
- `response.code_execution_call.in_progress`
- `response.code_execution_call.executing`
- `response.code_execution_call.completed`

**Code Interpreter:**
- `response.code_interpreter_call.in_progress`
- `response.code_interpreter_call.executing`
- `response.code_interpreter_call.completed`

## Manual Verification

Tested all server-side tools with both `generateText()` and
`streamText()` to ensure end-to-end functionality:

### ✅ Web Search Tool

```typescript
import { xai } from '@ai-sdk/xai';
import { generateText } from 'ai';

const { text, sources } = await generateText({
  model: xai.responses('grok-4-fast'),
  prompt: 'What are the latest developments in AI?',
  tools: {
    web_search: xai.tools.webSearch(),
  },
});

console.log(text); // Comprehensive response
console.log(sources); // Array of URL citations
```

**Result**: ✅ Returned comprehensive response with 14 URL citations, no
validation errors

### ✅ X Search Tool

```typescript
import { xai } from '@ai-sdk/xai';
import { generateText } from 'ai';

const { text, sources } = await generateText({
  model: xai.responses('grok-4-fast'),
  prompt: 'What are people saying about AI on X this week?',
  tools: {
    x_search: xai.tools.xSearch({
      allowedXHandles: ['elonmusk', 'xai'],
      fromDate: '2025-11-18',
      toDate: '2025-11-24',
      enableImageUnderstanding: true,
      enableVideoUnderstanding: true,
    }),
  },
});

console.log(text); // Analysis of X discussions
console.log(sources); // Array of X post citations
```

**Result**: ✅ Returned analysis with 16 X post citations, all streaming
events properly handled

### ✅ Code Execution Tool

```typescript
import { xai } from '@ai-sdk/xai';
import { generateText } from 'ai';

const { text } = await generateText({
  model: xai.responses('grok-4-fast'),
  prompt: 'Calculate the factorial of 20 using Python',
  tools: {
    code_execution: xai.tools.codeExecution(),
  },
});

console.log(text); // Result with code execution details
```

**Result**: ✅ Computed result with execution details, no validation
errors

### ✅ Multiple Tools with Streaming

```typescript
import { xai } from '@ai-sdk/xai';
import { streamText } from 'ai';

const { fullStream, usage: usagePromise } = streamText({
  model: xai.responses('grok-4-fast'),
  system: 'You are an AI research assistant.',
  tools: {
    web_search: xai.tools.webSearch(),
    x_search: xai.tools.xSearch(),
    code_execution: xai.tools.codeExecution(),
  },
  prompt: 'Research prompt caching in LLMs and explain how it reduces costs',
});

const sources = new Set<string>();
let lastToolName = '';

for await (const event of fullStream) {
  switch (event.type) {
    case 'tool-call':
      lastToolName = event.toolName;
      if (event.providerExecuted) {
        console.log(`[Calling ${event.toolName} on server...]`);
      }
      break;

    case 'tool-result':
      console.log(`[${lastToolName} completed]`);
      break;

    case 'text-delta':
      process.stdout.write(event.text);
      break;

    case 'source':
      if (event.sourceType === 'url') {
        sources.add(event.url);
      }
      break;
  }
}

const usage = await usagePromise;
console.log(`\nSources used: ${sources.size}`);
console.log(`Token usage: ${usage.inputTokens} input, ${usage.outputTokens} output`);
```

**Result**: ✅ Full streaming response with web searches, real-time
progress updates, and source citations. All streaming events
(`tool-call`, `tool-result`, `text-delta`, `source`) work correctly.

**Summary of manual testing:**
- ✅ All three tool types (web_search, x_search, code_execution) work
without validation errors
- ✅ Both `generateText()` and `streamText()` work correctly
- ✅ Source citations are properly parsed and returned
- ✅ Streaming progress events are handled correctly
- ✅ No "Invalid JSON response" or "No matching discriminator" errors

## Related issues

closes #10607
@vercel-ai-sdk vercel-ai-sdk bot merged commit 864881c into release-v5.0 Nov 25, 2025
24 checks passed
@vercel-ai-sdk vercel-ai-sdk bot deleted the backport-pr-10523-to-release-v5.0 branch November 25, 2025 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants