@ai-sdk/xai 0.0.0-64aae7dd-20260114144918 → 0.0.0-98261322-20260122142521

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +64 -5
  2. package/dist/index.js +1 -1
  3. package/dist/index.mjs +1 -1
  4. package/docs/01-xai.mdx +697 -0
  5. package/package.json +11 -6
  6. package/src/convert-to-xai-chat-messages.test.ts +243 -0
  7. package/src/convert-to-xai-chat-messages.ts +142 -0
  8. package/src/convert-xai-chat-usage.test.ts +240 -0
  9. package/src/convert-xai-chat-usage.ts +23 -0
  10. package/src/get-response-metadata.ts +19 -0
  11. package/src/index.ts +14 -0
  12. package/src/map-xai-finish-reason.ts +19 -0
  13. package/src/responses/__fixtures__/xai-code-execution-tool.1.json +68 -0
  14. package/src/responses/__fixtures__/xai-text-streaming.1.chunks.txt +698 -0
  15. package/src/responses/__fixtures__/xai-text-with-reasoning-streaming-store-false.1.chunks.txt +655 -0
  16. package/src/responses/__fixtures__/xai-text-with-reasoning-streaming.1.chunks.txt +679 -0
  17. package/src/responses/__fixtures__/xai-web-search-tool.1.chunks.txt +274 -0
  18. package/src/responses/__fixtures__/xai-web-search-tool.1.json +90 -0
  19. package/src/responses/__fixtures__/xai-x-search-tool.1.json +149 -0
  20. package/src/responses/__fixtures__/xai-x-search-tool.chunks.txt +1757 -0
  21. package/src/responses/__snapshots__/xai-responses-language-model.test.ts.snap +21929 -0
  22. package/src/responses/convert-to-xai-responses-input.test.ts +463 -0
  23. package/src/responses/convert-to-xai-responses-input.ts +206 -0
  24. package/src/responses/convert-xai-responses-usage.ts +24 -0
  25. package/src/responses/map-xai-responses-finish-reason.ts +20 -0
  26. package/src/responses/xai-responses-api.ts +393 -0
  27. package/src/responses/xai-responses-language-model.test.ts +1803 -0
  28. package/src/responses/xai-responses-language-model.ts +732 -0
  29. package/src/responses/xai-responses-options.ts +34 -0
  30. package/src/responses/xai-responses-prepare-tools.test.ts +497 -0
  31. package/src/responses/xai-responses-prepare-tools.ts +226 -0
  32. package/src/tool/code-execution.ts +17 -0
  33. package/src/tool/index.ts +15 -0
  34. package/src/tool/view-image.ts +20 -0
  35. package/src/tool/view-x-video.ts +18 -0
  36. package/src/tool/web-search.ts +56 -0
  37. package/src/tool/x-search.ts +63 -0
  38. package/src/version.ts +6 -0
  39. package/src/xai-chat-language-model.test.ts +1805 -0
  40. package/src/xai-chat-language-model.ts +681 -0
  41. package/src/xai-chat-options.ts +131 -0
  42. package/src/xai-chat-prompt.ts +44 -0
  43. package/src/xai-error.ts +19 -0
  44. package/src/xai-image-settings.ts +1 -0
  45. package/src/xai-prepare-tools.ts +95 -0
  46. package/src/xai-provider.test.ts +167 -0
  47. package/src/xai-provider.ts +162 -0
