@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,1805 @@
1
+ import { LanguageModelV3Prompt } from '@ai-sdk/provider';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { createTestServer } from '@ai-sdk/test-server/with-vitest';
4
+ import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test';
5
+ import { XaiChatLanguageModel } from './xai-chat-language-model';
6
+ import { createXai } from './xai-provider';
7
+
8
+ const TEST_PROMPT: LanguageModelV3Prompt = [
9
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
10
+ ];
11
+
12
+ vi.mock('./version', () => ({
13
+ VERSION: '0.0.0-test',
14
+ }));
15
+
16
+ const testConfig = {
17
+ provider: 'xai.chat',
18
+ baseURL: 'https://api.x.ai/v1',
19
+ headers: () => ({ authorization: 'Bearer test-api-key' }),
20
+ generateId: () => 'test-id',
21
+ };
22
+
23
+ const model = new XaiChatLanguageModel('grok-beta', testConfig);
24
+
25
+ const server = createTestServer({
26
+ 'https://api.x.ai/v1/chat/completions': {},
27
+ });
28
+
29
+ describe('XaiChatLanguageModel', () => {
30
+ it('should be instantiated correctly', () => {
31
+ expect(model.modelId).toBe('grok-beta');
32
+ expect(model.provider).toBe('xai.chat');
33
+ expect(model.specificationVersion).toBe('v3');
34
+ });
35
+
36
+ it('should have supported URLs', () => {
37
+ expect(model.supportedUrls).toEqual({
38
+ 'image/*': [/^https?:\/\/.*$/],
39
+ });
40
+ });
41
+
42
+ describe('doGenerate', () => {
43
+ function prepareJsonResponse({
44
+ content = '',
45
+ usage = {
46
+ prompt_tokens: 4,
47
+ total_tokens: 34,
48
+ completion_tokens: 30,
49
+ },
50
+ id = 'chatcmpl-test-id',
51
+ created = 1699472111,
52
+ model = 'grok-beta',
53
+ headers,
54
+ }: {
55
+ content?: string;
56
+ usage?: {
57
+ prompt_tokens: number;
58
+ total_tokens: number;
59
+ completion_tokens: number;
60
+ };
61
+ id?: string;
62
+ created?: number;
63
+ model?: string;
64
+ headers?: Record<string, string>;
65
+ }) {
66
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
67
+ type: 'json-value',
68
+ headers,
69
+ body: {
70
+ id,
71
+ object: 'chat.completion',
72
+ created,
73
+ model,
74
+ choices: [
75
+ {
76
+ index: 0,
77
+ message: {
78
+ role: 'assistant',
79
+ content,
80
+ tool_calls: null,
81
+ },
82
+ finish_reason: 'stop',
83
+ },
84
+ ],
85
+ usage,
86
+ },
87
+ };
88
+ }
89
+
90
+ it('should extract text content', async () => {
91
+ prepareJsonResponse({ content: 'Hello, World!' });
92
+
93
+ const { content } = await model.doGenerate({
94
+ prompt: TEST_PROMPT,
95
+ });
96
+
97
+ expect(content).toMatchInlineSnapshot(`
98
+ [
99
+ {
100
+ "text": "Hello, World!",
101
+ "type": "text",
102
+ },
103
+ ]
104
+ `);
105
+ });
106
+
107
+ it('should avoid duplication when there is a trailing assistant message', async () => {
108
+ prepareJsonResponse({ content: 'prefix and more content' });
109
+
110
+ const { content } = await model.doGenerate({
111
+ prompt: [
112
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
113
+ {
114
+ role: 'assistant',
115
+ content: [{ type: 'text', text: 'prefix ' }],
116
+ },
117
+ ],
118
+ });
119
+
120
+ expect(content).toMatchInlineSnapshot(`
121
+ [
122
+ {
123
+ "text": "prefix and more content",
124
+ "type": "text",
125
+ },
126
+ ]
127
+ `);
128
+ });
129
+
130
+ it('should extract tool call content', async () => {
131
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
132
+ type: 'json-value',
133
+ body: {
134
+ id: 'chatcmpl-test-tool-call',
135
+ object: 'chat.completion',
136
+ created: 1699472111,
137
+ model: 'grok-beta',
138
+ choices: [
139
+ {
140
+ index: 0,
141
+ message: {
142
+ role: 'assistant',
143
+ content: null,
144
+ tool_calls: [
145
+ {
146
+ id: 'call_test123',
147
+ type: 'function',
148
+ function: {
149
+ name: 'weatherTool',
150
+ arguments: '{"location": "paris"}',
151
+ },
152
+ },
153
+ ],
154
+ },
155
+ finish_reason: 'tool_calls',
156
+ },
157
+ ],
158
+ usage: {
159
+ prompt_tokens: 124,
160
+ total_tokens: 146,
161
+ completion_tokens: 22,
162
+ },
163
+ },
164
+ };
165
+
166
+ const { content } = await model.doGenerate({
167
+ prompt: TEST_PROMPT,
168
+ });
169
+
170
+ expect(content).toMatchInlineSnapshot(`
171
+ [
172
+ {
173
+ "input": "{"location": "paris"}",
174
+ "toolCallId": "call_test123",
175
+ "toolName": "weatherTool",
176
+ "type": "tool-call",
177
+ },
178
+ ]
179
+ `);
180
+ });
181
+
182
+ it('should extract usage', async () => {
183
+ prepareJsonResponse({
184
+ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 },
185
+ });
186
+
187
+ const { usage } = await model.doGenerate({
188
+ prompt: TEST_PROMPT,
189
+ });
190
+
191
+ expect(usage).toMatchInlineSnapshot(`
192
+ {
193
+ "inputTokens": {
194
+ "cacheRead": 0,
195
+ "cacheWrite": undefined,
196
+ "noCache": 20,
197
+ "total": 20,
198
+ },
199
+ "outputTokens": {
200
+ "reasoning": 0,
201
+ "text": 5,
202
+ "total": 5,
203
+ },
204
+ "raw": {
205
+ "completion_tokens": 5,
206
+ "prompt_tokens": 20,
207
+ "total_tokens": 25,
208
+ },
209
+ }
210
+ `);
211
+ });
212
+
213
+ it('should send additional response information', async () => {
214
+ prepareJsonResponse({
215
+ id: 'test-id',
216
+ created: 123,
217
+ model: 'test-model',
218
+ });
219
+
220
+ const { response } = await model.doGenerate({
221
+ prompt: TEST_PROMPT,
222
+ });
223
+
224
+ expect({
225
+ id: response?.id,
226
+ timestamp: response?.timestamp,
227
+ modelId: response?.modelId,
228
+ }).toStrictEqual({
229
+ id: 'test-id',
230
+ timestamp: new Date(123 * 1000),
231
+ modelId: 'test-model',
232
+ });
233
+ });
234
+
235
+ it('should expose the raw response headers', async () => {
236
+ prepareJsonResponse({
237
+ headers: { 'test-header': 'test-value' },
238
+ });
239
+
240
+ const { response } = await model.doGenerate({
241
+ prompt: TEST_PROMPT,
242
+ });
243
+
244
+ expect(response?.headers).toStrictEqual({
245
+ // default headers:
246
+ 'content-length': '271',
247
+ 'content-type': 'application/json',
248
+
249
+ // custom header
250
+ 'test-header': 'test-value',
251
+ });
252
+ });
253
+
254
+ it('should pass the model and the messages', async () => {
255
+ prepareJsonResponse({ content: '' });
256
+
257
+ await model.doGenerate({
258
+ prompt: TEST_PROMPT,
259
+ });
260
+
261
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
262
+ model: 'grok-beta',
263
+ messages: [{ role: 'user', content: 'Hello' }],
264
+ });
265
+ });
266
+
267
+ it('should pass tools and toolChoice', async () => {
268
+ prepareJsonResponse({ content: '' });
269
+
270
+ await model.doGenerate({
271
+ tools: [
272
+ {
273
+ type: 'function',
274
+ name: 'test-tool',
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: { value: { type: 'string' } },
278
+ required: ['value'],
279
+ additionalProperties: false,
280
+ $schema: 'http://json-schema.org/draft-07/schema#',
281
+ },
282
+ },
283
+ ],
284
+ toolChoice: {
285
+ type: 'tool',
286
+ toolName: 'test-tool',
287
+ },
288
+ prompt: TEST_PROMPT,
289
+ });
290
+
291
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
292
+ model: 'grok-beta',
293
+ messages: [{ role: 'user', content: 'Hello' }],
294
+ tools: [
295
+ {
296
+ type: 'function',
297
+ function: {
298
+ name: 'test-tool',
299
+ parameters: {
300
+ type: 'object',
301
+ properties: { value: { type: 'string' } },
302
+ required: ['value'],
303
+ additionalProperties: false,
304
+ $schema: 'http://json-schema.org/draft-07/schema#',
305
+ },
306
+ },
307
+ },
308
+ ],
309
+ tool_choice: {
310
+ type: 'function',
311
+ function: { name: 'test-tool' },
312
+ },
313
+ });
314
+ });
315
+
316
+ it('should pass parallel_function_calling provider option', async () => {
317
+ prepareJsonResponse({ content: '' });
318
+
319
+ await model.doGenerate({
320
+ prompt: TEST_PROMPT,
321
+ providerOptions: {
322
+ xai: {
323
+ parallel_function_calling: false,
324
+ },
325
+ },
326
+ });
327
+
328
+ expect(await server.calls[0].requestBodyJson).toMatchObject({
329
+ model: 'grok-beta',
330
+ messages: [{ role: 'user', content: 'Hello' }],
331
+ parallel_function_calling: false,
332
+ });
333
+ });
334
+
335
+ it('should pass headers', async () => {
336
+ prepareJsonResponse({ content: '' });
337
+
338
+ const modelWithHeaders = new XaiChatLanguageModel('grok-beta', {
339
+ provider: 'xai.chat',
340
+ baseURL: 'https://api.x.ai/v1',
341
+ headers: () => ({
342
+ authorization: 'Bearer test-api-key',
343
+ 'Custom-Provider-Header': 'provider-header-value',
344
+ }),
345
+
346
+ generateId: () => 'test-id',
347
+ });
348
+
349
+ await modelWithHeaders.doGenerate({
350
+ prompt: TEST_PROMPT,
351
+ headers: {
352
+ 'Custom-Request-Header': 'request-header-value',
353
+ },
354
+ });
355
+
356
+ const requestHeaders = server.calls[0].requestHeaders;
357
+
358
+ expect(requestHeaders).toStrictEqual({
359
+ authorization: 'Bearer test-api-key',
360
+ 'content-type': 'application/json',
361
+ 'custom-provider-header': 'provider-header-value',
362
+ 'custom-request-header': 'request-header-value',
363
+ });
364
+ });
365
+
366
+ it('should include provider user agent when using createXai', async () => {
367
+ prepareJsonResponse({ content: '' });
368
+
369
+ const xai = createXai({
370
+ apiKey: 'test-api-key',
371
+ headers: { 'Custom-Provider-Header': 'provider-header-value' },
372
+ });
373
+
374
+ const modelWithHeaders = xai.chat('grok-beta');
375
+
376
+ await modelWithHeaders.doGenerate({
377
+ prompt: TEST_PROMPT,
378
+ headers: { 'Custom-Request-Header': 'request-header-value' },
379
+ });
380
+
381
+ expect(server.calls[0].requestUserAgent).toContain(
382
+ `ai-sdk/xai/0.0.0-test`,
383
+ );
384
+ });
385
+
386
+ it('should send request body', async () => {
387
+ prepareJsonResponse({ content: '' });
388
+
389
+ const { request } = await model.doGenerate({
390
+ prompt: TEST_PROMPT,
391
+ });
392
+
393
+ expect(request).toMatchInlineSnapshot(`
394
+ {
395
+ "body": {
396
+ "max_completion_tokens": undefined,
397
+ "messages": [
398
+ {
399
+ "content": "Hello",
400
+ "role": "user",
401
+ },
402
+ ],
403
+ "model": "grok-beta",
404
+ "parallel_function_calling": undefined,
405
+ "reasoning_effort": undefined,
406
+ "response_format": undefined,
407
+ "search_parameters": undefined,
408
+ "seed": undefined,
409
+ "temperature": undefined,
410
+ "tool_choice": undefined,
411
+ "tools": undefined,
412
+ "top_p": undefined,
413
+ },
414
+ }
415
+ `);
416
+ });
417
+
418
+ it('should pass search parameters', async () => {
419
+ prepareJsonResponse({ content: '' });
420
+
421
+ await model.doGenerate({
422
+ prompt: TEST_PROMPT,
423
+ providerOptions: {
424
+ xai: {
425
+ searchParameters: {
426
+ mode: 'auto',
427
+ returnCitations: true,
428
+ fromDate: '2024-01-01',
429
+ toDate: '2024-12-31',
430
+ maxSearchResults: 10,
431
+ },
432
+ },
433
+ },
434
+ });
435
+
436
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
437
+ model: 'grok-beta',
438
+ messages: [{ role: 'user', content: 'Hello' }],
439
+ search_parameters: {
440
+ mode: 'auto',
441
+ return_citations: true,
442
+ from_date: '2024-01-01',
443
+ to_date: '2024-12-31',
444
+ max_search_results: 10,
445
+ },
446
+ });
447
+ });
448
+
449
+ it('should pass search parameters with sources array', async () => {
450
+ prepareJsonResponse({ content: '' });
451
+
452
+ await model.doGenerate({
453
+ prompt: TEST_PROMPT,
454
+ providerOptions: {
455
+ xai: {
456
+ searchParameters: {
457
+ mode: 'on',
458
+ sources: [
459
+ {
460
+ type: 'web',
461
+ country: 'US',
462
+ excludedWebsites: ['example.com'],
463
+ safeSearch: false,
464
+ },
465
+ {
466
+ type: 'x',
467
+ includedXHandles: ['grok'],
468
+ excludedXHandles: ['openai'],
469
+ postFavoriteCount: 5,
470
+ postViewCount: 50,
471
+ },
472
+ {
473
+ type: 'news',
474
+ country: 'GB',
475
+ },
476
+ {
477
+ type: 'rss',
478
+ links: ['https://status.x.ai/feed.xml'],
479
+ },
480
+ ],
481
+ },
482
+ },
483
+ },
484
+ });
485
+
486
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
487
+ model: 'grok-beta',
488
+ messages: [{ role: 'user', content: 'Hello' }],
489
+ search_parameters: {
490
+ mode: 'on',
491
+ sources: [
492
+ {
493
+ type: 'web',
494
+ country: 'US',
495
+ excluded_websites: ['example.com'],
496
+ safe_search: false,
497
+ },
498
+ {
499
+ type: 'x',
500
+ included_x_handles: ['grok'],
501
+ excluded_x_handles: ['openai'],
502
+ post_favorite_count: 5,
503
+ post_view_count: 50,
504
+ },
505
+ {
506
+ type: 'news',
507
+ country: 'GB',
508
+ },
509
+ {
510
+ type: 'rss',
511
+ links: ['https://status.x.ai/feed.xml'],
512
+ },
513
+ ],
514
+ },
515
+ });
516
+ });
517
+
518
+ it('should extract content when message content is a content object', async () => {
519
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
520
+ type: 'json-value',
521
+ body: {
522
+ id: 'object-id',
523
+ object: 'chat.completion',
524
+ created: 1699472111,
525
+ model: 'grok-beta',
526
+ choices: [
527
+ {
528
+ index: 0,
529
+ message: {
530
+ role: 'assistant',
531
+ content: 'Hello from object',
532
+ tool_calls: null,
533
+ },
534
+ finish_reason: 'stop',
535
+ },
536
+ ],
537
+ usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 },
538
+ },
539
+ };
540
+
541
+ const { content } = await model.doGenerate({
542
+ prompt: TEST_PROMPT,
543
+ });
544
+
545
+ expect(content).toMatchInlineSnapshot(`
546
+ [
547
+ {
548
+ "text": "Hello from object",
549
+ "type": "text",
550
+ },
551
+ ]
552
+ `);
553
+ });
554
+
555
+ it('should extract citations as sources', async () => {
556
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
557
+ type: 'json-value',
558
+ body: {
559
+ id: 'citations-test',
560
+ object: 'chat.completion',
561
+ created: 1699472111,
562
+ model: 'grok-beta',
563
+ choices: [
564
+ {
565
+ index: 0,
566
+ message: {
567
+ role: 'assistant',
568
+ content: 'Here are the latest developments in AI.',
569
+ tool_calls: null,
570
+ },
571
+ finish_reason: 'stop',
572
+ },
573
+ ],
574
+ usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 },
575
+ citations: [
576
+ 'https://example.com/article1',
577
+ 'https://example.com/article2',
578
+ ],
579
+ },
580
+ };
581
+
582
+ const { content } = await model.doGenerate({
583
+ prompt: TEST_PROMPT,
584
+ providerOptions: {
585
+ xai: {
586
+ searchParameters: {
587
+ mode: 'auto',
588
+ returnCitations: true,
589
+ },
590
+ },
591
+ },
592
+ });
593
+
594
+ expect(content).toMatchInlineSnapshot(`
595
+ [
596
+ {
597
+ "text": "Here are the latest developments in AI.",
598
+ "type": "text",
599
+ },
600
+ {
601
+ "id": "test-id",
602
+ "sourceType": "url",
603
+ "type": "source",
604
+ "url": "https://example.com/article1",
605
+ },
606
+ {
607
+ "id": "test-id",
608
+ "sourceType": "url",
609
+ "type": "source",
610
+ "url": "https://example.com/article2",
611
+ },
612
+ ]
613
+ `);
614
+ });
615
+
616
+ it('should handle complex search parameter combinations', async () => {
617
+ prepareJsonResponse({
618
+ content: 'Research results from multiple sources',
619
+ });
620
+
621
+ await model.doGenerate({
622
+ prompt: TEST_PROMPT,
623
+ providerOptions: {
624
+ xai: {
625
+ searchParameters: {
626
+ mode: 'on',
627
+ returnCitations: true,
628
+ fromDate: '2024-01-01',
629
+ toDate: '2024-12-31',
630
+ maxSearchResults: 15,
631
+ sources: [
632
+ {
633
+ type: 'web',
634
+ country: 'US',
635
+ allowedWebsites: ['arxiv.org', 'nature.com'],
636
+ safeSearch: true,
637
+ },
638
+ {
639
+ type: 'news',
640
+ country: 'GB',
641
+ excludedWebsites: ['tabloid.com'],
642
+ },
643
+ {
644
+ type: 'x',
645
+ includedXHandles: ['openai', 'deepmind'],
646
+ excludedXHandles: ['grok'],
647
+ postFavoriteCount: 10,
648
+ postViewCount: 100,
649
+ },
650
+ ],
651
+ },
652
+ },
653
+ },
654
+ });
655
+
656
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
657
+ model: 'grok-beta',
658
+ messages: [{ role: 'user', content: 'Hello' }],
659
+ search_parameters: {
660
+ mode: 'on',
661
+ return_citations: true,
662
+ from_date: '2024-01-01',
663
+ to_date: '2024-12-31',
664
+ max_search_results: 15,
665
+ sources: [
666
+ {
667
+ type: 'web',
668
+ country: 'US',
669
+ allowed_websites: ['arxiv.org', 'nature.com'],
670
+ safe_search: true,
671
+ },
672
+ {
673
+ type: 'news',
674
+ country: 'GB',
675
+ excluded_websites: ['tabloid.com'],
676
+ },
677
+ {
678
+ type: 'x',
679
+ included_x_handles: ['openai', 'deepmind'],
680
+ excluded_x_handles: ['grok'],
681
+ post_favorite_count: 10,
682
+ post_view_count: 100,
683
+ },
684
+ ],
685
+ },
686
+ });
687
+ });
688
+
689
+ it('should handle empty citations array', async () => {
690
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
691
+ type: 'json-value',
692
+ body: {
693
+ id: 'no-citations-test',
694
+ object: 'chat.completion',
695
+ created: 1699472111,
696
+ model: 'grok-beta',
697
+ choices: [
698
+ {
699
+ index: 0,
700
+ message: {
701
+ role: 'assistant',
702
+ content: 'Response without citations.',
703
+ tool_calls: null,
704
+ },
705
+ finish_reason: 'stop',
706
+ },
707
+ ],
708
+ usage: { prompt_tokens: 4, total_tokens: 34, completion_tokens: 30 },
709
+ citations: [],
710
+ },
711
+ };
712
+
713
+ const { content } = await model.doGenerate({
714
+ prompt: TEST_PROMPT,
715
+ providerOptions: {
716
+ xai: {
717
+ searchParameters: {
718
+ mode: 'auto',
719
+ returnCitations: true,
720
+ },
721
+ },
722
+ },
723
+ });
724
+
725
+ expect(content).toMatchInlineSnapshot(`
726
+ [
727
+ {
728
+ "text": "Response without citations.",
729
+ "type": "text",
730
+ },
731
+ ]
732
+ `);
733
+ });
734
+
735
+ it('should support json schema response format without warnings', async () => {
736
+ prepareJsonResponse({ content: '{"name":"john doe"}' });
737
+
738
+ const { warnings } = await model.doGenerate({
739
+ prompt: TEST_PROMPT,
740
+ responseFormat: {
741
+ type: 'json',
742
+ schema: {
743
+ type: 'object',
744
+ properties: {
745
+ name: { type: 'string' },
746
+ },
747
+ required: ['name'],
748
+ additionalProperties: false,
749
+ },
750
+ },
751
+ });
752
+
753
+ expect(warnings).toEqual([]);
754
+ });
755
+
756
+ it('should send json schema in response format', async () => {
757
+ prepareJsonResponse({ content: '{"name":"john"}' });
758
+
759
+ await model.doGenerate({
760
+ prompt: TEST_PROMPT,
761
+ responseFormat: {
762
+ type: 'json',
763
+ name: 'person',
764
+ schema: {
765
+ type: 'object',
766
+ properties: {
767
+ name: { type: 'string' },
768
+ },
769
+ required: ['name'],
770
+ },
771
+ },
772
+ });
773
+
774
+ expect(await server.calls[0].requestBodyJson).toMatchObject({
775
+ model: 'grok-beta',
776
+ response_format: {
777
+ type: 'json_schema',
778
+ json_schema: {
779
+ name: 'person',
780
+ schema: {
781
+ type: 'object',
782
+ properties: {
783
+ name: { type: 'string' },
784
+ },
785
+ required: ['name'],
786
+ },
787
+ strict: true,
788
+ },
789
+ },
790
+ });
791
+ });
792
+ });
793
+
794
+ describe('doStream', () => {
795
+ function prepareStreamResponse({
796
+ content,
797
+ headers,
798
+ }: {
799
+ content: string[];
800
+ headers?: Record<string, string>;
801
+ }) {
802
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
803
+ type: 'stream-chunks',
804
+ headers,
805
+ chunks: [
806
+ `data: {"id":"35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe","object":"chat.completion.chunk",` +
807
+ `"created":1750537778,"model":"grok-beta","choices":[{"index":0,` +
808
+ `"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
809
+ ...content.map(text => {
810
+ return (
811
+ `data: {"id":"35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe","object":"chat.completion.chunk",` +
812
+ `"created":1750537778,"model":"grok-beta","choices":[{"index":0,` +
813
+ `"delta":{"role":"assistant","content":"${text}"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`
814
+ );
815
+ }),
816
+ `data: {"id":"35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe","object":"chat.completion.chunk",` +
817
+ `"created":1750537778,"model":"grok-beta","choices":[{"index":0,` +
818
+ `"delta":{"content":""},"finish_reason":"stop"}],` +
819
+ `"usage":{"prompt_tokens":4,"total_tokens":36,"completion_tokens":32},"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
820
+ `data: [DONE]\n\n`,
821
+ ],
822
+ };
823
+ }
824
+
825
+ it('should stream text deltas', async () => {
826
+ prepareStreamResponse({ content: ['Hello', ', ', 'world!'] });
827
+
828
+ const { stream } = await model.doStream({
829
+ prompt: TEST_PROMPT,
830
+ includeRawChunks: false,
831
+ });
832
+
833
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
834
+ [
835
+ {
836
+ "type": "stream-start",
837
+ "warnings": [],
838
+ },
839
+ {
840
+ "id": "35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
841
+ "modelId": "grok-beta",
842
+ "timestamp": 2025-06-21T20:29:38.000Z,
843
+ "type": "response-metadata",
844
+ },
845
+ {
846
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
847
+ "type": "text-start",
848
+ },
849
+ {
850
+ "delta": "Hello",
851
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
852
+ "type": "text-delta",
853
+ },
854
+ {
855
+ "delta": ", ",
856
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
857
+ "type": "text-delta",
858
+ },
859
+ {
860
+ "delta": "world!",
861
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
862
+ "type": "text-delta",
863
+ },
864
+ {
865
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
866
+ "type": "text-end",
867
+ },
868
+ {
869
+ "finishReason": {
870
+ "raw": "stop",
871
+ "unified": "stop",
872
+ },
873
+ "type": "finish",
874
+ "usage": {
875
+ "inputTokens": {
876
+ "cacheRead": 0,
877
+ "cacheWrite": undefined,
878
+ "noCache": 4,
879
+ "total": 4,
880
+ },
881
+ "outputTokens": {
882
+ "reasoning": 0,
883
+ "text": 32,
884
+ "total": 32,
885
+ },
886
+ "raw": {
887
+ "completion_tokens": 32,
888
+ "prompt_tokens": 4,
889
+ "total_tokens": 36,
890
+ },
891
+ },
892
+ },
893
+ ]
894
+ `);
895
+ });
896
+
897
+ it('should avoid duplication when there is a trailing assistant message', async () => {
898
+ prepareStreamResponse({ content: ['prefix', ' and', ' more content'] });
899
+
900
+ const { stream } = await model.doStream({
901
+ prompt: [
902
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
903
+ {
904
+ role: 'assistant',
905
+ content: [{ type: 'text', text: 'prefix ' }],
906
+ },
907
+ ],
908
+ includeRawChunks: false,
909
+ });
910
+
911
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
912
+ [
913
+ {
914
+ "type": "stream-start",
915
+ "warnings": [],
916
+ },
917
+ {
918
+ "id": "35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
919
+ "modelId": "grok-beta",
920
+ "timestamp": 2025-06-21T20:29:38.000Z,
921
+ "type": "response-metadata",
922
+ },
923
+ {
924
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
925
+ "type": "text-start",
926
+ },
927
+ {
928
+ "delta": "prefix",
929
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
930
+ "type": "text-delta",
931
+ },
932
+ {
933
+ "delta": " and",
934
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
935
+ "type": "text-delta",
936
+ },
937
+ {
938
+ "delta": " more content",
939
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
940
+ "type": "text-delta",
941
+ },
942
+ {
943
+ "id": "text-35e18f56-4ec6-48e4-8ca0-c1c4cbeeebbe",
944
+ "type": "text-end",
945
+ },
946
+ {
947
+ "finishReason": {
948
+ "raw": "stop",
949
+ "unified": "stop",
950
+ },
951
+ "type": "finish",
952
+ "usage": {
953
+ "inputTokens": {
954
+ "cacheRead": 0,
955
+ "cacheWrite": undefined,
956
+ "noCache": 4,
957
+ "total": 4,
958
+ },
959
+ "outputTokens": {
960
+ "reasoning": 0,
961
+ "text": 32,
962
+ "total": 32,
963
+ },
964
+ "raw": {
965
+ "completion_tokens": 32,
966
+ "prompt_tokens": 4,
967
+ "total_tokens": 36,
968
+ },
969
+ },
970
+ },
971
+ ]
972
+ `);
973
+ });
974
+
975
+ it('should stream tool deltas', async () => {
976
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
977
+ type: 'stream-chunks',
978
+ chunks: [
979
+ `data: {"id":"a9648117-740c-4270-9e07-6a8457f23b7a","object":"chat.completion.chunk","created":1750535985,"model":"grok-beta",` +
980
+ `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
981
+ `data: {"id":"a9648117-740c-4270-9e07-6a8457f23b7a","object":"chat.completion.chunk","created":1750535985,"model":"grok-beta",` +
982
+ `"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"id":"call_yfBEybNYi","type":"function","function":{"name":"test-tool","arguments":` +
983
+ `"{\\"value\\":\\"Sparkle Day\\"}"` +
984
+ `}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":183,"total_tokens":316,"completion_tokens":133},"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
985
+ 'data: [DONE]\n\n',
986
+ ],
987
+ };
988
+
989
+ const { stream } = await model.doStream({
990
+ tools: [
991
+ {
992
+ type: 'function',
993
+ name: 'test-tool',
994
+ inputSchema: {
995
+ type: 'object',
996
+ properties: { value: { type: 'string' } },
997
+ required: ['value'],
998
+ additionalProperties: false,
999
+ $schema: 'http://json-schema.org/draft-07/schema#',
1000
+ },
1001
+ },
1002
+ ],
1003
+ prompt: TEST_PROMPT,
1004
+ includeRawChunks: false,
1005
+ });
1006
+
1007
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1008
+ [
1009
+ {
1010
+ "type": "stream-start",
1011
+ "warnings": [],
1012
+ },
1013
+ {
1014
+ "id": "a9648117-740c-4270-9e07-6a8457f23b7a",
1015
+ "modelId": "grok-beta",
1016
+ "timestamp": 2025-06-21T19:59:45.000Z,
1017
+ "type": "response-metadata",
1018
+ },
1019
+ {
1020
+ "id": "call_yfBEybNYi",
1021
+ "toolName": "test-tool",
1022
+ "type": "tool-input-start",
1023
+ },
1024
+ {
1025
+ "delta": "{"value":"Sparkle Day"}",
1026
+ "id": "call_yfBEybNYi",
1027
+ "type": "tool-input-delta",
1028
+ },
1029
+ {
1030
+ "id": "call_yfBEybNYi",
1031
+ "type": "tool-input-end",
1032
+ },
1033
+ {
1034
+ "input": "{"value":"Sparkle Day"}",
1035
+ "toolCallId": "call_yfBEybNYi",
1036
+ "toolName": "test-tool",
1037
+ "type": "tool-call",
1038
+ },
1039
+ {
1040
+ "finishReason": {
1041
+ "raw": "tool_calls",
1042
+ "unified": "tool-calls",
1043
+ },
1044
+ "type": "finish",
1045
+ "usage": {
1046
+ "inputTokens": {
1047
+ "cacheRead": 0,
1048
+ "cacheWrite": undefined,
1049
+ "noCache": 183,
1050
+ "total": 183,
1051
+ },
1052
+ "outputTokens": {
1053
+ "reasoning": 0,
1054
+ "text": 133,
1055
+ "total": 133,
1056
+ },
1057
+ "raw": {
1058
+ "completion_tokens": 133,
1059
+ "prompt_tokens": 183,
1060
+ "total_tokens": 316,
1061
+ },
1062
+ },
1063
+ },
1064
+ ]
1065
+ `);
1066
+ });
1067
+
1068
+ it('should expose the raw response headers', async () => {
1069
+ prepareStreamResponse({
1070
+ content: [],
1071
+ headers: { 'test-header': 'test-value' },
1072
+ });
1073
+
1074
+ const { response } = await model.doStream({
1075
+ prompt: TEST_PROMPT,
1076
+ includeRawChunks: false,
1077
+ });
1078
+
1079
+ expect(response?.headers).toStrictEqual({
1080
+ // default headers:
1081
+ 'content-type': 'text/event-stream',
1082
+ 'cache-control': 'no-cache',
1083
+ connection: 'keep-alive',
1084
+
1085
+ // custom header
1086
+ 'test-header': 'test-value',
1087
+ });
1088
+ });
1089
+
1090
+ it('should pass the messages', async () => {
1091
+ prepareStreamResponse({ content: [''] });
1092
+
1093
+ await model.doStream({
1094
+ prompt: TEST_PROMPT,
1095
+ includeRawChunks: false,
1096
+ });
1097
+
1098
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1099
+ stream: true,
1100
+ model: 'grok-beta',
1101
+ messages: [{ role: 'user', content: 'Hello' }],
1102
+ stream_options: {
1103
+ include_usage: true,
1104
+ },
1105
+ });
1106
+ });
1107
+
1108
+ it('should pass headers', async () => {
1109
+ prepareStreamResponse({ content: [] });
1110
+
1111
+ const modelWithHeaders = new XaiChatLanguageModel('grok-beta', {
1112
+ provider: 'xai.chat',
1113
+ baseURL: 'https://api.x.ai/v1',
1114
+ headers: () => ({
1115
+ authorization: 'Bearer test-api-key',
1116
+ 'Custom-Provider-Header': 'provider-header-value',
1117
+ }),
1118
+ generateId: () => 'test-id',
1119
+ });
1120
+
1121
+ await modelWithHeaders.doStream({
1122
+ prompt: TEST_PROMPT,
1123
+ includeRawChunks: false,
1124
+ headers: {
1125
+ 'Custom-Request-Header': 'request-header-value',
1126
+ },
1127
+ });
1128
+
1129
+ expect(server.calls[0].requestHeaders).toStrictEqual({
1130
+ authorization: 'Bearer test-api-key',
1131
+ 'content-type': 'application/json',
1132
+ 'custom-provider-header': 'provider-header-value',
1133
+ 'custom-request-header': 'request-header-value',
1134
+ });
1135
+ });
1136
+
1137
+ it('should send request body', async () => {
1138
+ prepareStreamResponse({ content: [] });
1139
+
1140
+ const { request } = await model.doStream({
1141
+ prompt: TEST_PROMPT,
1142
+ includeRawChunks: false,
1143
+ });
1144
+
1145
+ expect(request).toMatchInlineSnapshot(`
1146
+ {
1147
+ "body": {
1148
+ "max_completion_tokens": undefined,
1149
+ "messages": [
1150
+ {
1151
+ "content": "Hello",
1152
+ "role": "user",
1153
+ },
1154
+ ],
1155
+ "model": "grok-beta",
1156
+ "parallel_function_calling": undefined,
1157
+ "reasoning_effort": undefined,
1158
+ "response_format": undefined,
1159
+ "search_parameters": undefined,
1160
+ "seed": undefined,
1161
+ "stream": true,
1162
+ "stream_options": {
1163
+ "include_usage": true,
1164
+ },
1165
+ "temperature": undefined,
1166
+ "tool_choice": undefined,
1167
+ "tools": undefined,
1168
+ "top_p": undefined,
1169
+ },
1170
+ }
1171
+ `);
1172
+ });
1173
+
1174
+ it('should stream citations as sources', async () => {
1175
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1176
+ type: 'stream-chunks',
1177
+ chunks: [
1178
+ `data: {"id":"c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c","object":"chat.completion.chunk","created":1750538200,"model":"grok-beta",` +
1179
+ `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
1180
+ `data: {"id":"c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c","object":"chat.completion.chunk","created":1750538200,"model":"grok-beta",` +
1181
+ `"choices":[{"index":0,"delta":{"content":"Latest AI news"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
1182
+ `data: {"id":"c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c","object":"chat.completion.chunk","created":1750538200,"model":"grok-beta",` +
1183
+ `"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` +
1184
+ `"usage":{"prompt_tokens":4,"total_tokens":34,"completion_tokens":30},` +
1185
+ `"citations":["https://example.com/source1","https://example.com/source2"],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
1186
+ `data: [DONE]\n\n`,
1187
+ ],
1188
+ };
1189
+
1190
+ const { stream } = await model.doStream({
1191
+ prompt: TEST_PROMPT,
1192
+ includeRawChunks: false,
1193
+ providerOptions: {
1194
+ xai: {
1195
+ searchParameters: {
1196
+ mode: 'auto',
1197
+ returnCitations: true,
1198
+ },
1199
+ },
1200
+ },
1201
+ });
1202
+
1203
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1204
+ [
1205
+ {
1206
+ "type": "stream-start",
1207
+ "warnings": [],
1208
+ },
1209
+ {
1210
+ "id": "c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c",
1211
+ "modelId": "grok-beta",
1212
+ "timestamp": 2025-06-21T20:36:40.000Z,
1213
+ "type": "response-metadata",
1214
+ },
1215
+ {
1216
+ "id": "text-c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c",
1217
+ "type": "text-start",
1218
+ },
1219
+ {
1220
+ "delta": "Latest AI news",
1221
+ "id": "text-c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c",
1222
+ "type": "text-delta",
1223
+ },
1224
+ {
1225
+ "id": "test-id",
1226
+ "sourceType": "url",
1227
+ "type": "source",
1228
+ "url": "https://example.com/source1",
1229
+ },
1230
+ {
1231
+ "id": "test-id",
1232
+ "sourceType": "url",
1233
+ "type": "source",
1234
+ "url": "https://example.com/source2",
1235
+ },
1236
+ {
1237
+ "id": "text-c8e45f92-7a3b-4d8e-9c1f-5e6a8b9d2f4c",
1238
+ "type": "text-end",
1239
+ },
1240
+ {
1241
+ "finishReason": {
1242
+ "raw": "stop",
1243
+ "unified": "stop",
1244
+ },
1245
+ "type": "finish",
1246
+ "usage": {
1247
+ "inputTokens": {
1248
+ "cacheRead": 0,
1249
+ "cacheWrite": undefined,
1250
+ "noCache": 4,
1251
+ "total": 4,
1252
+ },
1253
+ "outputTokens": {
1254
+ "reasoning": 0,
1255
+ "text": 30,
1256
+ "total": 30,
1257
+ },
1258
+ "raw": {
1259
+ "completion_tokens": 30,
1260
+ "prompt_tokens": 4,
1261
+ "total_tokens": 34,
1262
+ },
1263
+ },
1264
+ },
1265
+ ]
1266
+ `);
1267
+ });
1268
+ });
1269
+
1270
+ describe('reasoning models', () => {
1271
+ const reasoningModel = new XaiChatLanguageModel('grok-3-mini', testConfig);
1272
+
1273
+ function prepareReasoningResponse({
1274
+ content = 'The result is 303.',
1275
+ reasoning_content = 'Let me calculate 101 multiplied by 3: 101 * 3 = 303.',
1276
+ usage = {
1277
+ prompt_tokens: 15,
1278
+ total_tokens: 35,
1279
+ completion_tokens: 20,
1280
+ completion_tokens_details: {
1281
+ reasoning_tokens: 10,
1282
+ },
1283
+ },
1284
+ }: {
1285
+ content?: string;
1286
+ reasoning_content?: string;
1287
+ usage?: {
1288
+ prompt_tokens: number;
1289
+ total_tokens: number;
1290
+ completion_tokens: number;
1291
+ completion_tokens_details?: {
1292
+ reasoning_tokens?: number;
1293
+ };
1294
+ };
1295
+ }) {
1296
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1297
+ type: 'json-value',
1298
+ body: {
1299
+ id: 'chatcmpl-reasoning-test',
1300
+ object: 'chat.completion',
1301
+ created: 1699472111,
1302
+ model: 'grok-3-mini',
1303
+ choices: [
1304
+ {
1305
+ index: 0,
1306
+ message: {
1307
+ role: 'assistant',
1308
+ content,
1309
+ reasoning_content,
1310
+ tool_calls: null,
1311
+ },
1312
+ finish_reason: 'stop',
1313
+ },
1314
+ ],
1315
+ usage,
1316
+ },
1317
+ };
1318
+ }
1319
+
1320
+ it('should pass reasoning_effort parameter', async () => {
1321
+ prepareReasoningResponse({});
1322
+
1323
+ await reasoningModel.doGenerate({
1324
+ prompt: TEST_PROMPT,
1325
+ providerOptions: {
1326
+ xai: { reasoningEffort: 'high' },
1327
+ },
1328
+ });
1329
+
1330
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1331
+ model: 'grok-3-mini',
1332
+ messages: [{ role: 'user', content: 'Hello' }],
1333
+ reasoning_effort: 'high',
1334
+ });
1335
+ });
1336
+
1337
+ it('should extract reasoning content', async () => {
1338
+ prepareReasoningResponse({
1339
+ content: 'The answer is 303.',
1340
+ reasoning_content: 'Let me think: 101 * 3 = 303.',
1341
+ });
1342
+
1343
+ const { content } = await reasoningModel.doGenerate({
1344
+ prompt: TEST_PROMPT,
1345
+ providerOptions: {
1346
+ xai: { reasoningEffort: 'low' },
1347
+ },
1348
+ });
1349
+
1350
+ expect(content).toMatchInlineSnapshot(`
1351
+ [
1352
+ {
1353
+ "text": "The answer is 303.",
1354
+ "type": "text",
1355
+ },
1356
+ {
1357
+ "text": "Let me think: 101 * 3 = 303.",
1358
+ "type": "reasoning",
1359
+ },
1360
+ ]
1361
+ `);
1362
+ });
1363
+
1364
+ it('should extract reasoning tokens from usage', async () => {
1365
+ prepareReasoningResponse({
1366
+ usage: {
1367
+ prompt_tokens: 15,
1368
+ completion_tokens: 20,
1369
+ total_tokens: 35,
1370
+ completion_tokens_details: {
1371
+ reasoning_tokens: 10,
1372
+ },
1373
+ },
1374
+ });
1375
+
1376
+ const { usage } = await reasoningModel.doGenerate({
1377
+ prompt: TEST_PROMPT,
1378
+ providerOptions: {
1379
+ xai: { reasoningEffort: 'high' },
1380
+ },
1381
+ });
1382
+
1383
+ expect(usage).toMatchInlineSnapshot(`
1384
+ {
1385
+ "inputTokens": {
1386
+ "cacheRead": 0,
1387
+ "cacheWrite": undefined,
1388
+ "noCache": 15,
1389
+ "total": 15,
1390
+ },
1391
+ "outputTokens": {
1392
+ "reasoning": 10,
1393
+ "text": 10,
1394
+ "total": 20,
1395
+ },
1396
+ "raw": {
1397
+ "completion_tokens": 20,
1398
+ "completion_tokens_details": {
1399
+ "reasoning_tokens": 10,
1400
+ },
1401
+ "prompt_tokens": 15,
1402
+ "total_tokens": 35,
1403
+ },
1404
+ }
1405
+ `);
1406
+ });
1407
+
1408
+ it('should handle reasoning streaming', async () => {
1409
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1410
+ type: 'stream-chunks',
1411
+ chunks: [
1412
+ `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` +
1413
+ `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1414
+ `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` +
1415
+ `"choices":[{"index":0,"delta":{"reasoning_content":"Let me calculate: "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1416
+ `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` +
1417
+ `"choices":[{"index":0,"delta":{"reasoning_content":"101 * 3 = 303"},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1418
+ `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` +
1419
+ `"choices":[{"index":0,"delta":{"content":"The answer is 303."},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1420
+ `data: {"id":"b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b","object":"chat.completion.chunk","created":1750538120,"model":"grok-3-mini",` +
1421
+ `"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` +
1422
+ `"usage":{"prompt_tokens":15,"total_tokens":35,"completion_tokens":20,"completion_tokens_details":{"reasoning_tokens":10}},"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1423
+ `data: [DONE]\n\n`,
1424
+ ],
1425
+ };
1426
+
1427
+ const { stream } = await reasoningModel.doStream({
1428
+ prompt: TEST_PROMPT,
1429
+ includeRawChunks: false,
1430
+ providerOptions: {
1431
+ xai: { reasoningEffort: 'low' },
1432
+ },
1433
+ });
1434
+
1435
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1436
+ [
1437
+ {
1438
+ "type": "stream-start",
1439
+ "warnings": [],
1440
+ },
1441
+ {
1442
+ "id": "b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1443
+ "modelId": "grok-3-mini",
1444
+ "timestamp": 2025-06-21T20:35:20.000Z,
1445
+ "type": "response-metadata",
1446
+ },
1447
+ {
1448
+ "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1449
+ "type": "reasoning-start",
1450
+ },
1451
+ {
1452
+ "delta": "Let me calculate: ",
1453
+ "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1454
+ "type": "reasoning-delta",
1455
+ },
1456
+ {
1457
+ "delta": "101 * 3 = 303",
1458
+ "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1459
+ "type": "reasoning-delta",
1460
+ },
1461
+ {
1462
+ "id": "reasoning-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1463
+ "type": "reasoning-end",
1464
+ },
1465
+ {
1466
+ "id": "text-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1467
+ "type": "text-start",
1468
+ },
1469
+ {
1470
+ "delta": "The answer is 303.",
1471
+ "id": "text-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1472
+ "type": "text-delta",
1473
+ },
1474
+ {
1475
+ "id": "text-b7f32e89-8d6c-4a1e-9f5b-2c8e7a9d4f6b",
1476
+ "type": "text-end",
1477
+ },
1478
+ {
1479
+ "finishReason": {
1480
+ "raw": "stop",
1481
+ "unified": "stop",
1482
+ },
1483
+ "type": "finish",
1484
+ "usage": {
1485
+ "inputTokens": {
1486
+ "cacheRead": 0,
1487
+ "cacheWrite": undefined,
1488
+ "noCache": 15,
1489
+ "total": 15,
1490
+ },
1491
+ "outputTokens": {
1492
+ "reasoning": 10,
1493
+ "text": 10,
1494
+ "total": 20,
1495
+ },
1496
+ "raw": {
1497
+ "completion_tokens": 20,
1498
+ "completion_tokens_details": {
1499
+ "reasoning_tokens": 10,
1500
+ },
1501
+ "prompt_tokens": 15,
1502
+ "total_tokens": 35,
1503
+ },
1504
+ },
1505
+ },
1506
+ ]
1507
+ `);
1508
+ });
1509
+
1510
+ it('should deduplicate repetitive reasoning deltas', async () => {
1511
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1512
+ type: 'stream-chunks',
1513
+ chunks: [
1514
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1515
+ `"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1516
+ // Multiple identical "Thinking..." deltas (simulating Grok 4 issue)
1517
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1518
+ `"choices":[{"index":0,"delta":{"reasoning_content":"Thinking... "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1519
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1520
+ `"choices":[{"index":0,"delta":{"reasoning_content":"Thinking... "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1521
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1522
+ `"choices":[{"index":0,"delta":{"reasoning_content":"Thinking... "},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1523
+ // Different reasoning content should still come through
1524
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1525
+ `"choices":[{"index":0,"delta":{"reasoning_content":"Actually calculating now..."},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1526
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1527
+ `"choices":[{"index":0,"delta":{"content":"The answer is 42."},"finish_reason":null}],"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1528
+ `data: {"id":"grok-4-test","object":"chat.completion.chunk","created":1750538120,"model":"grok-4-0709",` +
1529
+ `"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],` +
1530
+ `"usage":{"prompt_tokens":15,"total_tokens":35,"completion_tokens":20,"completion_tokens_details":{"reasoning_tokens":10}},"system_fingerprint":"fp_reasoning_v1"}\n\n`,
1531
+ `data: [DONE]\n\n`,
1532
+ ],
1533
+ };
1534
+
1535
+ const { stream } = await reasoningModel.doStream({
1536
+ prompt: TEST_PROMPT,
1537
+ includeRawChunks: false,
1538
+ providerOptions: {
1539
+ xai: { reasoningEffort: 'low' },
1540
+ },
1541
+ });
1542
+
1543
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1544
+ [
1545
+ {
1546
+ "type": "stream-start",
1547
+ "warnings": [],
1548
+ },
1549
+ {
1550
+ "id": "grok-4-test",
1551
+ "modelId": "grok-4-0709",
1552
+ "timestamp": 2025-06-21T20:35:20.000Z,
1553
+ "type": "response-metadata",
1554
+ },
1555
+ {
1556
+ "id": "reasoning-grok-4-test",
1557
+ "type": "reasoning-start",
1558
+ },
1559
+ {
1560
+ "delta": "Thinking... ",
1561
+ "id": "reasoning-grok-4-test",
1562
+ "type": "reasoning-delta",
1563
+ },
1564
+ {
1565
+ "delta": "Actually calculating now...",
1566
+ "id": "reasoning-grok-4-test",
1567
+ "type": "reasoning-delta",
1568
+ },
1569
+ {
1570
+ "id": "reasoning-grok-4-test",
1571
+ "type": "reasoning-end",
1572
+ },
1573
+ {
1574
+ "id": "text-grok-4-test",
1575
+ "type": "text-start",
1576
+ },
1577
+ {
1578
+ "delta": "The answer is 42.",
1579
+ "id": "text-grok-4-test",
1580
+ "type": "text-delta",
1581
+ },
1582
+ {
1583
+ "id": "text-grok-4-test",
1584
+ "type": "text-end",
1585
+ },
1586
+ {
1587
+ "finishReason": {
1588
+ "raw": "stop",
1589
+ "unified": "stop",
1590
+ },
1591
+ "type": "finish",
1592
+ "usage": {
1593
+ "inputTokens": {
1594
+ "cacheRead": 0,
1595
+ "cacheWrite": undefined,
1596
+ "noCache": 15,
1597
+ "total": 15,
1598
+ },
1599
+ "outputTokens": {
1600
+ "reasoning": 10,
1601
+ "text": 10,
1602
+ "total": 20,
1603
+ },
1604
+ "raw": {
1605
+ "completion_tokens": 20,
1606
+ "completion_tokens_details": {
1607
+ "reasoning_tokens": 10,
1608
+ },
1609
+ "prompt_tokens": 15,
1610
+ "total_tokens": 35,
1611
+ },
1612
+ },
1613
+ },
1614
+ ]
1615
+ `);
1616
+ });
1617
+ });
1618
+ });
1619
+
1620
+ describe('doStream with raw chunks', () => {
1621
+ it('should stream raw chunks when includeRawChunks is true', async () => {
1622
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1623
+ type: 'stream-chunks',
1624
+ chunks: [
1625
+ `data: {"id":"d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d","object":"chat.completion.chunk","created":1750538300,"model":"grok-beta","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
1626
+ `data: {"id":"e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b","object":"chat.completion.chunk","created":1750538301,"model":"grok-beta","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
1627
+ `data: {"id":"f3b58c9a-4e7f-5d9e-ab2c-8e6f9d0e3b5c","object":"chat.completion.chunk","created":1750538302,"model":"grok-beta","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15},"citations":["https://example.com"],"system_fingerprint":"fp_13a6dc65a6"}\n\n`,
1628
+ 'data: [DONE]\n\n',
1629
+ ],
1630
+ };
1631
+
1632
+ const { stream } = await model.doStream({
1633
+ prompt: TEST_PROMPT,
1634
+ includeRawChunks: true,
1635
+ });
1636
+
1637
+ const chunks = await convertReadableStreamToArray(stream);
1638
+
1639
+ expect(chunks).toMatchInlineSnapshot(`
1640
+ [
1641
+ {
1642
+ "type": "stream-start",
1643
+ "warnings": [],
1644
+ },
1645
+ {
1646
+ "rawValue": {
1647
+ "choices": [
1648
+ {
1649
+ "delta": {
1650
+ "content": "Hello",
1651
+ "role": "assistant",
1652
+ },
1653
+ "finish_reason": null,
1654
+ "index": 0,
1655
+ },
1656
+ ],
1657
+ "created": 1750538300,
1658
+ "id": "d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d",
1659
+ "model": "grok-beta",
1660
+ "object": "chat.completion.chunk",
1661
+ "system_fingerprint": "fp_13a6dc65a6",
1662
+ },
1663
+ "type": "raw",
1664
+ },
1665
+ {
1666
+ "id": "d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d",
1667
+ "modelId": "grok-beta",
1668
+ "timestamp": 2025-06-21T20:38:20.000Z,
1669
+ "type": "response-metadata",
1670
+ },
1671
+ {
1672
+ "id": "text-d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d",
1673
+ "type": "text-start",
1674
+ },
1675
+ {
1676
+ "delta": "Hello",
1677
+ "id": "text-d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d",
1678
+ "type": "text-delta",
1679
+ },
1680
+ {
1681
+ "rawValue": {
1682
+ "choices": [
1683
+ {
1684
+ "delta": {
1685
+ "content": " world",
1686
+ },
1687
+ "finish_reason": null,
1688
+ "index": 0,
1689
+ },
1690
+ ],
1691
+ "created": 1750538301,
1692
+ "id": "e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b",
1693
+ "model": "grok-beta",
1694
+ "object": "chat.completion.chunk",
1695
+ "system_fingerprint": "fp_13a6dc65a6",
1696
+ },
1697
+ "type": "raw",
1698
+ },
1699
+ {
1700
+ "id": "text-e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b",
1701
+ "type": "text-start",
1702
+ },
1703
+ {
1704
+ "delta": " world",
1705
+ "id": "text-e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b",
1706
+ "type": "text-delta",
1707
+ },
1708
+ {
1709
+ "rawValue": {
1710
+ "choices": [
1711
+ {
1712
+ "delta": {},
1713
+ "finish_reason": "stop",
1714
+ "index": 0,
1715
+ },
1716
+ ],
1717
+ "citations": [
1718
+ "https://example.com",
1719
+ ],
1720
+ "created": 1750538302,
1721
+ "id": "f3b58c9a-4e7f-5d9e-ab2c-8e6f9d0e3b5c",
1722
+ "model": "grok-beta",
1723
+ "object": "chat.completion.chunk",
1724
+ "system_fingerprint": "fp_13a6dc65a6",
1725
+ "usage": {
1726
+ "completion_tokens": 5,
1727
+ "prompt_tokens": 10,
1728
+ "total_tokens": 15,
1729
+ },
1730
+ },
1731
+ "type": "raw",
1732
+ },
1733
+ {
1734
+ "id": "test-id",
1735
+ "sourceType": "url",
1736
+ "type": "source",
1737
+ "url": "https://example.com",
1738
+ },
1739
+ {
1740
+ "id": "text-d9f56e23-8b4c-4e7a-9d2f-6c8a9b5e3f7d",
1741
+ "type": "text-end",
1742
+ },
1743
+ {
1744
+ "id": "text-e2a47b89-3f6d-4c8e-9a1b-7d5f8c9e2a4b",
1745
+ "type": "text-end",
1746
+ },
1747
+ {
1748
+ "finishReason": {
1749
+ "raw": "stop",
1750
+ "unified": "stop",
1751
+ },
1752
+ "type": "finish",
1753
+ "usage": {
1754
+ "inputTokens": {
1755
+ "cacheRead": 0,
1756
+ "cacheWrite": undefined,
1757
+ "noCache": 10,
1758
+ "total": 10,
1759
+ },
1760
+ "outputTokens": {
1761
+ "reasoning": 0,
1762
+ "text": 5,
1763
+ "total": 5,
1764
+ },
1765
+ "raw": {
1766
+ "completion_tokens": 5,
1767
+ "prompt_tokens": 10,
1768
+ "total_tokens": 15,
1769
+ },
1770
+ },
1771
+ },
1772
+ ]
1773
+ `);
1774
+ });
1775
+
1776
+ describe('error handling', () => {
1777
+ it('should throw APICallError when xai returns error with 200 status (doGenerate)', async () => {
1778
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1779
+ type: 'json-value',
1780
+ body: {
1781
+ code: 'The service is currently unavailable',
1782
+ error: 'Timed out waiting for first token',
1783
+ },
1784
+ };
1785
+
1786
+ await expect(model.doGenerate({ prompt: TEST_PROMPT })).rejects.toThrow(
1787
+ 'Timed out waiting for first token',
1788
+ );
1789
+ });
1790
+
1791
+ it('should throw APICallError when xai returns error with 200 status (doStream)', async () => {
1792
+ server.urls['https://api.x.ai/v1/chat/completions'].response = {
1793
+ type: 'json-value',
1794
+ body: {
1795
+ code: 'The service is currently unavailable',
1796
+ error: 'Timed out waiting for first token',
1797
+ },
1798
+ };
1799
+
1800
+ await expect(model.doStream({ prompt: TEST_PROMPT })).rejects.toThrow(
1801
+ 'Timed out waiting for first token',
1802
+ );
1803
+ });
1804
+ });
1805
+ });