@@ -0,0 +1,463 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { convertToXaiResponsesInput } from './convert-to-xai-responses-input';
3
+
4
+ describe('convertToXaiResponsesInput', () => {
5
+ describe('system messages', () => {
6
+ it('should convert system messages', async () => {
7
+ const result = await convertToXaiResponsesInput({
8
+ prompt: [{ role: 'system', content: 'you are a helpful assistant' }],
9
+ });
10
+
11
+ expect(result.input).toMatchInlineSnapshot(`
12
+ [
13
+ {
14
+ "content": "you are a helpful assistant",
15
+ "role": "system",
16
+ },
17
+ ]
18
+ `);
19
+ expect(result.inputWarnings).toEqual([]);
20
+ });
21
+ });
22
+
23
+ describe('user messages', () => {
24
+ it('should convert single text part', async () => {
25
+ const result = await convertToXaiResponsesInput({
26
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
27
+ });
28
+
29
+ expect(result.input).toMatchInlineSnapshot(`
30
+ [
31
+ {
32
+ "content": [
33
+ {
34
+ "text": "hello",
35
+ "type": "input_text",
36
+ },
37
+ ],
38
+ "role": "user",
39
+ },
40
+ ]
41
+ `);
42
+ });
43
+
44
+ it('should concatenate multiple text parts', async () => {
45
+ const result = await convertToXaiResponsesInput({
46
+ prompt: [
47
+ {
48
+ role: 'user',
49
+ content: [
50
+ { type: 'text', text: 'hello ' },
51
+ { type: 'text', text: 'world' },
52
+ ],
53
+ },
54
+ ],
55
+ });
56
+
57
+ expect(result.input).toMatchInlineSnapshot(`
58
+ [
59
+ {
60
+ "content": [
61
+ {
62
+ "text": "hello ",
63
+ "type": "input_text",
64
+ },
65
+ {
66
+ "text": "world",
67
+ "type": "input_text",
68
+ },
69
+ ],
70
+ "role": "user",
71
+ },
72
+ ]
73
+ `);
74
+ });
75
+
76
+ it('should convert image file parts with URL', async () => {
77
+ const result = await convertToXaiResponsesInput({
78
+ prompt: [
79
+ {
80
+ role: 'user',
81
+ content: [
82
+ { type: 'text', text: 'what is in this image' },
83
+ {
84
+ type: 'file',
85
+ mediaType: 'image/jpeg',
86
+ data: new URL('https://example.com/image.jpg'),
87
+ },
88
+ ],
89
+ },
90
+ ],
91
+ });
92
+
93
+ expect(result.input).toMatchInlineSnapshot(`
94
+ [
95
+ {
96
+ "content": [
97
+ {
98
+ "text": "what is in this image",
99
+ "type": "input_text",
100
+ },
101
+ {
102
+ "image_url": "https://example.com/image.jpg",
103
+ "type": "input_image",
104
+ },
105
+ ],
106
+ "role": "user",
107
+ },
108
+ ]
109
+ `);
110
+ expect(result.inputWarnings).toEqual([]);
111
+ });
112
+
113
+ it('should convert image file parts with base64 data', async () => {
114
+ const result = await convertToXaiResponsesInput({
115
+ prompt: [
116
+ {
117
+ role: 'user',
118
+ content: [
119
+ { type: 'text', text: 'describe this' },
120
+ {
121
+ type: 'file',
122
+ mediaType: 'image/png',
123
+ data: new Uint8Array([1, 2, 3]),
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ });
129
+
130
+ expect(result.input).toMatchInlineSnapshot(`
131
+ [
132
+ {
133
+ "content": [
134
+ {
135
+ "text": "describe this",
136
+ "type": "input_text",
137
+ },
138
+ {
139
+ "image_url": "data:image/png;base64,AQID",
140
+ "type": "input_image",
141
+ },
142
+ ],
143
+ "role": "user",
144
+ },
145
+ ]
146
+ `);
147
+ });
148
+
149
+ it('should throw for unsupported file types', async () => {
150
+ await expect(
151
+ convertToXaiResponsesInput({
152
+ prompt: [
153
+ {
154
+ role: 'user',
155
+ content: [
156
+ { type: 'text', text: 'check this file' },
157
+ {
158
+ type: 'file',
159
+ mediaType: 'application/pdf',
160
+ data: new Uint8Array([1, 2, 3]),
161
+ },
162
+ ],
163
+ },
164
+ ],
165
+ }),
166
+ ).rejects.toThrow('file part media type application/pdf');
167
+ });
168
+ });
169
+
170
+ describe('assistant messages', () => {
171
+ it('should convert text content', async () => {
172
+ const result = await convertToXaiResponsesInput({
173
+ prompt: [
174
+ { role: 'assistant', content: [{ type: 'text', text: 'hi there' }] },
175
+ ],
176
+ });
177
+
178
+ expect(result.input).toMatchInlineSnapshot(`
179
+ [
180
+ {
181
+ "content": "hi there",
182
+ "id": undefined,
183
+ "role": "assistant",
184
+ },
185
+ ]
186
+ `);
187
+ });
188
+
189
+ it('should handle client-side tool-call parts', async () => {
190
+ const result = await convertToXaiResponsesInput({
191
+ prompt: [
192
+ {
193
+ role: 'assistant',
194
+ content: [
195
+ {
196
+ type: 'tool-call',
197
+ toolCallId: 'call_123',
198
+ toolName: 'weather',
199
+ input: { location: 'sf' },
200
+ },
201
+ ],
202
+ },
203
+ ],
204
+ });
205
+
206
+ expect(result.input).toMatchInlineSnapshot(`
207
+ [
208
+ {
209
+ "arguments": "{"location":"sf"}",
210
+ "call_id": "call_123",
211
+ "id": "call_123",
212
+ "name": "weather",
213
+ "status": "completed",
214
+ "type": "function_call",
215
+ },
216
+ ]
217
+ `);
218
+ });
219
+
220
+ it('should handle client-side tool-call parts named like server-side tools', async () => {
221
+ const result = await convertToXaiResponsesInput({
222
+ prompt: [
223
+ {
224
+ role: 'assistant',
225
+ content: [
226
+ {
227
+ type: 'tool-call',
228
+ toolCallId: 'call_ws',
229
+ toolName: 'web_search',
230
+ input: { query: 'latest news' },
231
+ },
232
+ ],
233
+ },
234
+ ],
235
+ });
236
+
237
+ expect(result.input).toMatchInlineSnapshot(`
238
+ [
239
+ {
240
+ "arguments": "{\"query\":\"latest news\"}",
241
+ "call_id": "call_ws",
242
+ "id": "call_ws",
243
+ "name": "web_search",
244
+ "status": "completed",
245
+ "type": "function_call",
246
+ },
247
+ ]
248
+ `);
249
+ });
250
+
251
+ it('should skip server-side tool-call parts', async () => {
252
+ const result = await convertToXaiResponsesInput({
253
+ prompt: [
254
+ {
255
+ role: 'assistant',
256
+ content: [
257
+ {
258
+ type: 'tool-call',
259
+ toolCallId: 'ws_123',
260
+ toolName: 'web_search',
261
+ input: {},
262
+ providerExecuted: true,
263
+ },
264
+ ],
265
+ },
266
+ ],
267
+ });
268
+
269
+ expect(result.input).toEqual([]);
270
+ });
271
+ });
272
+
273
+ describe('tool messages', () => {
274
+ it('should convert tool-result to function_call_output with json', async () => {
275
+ const result = await convertToXaiResponsesInput({
276
+ prompt: [
277
+ {
278
+ role: 'tool',
279
+ content: [
280
+ {
281
+ type: 'tool-result',
282
+ toolCallId: 'call_123',
283
+ toolName: 'weather',
284
+ output: {
285
+ type: 'json',
286
+ value: { temp: 72 },
287
+ },
288
+ },
289
+ ],
290
+ },
291
+ ],
292
+ });
293
+
294
+ expect(result.input).toMatchInlineSnapshot(`
295
+ [
296
+ {
297
+ "call_id": "call_123",
298
+ "output": "{"temp":72}",
299
+ "type": "function_call_output",
300
+ },
301
+ ]
302
+ `);
303
+ });
304
+
305
+ it('should handle text output', async () => {
306
+ const result = await convertToXaiResponsesInput({
307
+ prompt: [
308
+ {
309
+ role: 'tool',
310
+ content: [
311
+ {
312
+ type: 'tool-result',
313
+ toolCallId: 'call_123',
314
+ toolName: 'weather',
315
+ output: {
316
+ type: 'text',
317
+ value: 'sunny, 72 degrees',
318
+ },
319
+ },
320
+ ],
321
+ },
322
+ ],
323
+ });
324
+
325
+ expect(result.input).toMatchInlineSnapshot(`
326
+ [
327
+ {
328
+ "call_id": "call_123",
329
+ "output": "sunny, 72 degrees",
330
+ "type": "function_call_output",
331
+ },
332
+ ]
333
+ `);
334
+ });
335
+ });
336
+
337
+ describe('multi-turn conversations', () => {
338
+ it('should handle full conversation with client-side tool calls', async () => {
339
+ const result = await convertToXaiResponsesInput({
340
+ prompt: [
341
+ {
342
+ role: 'user',
343
+ content: [{ type: 'text', text: 'whats the weather' }],
344
+ },
345
+ {
346
+ role: 'assistant',
347
+ content: [
348
+ {
349
+ type: 'tool-call',
350
+ toolCallId: 'call_123',
351
+ toolName: 'weather',
352
+ input: { location: 'sf' },
353
+ },
354
+ ],
355
+ },
356
+ {
357
+ role: 'tool',
358
+ content: [
359
+ {
360
+ type: 'tool-result',
361
+ toolCallId: 'call_123',
362
+ toolName: 'weather',
363
+ output: {
364
+ type: 'json',
365
+ value: { temp: 72 },
366
+ },
367
+ },
368
+ ],
369
+ },
370
+ {
371
+ role: 'assistant',
372
+ content: [{ type: 'text', text: 'its 72 degrees' }],
373
+ },
374
+ ],
375
+ });
376
+
377
+ expect(result.input).toMatchInlineSnapshot(`
378
+ [
379
+ {
380
+ "content": [
381
+ {
382
+ "text": "whats the weather",
383
+ "type": "input_text",
384
+ },
385
+ ],
386
+ "role": "user",
387
+ },
388
+ {
389
+ "arguments": "{"location":"sf"}",
390
+ "call_id": "call_123",
391
+ "id": "call_123",
392
+ "name": "weather",
393
+ "status": "completed",
394
+ "type": "function_call",
395
+ },
396
+ {
397
+ "call_id": "call_123",
398
+ "output": "{"temp":72}",
399
+ "type": "function_call_output",
400
+ },
401
+ {
402
+ "content": "its 72 degrees",
403
+ "id": undefined,
404
+ "role": "assistant",
405
+ },
406
+ ]
407
+ `);
408
+ });
409
+
410
+ it('should handle conversation with server-side tool calls and item references', async () => {
411
+ const result = await convertToXaiResponsesInput({
412
+ prompt: [
413
+ {
414
+ role: 'user',
415
+ content: [{ type: 'text', text: 'search for ai news' }],
416
+ },
417
+ {
418
+ role: 'assistant',
419
+ content: [
420
+ {
421
+ type: 'tool-call',
422
+ toolCallId: 'ws_123',
423
+ toolName: 'web_search',
424
+ input: {},
425
+ providerExecuted: true,
426
+ },
427
+ {
428
+ type: 'tool-result',
429
+ toolCallId: 'ws_123',
430
+ toolName: 'web_search',
431
+ output: {
432
+ type: 'json',
433
+ value: {},
434
+ },
435
+ },
436
+ { type: 'text', text: 'here are the results' },
437
+ ],
438
+ },
439
+ ],
440
+ store: true,
441
+ });
442
+
443
+ expect(result.input).toMatchInlineSnapshot(`
444
+ [
445
+ {
446
+ "content": [
447
+ {
448
+ "text": "search for ai news",
449
+ "type": "input_text",
450
+ },
451
+ ],
452
+ "role": "user",
453
+ },
454
+ {
455
+ "content": "here are the results",
456
+ "id": undefined,
457
+ "role": "assistant",
458
+ },
459
+ ]
460
+ `);
461
+ });
462
+ });
463
+ });
@@ -0,0 +1,206 @@
1
+ import {
2
+ SharedV3Warning,
3
+ LanguageModelV3Message,
4
+ UnsupportedFunctionalityError,
5
+ } from '@ai-sdk/provider';
6
+ import { convertToBase64 } from '@ai-sdk/provider-utils';
7
+ import {
8
+ XaiResponsesInput,
9
+ XaiResponsesUserMessageContentPart,
10
+ } from './xai-responses-api';
11
+
12
+ export async function convertToXaiResponsesInput({
13
+ prompt,
14
+ }: {
15
+ prompt: LanguageModelV3Message[];
16
+ store?: boolean;
17
+ }): Promise<{
18
+ input: XaiResponsesInput;
19
+ inputWarnings: SharedV3Warning[];
20
+ }> {
21
+ const input: XaiResponsesInput = [];
22
+ const inputWarnings: SharedV3Warning[] = [];
23
+
24
+ for (const message of prompt) {
25
+ switch (message.role) {
26
+ case 'system': {
27
+ input.push({
28
+ role: 'system',
29
+ content: message.content,
30
+ });
31
+ break;
32
+ }
33
+
34
+ case 'user': {
35
+ const contentParts: XaiResponsesUserMessageContentPart[] = [];
36
+
37
+ for (const block of message.content) {
38
+ switch (block.type) {
39
+ case 'text': {
40
+ contentParts.push({ type: 'input_text', text: block.text });
41
+ break;
42
+ }
43
+
44
+ case 'file': {
45
+ if (block.mediaType.startsWith('image/')) {
46
+ const mediaType =
47
+ block.mediaType === 'image/*'
48
+ ? 'image/jpeg'
49
+ : block.mediaType;
50
+
51
+ const imageUrl =
52
+ block.data instanceof URL
53
+ ? block.data.toString()
54
+ : `data:${mediaType};base64,${convertToBase64(block.data)}`;
55
+
56
+ contentParts.push({ type: 'input_image', image_url: imageUrl });
57
+ } else {
58
+ throw new UnsupportedFunctionalityError({
59
+ functionality: `file part media type ${block.mediaType}`,
60
+ });
61
+ }
62
+ break;
63
+ }
64
+
65
+ default: {
66
+ const _exhaustiveCheck: never = block;
67
+ inputWarnings.push({
68
+ type: 'other',
69
+ message:
70
+ 'xAI Responses API does not support this content type in user messages',
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ input.push({
77
+ role: 'user',
78
+ content: contentParts,
79
+ });
80
+ break;
81
+ }
82
+
83
+ case 'assistant': {
84
+ for (const part of message.content) {
85
+ switch (part.type) {
86
+ case 'text': {
87
+ const id =
88
+ typeof part.providerOptions?.xai?.itemId === 'string'
89
+ ? part.providerOptions.xai.itemId
90
+ : undefined;
91
+
92
+ input.push({
93
+ role: 'assistant',
94
+ content: part.text,
95
+ id,
96
+ });
97
+
98
+ break;
99
+ }
100
+
101
+ case 'tool-call': {
102
+ if (part.providerExecuted) {
103
+ break;
104
+ }
105
+
106
+ const id =
107
+ typeof part.providerOptions?.xai?.itemId === 'string'
108
+ ? part.providerOptions.xai.itemId
109
+ : undefined;
110
+
111
+ input.push({
112
+ type: 'function_call',
113
+ id: id ?? part.toolCallId,
114
+ call_id: part.toolCallId,
115
+ name: part.toolName,
116
+ arguments: JSON.stringify(part.input),
117
+ status: 'completed',
118
+ });
119
+ break;
120
+ }
121
+
122
+ case 'tool-result': {
123
+ break;
124
+ }
125
+
126
+ case 'reasoning':
127
+ case 'file': {
128
+ inputWarnings.push({
129
+ type: 'other',
130
+ message: `xAI Responses API does not support ${part.type} in assistant messages`,
131
+ });
132
+ break;
133
+ }
134
+
135
+ default: {
136
+ const _exhaustiveCheck: never = part;
137
+ inputWarnings.push({
138
+ type: 'other',
139
+ message:
140
+ 'xAI Responses API does not support this content type in assistant messages',
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ break;
147
+ }
148
+
149
+ case 'tool': {
150
+ for (const part of message.content) {
151
+ if (part.type === 'tool-approval-response') {
152
+ continue;
153
+ }
154
+ const output = part.output;
155
+
156
+ let outputValue: string;
157
+ switch (output.type) {
158
+ case 'text':
159
+ case 'error-text':
160
+ outputValue = output.value;
161
+ break;
162
+ case 'execution-denied':
163
+ outputValue = output.reason ?? 'tool execution denied';
164
+ break;
165
+ case 'json':
166
+ case 'error-json':
167
+ outputValue = JSON.stringify(output.value);
168
+ break;
169
+ case 'content':
170
+ outputValue = output.value
171
+ .map(item => {
172
+ if (item.type === 'text') {
173
+ return item.text;
174
+ }
175
+ return '';
176
+ })
177
+ .join('');
178
+ break;
179
+ default: {
180
+ const _exhaustiveCheck: never = output;
181
+ outputValue = '';
182
+ }
183
+ }
184
+
185
+ input.push({
186
+ type: 'function_call_output',
187
+ call_id: part.toolCallId,
188
+ output: outputValue,
189
+ });
190
+ }
191
+
192
+ break;
193
+ }
194
+
195
+ default: {
196
+ const _exhaustiveCheck: never = message;
197
+ inputWarnings.push({
198
+ type: 'other',
199
+ message: 'unsupported message role',
200
+ });
201
+ }
202
+ }
203
+ }
204
+
205
+ return { input, inputWarnings };
206
+ }
@@ -0,0 +1,24 @@
1
+ import { LanguageModelV3Usage } from '@ai-sdk/provider';
2
+ import { XaiResponsesUsage } from './xai-responses-api';
3
+
4
+ export function convertXaiResponsesUsage(
5
+ usage: XaiResponsesUsage,
6
+ ): LanguageModelV3Usage {
7
+ const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? 0;
8
+ const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0;
9
+
10
+ return {
11
+ inputTokens: {
12
+ total: usage.input_tokens,
13
+ noCache: usage.input_tokens - cacheReadTokens,
14
+ cacheRead: cacheReadTokens,
15
+ cacheWrite: undefined,
16
+ },
17
+ outputTokens: {
18
+ total: usage.output_tokens,
19
+ text: usage.output_tokens - reasoningTokens,
20
+ reasoning: reasoningTokens,
21
+ },
22
+ raw: usage,
23
+ };
24
+ }
@@ -0,0 +1,20 @@
1
+ import { LanguageModelV3FinishReason } from '@ai-sdk/provider';
2
+
3
+ export function mapXaiResponsesFinishReason(
4
+ finishReason: string | null | undefined,
5
+ ): LanguageModelV3FinishReason['unified'] {
6
+ switch (finishReason) {
7
+ case 'stop':
8
+ case 'completed':
9
+ return 'stop';
10
+ case 'length':
11
+ return 'length';
12
+ case 'tool_calls':
13
+ case 'function_call':
14
+ return 'tool-calls';
15
+ case 'content_filter':
16
+ return 'content-filter';
17
+ default:
18
+ return 'other';
19
+ }
20
+ }