@ai-sdk/openai 3.0.14 → 3.0.15

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 (110) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.mjs +1 -1
  4. package/package.json +6 -5
  5. package/src/chat/__fixtures__/azure-model-router.1.chunks.txt +8 -0
  6. package/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap +88 -0
  7. package/src/chat/convert-openai-chat-usage.ts +57 -0
  8. package/src/chat/convert-to-openai-chat-messages.test.ts +516 -0
  9. package/src/chat/convert-to-openai-chat-messages.ts +225 -0
  10. package/src/chat/get-response-metadata.ts +15 -0
  11. package/src/chat/map-openai-finish-reason.ts +19 -0
  12. package/src/chat/openai-chat-api.ts +198 -0
  13. package/src/chat/openai-chat-language-model.test.ts +3496 -0
  14. package/src/chat/openai-chat-language-model.ts +700 -0
  15. package/src/chat/openai-chat-options.ts +186 -0
  16. package/src/chat/openai-chat-prepare-tools.test.ts +322 -0
  17. package/src/chat/openai-chat-prepare-tools.ts +84 -0
  18. package/src/chat/openai-chat-prompt.ts +70 -0
  19. package/src/completion/convert-openai-completion-usage.ts +46 -0
  20. package/src/completion/convert-to-openai-completion-prompt.ts +93 -0
  21. package/src/completion/get-response-metadata.ts +15 -0
  22. package/src/completion/map-openai-finish-reason.ts +19 -0
  23. package/src/completion/openai-completion-api.ts +81 -0
  24. package/src/completion/openai-completion-language-model.test.ts +752 -0
  25. package/src/completion/openai-completion-language-model.ts +336 -0
  26. package/src/completion/openai-completion-options.ts +58 -0
  27. package/src/embedding/__snapshots__/openai-embedding-model.test.ts.snap +43 -0
  28. package/src/embedding/openai-embedding-api.ts +13 -0
  29. package/src/embedding/openai-embedding-model.test.ts +146 -0
  30. package/src/embedding/openai-embedding-model.ts +95 -0
  31. package/src/embedding/openai-embedding-options.ts +30 -0
  32. package/src/image/openai-image-api.ts +35 -0
  33. package/src/image/openai-image-model.test.ts +722 -0
  34. package/src/image/openai-image-model.ts +305 -0
  35. package/src/image/openai-image-options.ts +28 -0
  36. package/src/index.ts +9 -0
  37. package/src/internal/index.ts +19 -0
  38. package/src/openai-config.ts +18 -0
  39. package/src/openai-error.test.ts +34 -0
  40. package/src/openai-error.ts +22 -0
  41. package/src/openai-language-model-capabilities.test.ts +93 -0
  42. package/src/openai-language-model-capabilities.ts +54 -0
  43. package/src/openai-provider.test.ts +98 -0
  44. package/src/openai-provider.ts +270 -0
  45. package/src/openai-tools.ts +114 -0
  46. package/src/responses/__fixtures__/openai-apply-patch-tool-delete.1.chunks.txt +5 -0
  47. package/src/responses/__fixtures__/openai-apply-patch-tool.1.chunks.txt +38 -0
  48. package/src/responses/__fixtures__/openai-apply-patch-tool.1.json +69 -0
  49. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.chunks.txt +393 -0
  50. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.json +137 -0
  51. package/src/responses/__fixtures__/openai-error.1.chunks.txt +4 -0
  52. package/src/responses/__fixtures__/openai-error.1.json +8 -0
  53. package/src/responses/__fixtures__/openai-file-search-tool.1.chunks.txt +94 -0
  54. package/src/responses/__fixtures__/openai-file-search-tool.1.json +89 -0
  55. package/src/responses/__fixtures__/openai-file-search-tool.2.chunks.txt +93 -0
  56. package/src/responses/__fixtures__/openai-file-search-tool.2.json +112 -0
  57. package/src/responses/__fixtures__/openai-image-generation-tool.1.chunks.txt +16 -0
  58. package/src/responses/__fixtures__/openai-image-generation-tool.1.json +96 -0
  59. package/src/responses/__fixtures__/openai-local-shell-tool.1.chunks.txt +7 -0
  60. package/src/responses/__fixtures__/openai-local-shell-tool.1.json +70 -0
  61. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.chunks.txt +11 -0
  62. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.json +169 -0
  63. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.chunks.txt +123 -0
  64. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.json +176 -0
  65. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.chunks.txt +11 -0
  66. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.json +169 -0
  67. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.chunks.txt +84 -0
  68. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.json +182 -0
  69. package/src/responses/__fixtures__/openai-mcp-tool.1.chunks.txt +373 -0
  70. package/src/responses/__fixtures__/openai-mcp-tool.1.json +159 -0
  71. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.chunks.txt +110 -0
  72. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.json +117 -0
  73. package/src/responses/__fixtures__/openai-shell-tool.1.chunks.txt +182 -0
  74. package/src/responses/__fixtures__/openai-shell-tool.1.json +73 -0
  75. package/src/responses/__fixtures__/openai-web-search-tool.1.chunks.txt +185 -0
  76. package/src/responses/__fixtures__/openai-web-search-tool.1.json +266 -0
  77. package/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap +10955 -0
  78. package/src/responses/convert-openai-responses-usage.ts +53 -0
  79. package/src/responses/convert-to-openai-responses-input.test.ts +2976 -0
  80. package/src/responses/convert-to-openai-responses-input.ts +578 -0
  81. package/src/responses/map-openai-responses-finish-reason.ts +22 -0
  82. package/src/responses/openai-responses-api.test.ts +89 -0
  83. package/src/responses/openai-responses-api.ts +1086 -0
  84. package/src/responses/openai-responses-language-model.test.ts +6927 -0
  85. package/src/responses/openai-responses-language-model.ts +1932 -0
  86. package/src/responses/openai-responses-options.ts +312 -0
  87. package/src/responses/openai-responses-prepare-tools.test.ts +924 -0
  88. package/src/responses/openai-responses-prepare-tools.ts +264 -0
  89. package/src/responses/openai-responses-provider-metadata.ts +39 -0
  90. package/src/speech/openai-speech-api.ts +38 -0
  91. package/src/speech/openai-speech-model.test.ts +202 -0
  92. package/src/speech/openai-speech-model.ts +137 -0
  93. package/src/speech/openai-speech-options.ts +22 -0
  94. package/src/tool/apply-patch.ts +141 -0
  95. package/src/tool/code-interpreter.ts +104 -0
  96. package/src/tool/file-search.ts +145 -0
  97. package/src/tool/image-generation.ts +126 -0
  98. package/src/tool/local-shell.test-d.ts +20 -0
  99. package/src/tool/local-shell.ts +72 -0
  100. package/src/tool/mcp.ts +125 -0
  101. package/src/tool/shell.ts +85 -0
  102. package/src/tool/web-search-preview.ts +139 -0
  103. package/src/tool/web-search.test-d.ts +13 -0
  104. package/src/tool/web-search.ts +179 -0
  105. package/src/transcription/openai-transcription-api.ts +37 -0
  106. package/src/transcription/openai-transcription-model.test.ts +507 -0
  107. package/src/transcription/openai-transcription-model.ts +232 -0
  108. package/src/transcription/openai-transcription-options.ts +50 -0
  109. package/src/transcription/transcription-test.mp3 +0 -0
  110. package/src/version.ts +6 -0
@@ -0,0 +1,3496 @@
1
+ import fs from 'node:fs';
2
+
3
+ import { LanguageModelV3Prompt } from '@ai-sdk/provider';
4
+ import { createTestServer } from '@ai-sdk/test-server/with-vitest';
5
+ import {
6
+ convertReadableStreamToArray,
7
+ isNodeVersion,
8
+ } from '@ai-sdk/provider-utils/test';
9
+ import { createOpenAI } from '../openai-provider';
10
+ import { describe, it, expect, vi } from 'vitest';
11
+
12
+ vi.mock('../version', () => ({
13
+ VERSION: '0.0.0-test',
14
+ }));
15
+
16
+ const TEST_PROMPT: LanguageModelV3Prompt = [
17
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
18
+ ];
19
+
20
+ const TEST_LOGPROBS = {
21
+ content: [
22
+ {
23
+ token: 'Hello',
24
+ logprob: -0.0009994634,
25
+ top_logprobs: [
26
+ {
27
+ token: 'Hello',
28
+ logprob: -0.0009994634,
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ token: '!',
34
+ logprob: -0.13410144,
35
+ top_logprobs: [
36
+ {
37
+ token: '!',
38
+ logprob: -0.13410144,
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ token: ' How',
44
+ logprob: -0.0009250381,
45
+ top_logprobs: [
46
+ {
47
+ token: ' How',
48
+ logprob: -0.0009250381,
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ token: ' can',
54
+ logprob: -0.047709424,
55
+ top_logprobs: [
56
+ {
57
+ token: ' can',
58
+ logprob: -0.047709424,
59
+ },
60
+ ],
61
+ },
62
+ {
63
+ token: ' I',
64
+ logprob: -0.000009014684,
65
+ top_logprobs: [
66
+ {
67
+ token: ' I',
68
+ logprob: -0.000009014684,
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ token: ' assist',
74
+ logprob: -0.009125131,
75
+ top_logprobs: [
76
+ {
77
+ token: ' assist',
78
+ logprob: -0.009125131,
79
+ },
80
+ ],
81
+ },
82
+ {
83
+ token: ' you',
84
+ logprob: -0.0000066306106,
85
+ top_logprobs: [
86
+ {
87
+ token: ' you',
88
+ logprob: -0.0000066306106,
89
+ },
90
+ ],
91
+ },
92
+ {
93
+ token: ' today',
94
+ logprob: -0.00011093382,
95
+ top_logprobs: [
96
+ {
97
+ token: ' today',
98
+ logprob: -0.00011093382,
99
+ },
100
+ ],
101
+ },
102
+ {
103
+ token: '?',
104
+ logprob: -0.00004596782,
105
+ top_logprobs: [
106
+ {
107
+ token: '?',
108
+ logprob: -0.00004596782,
109
+ },
110
+ ],
111
+ },
112
+ ],
113
+ };
114
+
115
+ const provider = createOpenAI({
116
+ apiKey: 'test-api-key',
117
+ });
118
+
119
+ const model = provider.chat('gpt-3.5-turbo');
120
+
121
+ const server = createTestServer({
122
+ 'https://api.openai.com/v1/chat/completions': {},
123
+ });
124
+
125
+ function prepareChunksFixtureResponse(filename: string) {
126
+ const chunks = fs
127
+ .readFileSync(`src/chat/__fixtures__/${filename}.chunks.txt`, 'utf8')
128
+ .split('\n')
129
+ .map(line => `data: ${line}\n\n`);
130
+ chunks.push('data: [DONE]\n\n');
131
+
132
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
133
+ type: 'stream-chunks',
134
+ chunks,
135
+ };
136
+ }
137
+
138
+ describe('doGenerate', () => {
139
+ function prepareJsonResponse({
140
+ content = '',
141
+ tool_calls,
142
+ function_call,
143
+ annotations,
144
+ usage = {
145
+ prompt_tokens: 4,
146
+ total_tokens: 34,
147
+ completion_tokens: 30,
148
+ },
149
+ finish_reason = 'stop',
150
+ id = 'chatcmpl-95ZTZkhr0mHNKqerQfiwkuox3PHAd',
151
+ created = 1711115037,
152
+ model = 'gpt-3.5-turbo-0125',
153
+ logprobs = null,
154
+ headers,
155
+ }: {
156
+ content?: string;
157
+ tool_calls?: Array<{
158
+ id: string;
159
+ type: 'function';
160
+ function: {
161
+ name: string;
162
+ arguments: string;
163
+ };
164
+ }>;
165
+ function_call?: {
166
+ name: string;
167
+ arguments: string;
168
+ };
169
+ annotations?: Array<{
170
+ type: 'url_citation';
171
+ url_citation: {
172
+ start_index: number;
173
+ end_index: number;
174
+ url: string;
175
+ title: string;
176
+ };
177
+ }>;
178
+ logprobs?: {
179
+ content:
180
+ | {
181
+ token: string;
182
+ logprob: number;
183
+ top_logprobs: { token: string; logprob: number }[];
184
+ }[]
185
+ | null;
186
+ } | null;
187
+ usage?: {
188
+ prompt_tokens?: number;
189
+ total_tokens?: number;
190
+ completion_tokens?: number;
191
+ completion_tokens_details?: {
192
+ reasoning_tokens?: number;
193
+ accepted_prediction_tokens?: number;
194
+ rejected_prediction_tokens?: number;
195
+ };
196
+ prompt_tokens_details?: {
197
+ cached_tokens?: number;
198
+ };
199
+ };
200
+ finish_reason?: string;
201
+ created?: number;
202
+ id?: string;
203
+ model?: string;
204
+ headers?: Record<string, string>;
205
+ } = {}) {
206
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
207
+ type: 'json-value',
208
+ headers,
209
+ body: {
210
+ id,
211
+ object: 'chat.completion',
212
+ created,
213
+ model,
214
+ choices: [
215
+ {
216
+ index: 0,
217
+ message: {
218
+ role: 'assistant',
219
+ content,
220
+ tool_calls,
221
+ function_call,
222
+ annotations,
223
+ },
224
+ ...(logprobs ? { logprobs } : {}),
225
+ finish_reason,
226
+ },
227
+ ],
228
+ usage,
229
+ system_fingerprint: 'fp_3bc1b5746c',
230
+ },
231
+ };
232
+ }
233
+
234
+ it('should extract text response', async () => {
235
+ prepareJsonResponse({ content: 'Hello, World!' });
236
+
237
+ const result = await model.doGenerate({
238
+ prompt: TEST_PROMPT,
239
+ });
240
+
241
+ expect(result.content).toMatchInlineSnapshot(`
242
+ [
243
+ {
244
+ "text": "Hello, World!",
245
+ "type": "text",
246
+ },
247
+ ]
248
+ `);
249
+ });
250
+
251
+ it('should extract usage', async () => {
252
+ prepareJsonResponse({
253
+ usage: { prompt_tokens: 20, total_tokens: 25, completion_tokens: 5 },
254
+ });
255
+
256
+ const { usage } = await model.doGenerate({
257
+ prompt: TEST_PROMPT,
258
+ });
259
+
260
+ expect(usage).toMatchInlineSnapshot(`
261
+ {
262
+ "inputTokens": {
263
+ "cacheRead": 0,
264
+ "cacheWrite": undefined,
265
+ "noCache": 20,
266
+ "total": 20,
267
+ },
268
+ "outputTokens": {
269
+ "reasoning": 0,
270
+ "text": 5,
271
+ "total": 5,
272
+ },
273
+ "raw": {
274
+ "completion_tokens": 5,
275
+ "prompt_tokens": 20,
276
+ "total_tokens": 25,
277
+ },
278
+ }
279
+ `);
280
+ });
281
+
282
+ it('should send request body', async () => {
283
+ prepareJsonResponse({});
284
+
285
+ const { request } = await model.doGenerate({
286
+ prompt: TEST_PROMPT,
287
+ });
288
+
289
+ expect(request).toMatchInlineSnapshot(`
290
+ {
291
+ "body": {
292
+ "frequency_penalty": undefined,
293
+ "logit_bias": undefined,
294
+ "logprobs": undefined,
295
+ "max_completion_tokens": undefined,
296
+ "max_tokens": undefined,
297
+ "messages": [
298
+ {
299
+ "content": "Hello",
300
+ "role": "user",
301
+ },
302
+ ],
303
+ "metadata": undefined,
304
+ "model": "gpt-3.5-turbo",
305
+ "parallel_tool_calls": undefined,
306
+ "prediction": undefined,
307
+ "presence_penalty": undefined,
308
+ "prompt_cache_key": undefined,
309
+ "prompt_cache_retention": undefined,
310
+ "reasoning_effort": undefined,
311
+ "response_format": undefined,
312
+ "safety_identifier": undefined,
313
+ "seed": undefined,
314
+ "service_tier": undefined,
315
+ "stop": undefined,
316
+ "store": undefined,
317
+ "temperature": undefined,
318
+ "tool_choice": undefined,
319
+ "tools": undefined,
320
+ "top_logprobs": undefined,
321
+ "top_p": undefined,
322
+ "user": undefined,
323
+ "verbosity": undefined,
324
+ },
325
+ }
326
+ `);
327
+ });
328
+
329
+ it('should send additional response information', async () => {
330
+ prepareJsonResponse({
331
+ id: 'test-id',
332
+ created: 123,
333
+ model: 'test-model',
334
+ });
335
+
336
+ const { response } = await model.doGenerate({
337
+ prompt: TEST_PROMPT,
338
+ });
339
+
340
+ expect(response).toMatchInlineSnapshot(`
341
+ {
342
+ "body": {
343
+ "choices": [
344
+ {
345
+ "finish_reason": "stop",
346
+ "index": 0,
347
+ "message": {
348
+ "content": "",
349
+ "role": "assistant",
350
+ },
351
+ },
352
+ ],
353
+ "created": 123,
354
+ "id": "test-id",
355
+ "model": "test-model",
356
+ "object": "chat.completion",
357
+ "system_fingerprint": "fp_3bc1b5746c",
358
+ "usage": {
359
+ "completion_tokens": 30,
360
+ "prompt_tokens": 4,
361
+ "total_tokens": 34,
362
+ },
363
+ },
364
+ "headers": {
365
+ "content-length": "275",
366
+ "content-type": "application/json",
367
+ },
368
+ "id": "test-id",
369
+ "modelId": "test-model",
370
+ "timestamp": 1970-01-01T00:02:03.000Z,
371
+ }
372
+ `);
373
+ });
374
+
375
+ it('should support partial usage', async () => {
376
+ prepareJsonResponse({
377
+ usage: { prompt_tokens: 20, total_tokens: 20 },
378
+ });
379
+
380
+ const { usage } = await model.doGenerate({
381
+ prompt: TEST_PROMPT,
382
+ });
383
+
384
+ expect(usage).toMatchInlineSnapshot(`
385
+ {
386
+ "inputTokens": {
387
+ "cacheRead": 0,
388
+ "cacheWrite": undefined,
389
+ "noCache": 20,
390
+ "total": 20,
391
+ },
392
+ "outputTokens": {
393
+ "reasoning": 0,
394
+ "text": 0,
395
+ "total": 0,
396
+ },
397
+ "raw": {
398
+ "prompt_tokens": 20,
399
+ "total_tokens": 20,
400
+ },
401
+ }
402
+ `);
403
+ });
404
+
405
+ it('should extract logprobs', async () => {
406
+ prepareJsonResponse({
407
+ logprobs: TEST_LOGPROBS,
408
+ });
409
+
410
+ const response = await provider.chat('gpt-3.5-turbo').doGenerate({
411
+ prompt: TEST_PROMPT,
412
+ providerOptions: {
413
+ openai: {
414
+ logprobs: 1,
415
+ },
416
+ },
417
+ });
418
+ expect(response.providerMetadata?.openai.logprobs).toStrictEqual(
419
+ TEST_LOGPROBS.content,
420
+ );
421
+ });
422
+
423
+ it('should extract finish reason', async () => {
424
+ prepareJsonResponse({
425
+ finish_reason: 'stop',
426
+ });
427
+
428
+ const response = await model.doGenerate({
429
+ prompt: TEST_PROMPT,
430
+ });
431
+
432
+ expect(response.finishReason).toMatchInlineSnapshot(`
433
+ {
434
+ "raw": "stop",
435
+ "unified": "stop",
436
+ }
437
+ `);
438
+ });
439
+
440
+ it('should support unknown finish reason', async () => {
441
+ prepareJsonResponse({
442
+ finish_reason: 'eos',
443
+ });
444
+
445
+ const response = await model.doGenerate({
446
+ prompt: TEST_PROMPT,
447
+ });
448
+
449
+ expect(response.finishReason).toMatchInlineSnapshot(`
450
+ {
451
+ "raw": "eos",
452
+ "unified": "other",
453
+ }
454
+ `);
455
+ });
456
+
457
+ it('should expose the raw response headers', async () => {
458
+ prepareJsonResponse({
459
+ headers: { 'test-header': 'test-value' },
460
+ });
461
+
462
+ const { response } = await model.doGenerate({
463
+ prompt: TEST_PROMPT,
464
+ });
465
+
466
+ expect(response?.headers).toMatchInlineSnapshot(`
467
+ {
468
+ "content-length": "321",
469
+ "content-type": "application/json",
470
+ "test-header": "test-value",
471
+ }
472
+ `);
473
+ });
474
+
475
+ it('should pass the model and the messages', async () => {
476
+ prepareJsonResponse({ content: '' });
477
+
478
+ await model.doGenerate({
479
+ prompt: TEST_PROMPT,
480
+ });
481
+
482
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
483
+ model: 'gpt-3.5-turbo',
484
+ messages: [{ role: 'user', content: 'Hello' }],
485
+ });
486
+ });
487
+
488
+ it('should pass settings', async () => {
489
+ prepareJsonResponse();
490
+
491
+ await provider.chat('gpt-3.5-turbo').doGenerate({
492
+ prompt: TEST_PROMPT,
493
+ providerOptions: {
494
+ openai: {
495
+ logitBias: { 50256: -100 },
496
+ parallelToolCalls: false,
497
+ user: 'test-user-id',
498
+ },
499
+ },
500
+ });
501
+
502
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
503
+ {
504
+ "logit_bias": {
505
+ "50256": -100,
506
+ },
507
+ "messages": [
508
+ {
509
+ "content": "Hello",
510
+ "role": "user",
511
+ },
512
+ ],
513
+ "model": "gpt-3.5-turbo",
514
+ "parallel_tool_calls": false,
515
+ "user": "test-user-id",
516
+ }
517
+ `);
518
+ });
519
+
520
+ it('should pass reasoningEffort setting from provider metadata', async () => {
521
+ prepareJsonResponse({ content: '' });
522
+
523
+ const model = provider.chat('o4-mini');
524
+
525
+ await model.doGenerate({
526
+ prompt: TEST_PROMPT,
527
+ providerOptions: {
528
+ openai: { reasoningEffort: 'low' },
529
+ },
530
+ });
531
+
532
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
533
+ model: 'o4-mini',
534
+ messages: [{ role: 'user', content: 'Hello' }],
535
+ reasoning_effort: 'low',
536
+ });
537
+ });
538
+
539
+ it('should pass reasoningEffort setting from settings', async () => {
540
+ prepareJsonResponse({ content: '' });
541
+
542
+ const model = provider.chat('o4-mini');
543
+
544
+ await model.doGenerate({
545
+ prompt: TEST_PROMPT,
546
+ providerOptions: {
547
+ openai: { reasoningEffort: 'high' },
548
+ },
549
+ });
550
+
551
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
552
+ model: 'o4-mini',
553
+ messages: [{ role: 'user', content: 'Hello' }],
554
+ reasoning_effort: 'high',
555
+ });
556
+ });
557
+
558
+ it('should pass reasoningEffort xhigh setting', async () => {
559
+ prepareJsonResponse({ content: '' });
560
+
561
+ const model = provider.chat('gpt-5.1-codex-max');
562
+
563
+ await model.doGenerate({
564
+ prompt: TEST_PROMPT,
565
+ providerOptions: {
566
+ openai: { reasoningEffort: 'xhigh' },
567
+ },
568
+ });
569
+
570
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
571
+ model: 'gpt-5.1-codex-max',
572
+ messages: [{ role: 'user', content: 'Hello' }],
573
+ reasoning_effort: 'xhigh',
574
+ });
575
+ });
576
+
577
+ it('should pass textVerbosity setting from provider options', async () => {
578
+ prepareJsonResponse({ content: '' });
579
+
580
+ const model = provider.chat('gpt-4o');
581
+
582
+ await model.doGenerate({
583
+ prompt: TEST_PROMPT,
584
+ providerOptions: {
585
+ openai: { textVerbosity: 'low' },
586
+ },
587
+ });
588
+
589
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
590
+ model: 'gpt-4o',
591
+ messages: [{ role: 'user', content: 'Hello' }],
592
+ verbosity: 'low',
593
+ });
594
+ });
595
+
596
+ it('should pass tools and toolChoice', async () => {
597
+ prepareJsonResponse({ content: '' });
598
+
599
+ await model.doGenerate({
600
+ tools: [
601
+ {
602
+ type: 'function',
603
+ name: 'test-tool',
604
+ inputSchema: {
605
+ type: 'object',
606
+ properties: { value: { type: 'string' } },
607
+ required: ['value'],
608
+ additionalProperties: false,
609
+ $schema: 'http://json-schema.org/draft-07/schema#',
610
+ },
611
+ },
612
+ ],
613
+ toolChoice: {
614
+ type: 'tool',
615
+ toolName: 'test-tool',
616
+ },
617
+ prompt: TEST_PROMPT,
618
+ });
619
+
620
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
621
+ {
622
+ "messages": [
623
+ {
624
+ "content": "Hello",
625
+ "role": "user",
626
+ },
627
+ ],
628
+ "model": "gpt-3.5-turbo",
629
+ "tool_choice": {
630
+ "function": {
631
+ "name": "test-tool",
632
+ },
633
+ "type": "function",
634
+ },
635
+ "tools": [
636
+ {
637
+ "function": {
638
+ "name": "test-tool",
639
+ "parameters": {
640
+ "$schema": "http://json-schema.org/draft-07/schema#",
641
+ "additionalProperties": false,
642
+ "properties": {
643
+ "value": {
644
+ "type": "string",
645
+ },
646
+ },
647
+ "required": [
648
+ "value",
649
+ ],
650
+ "type": "object",
651
+ },
652
+ },
653
+ "type": "function",
654
+ },
655
+ ],
656
+ }
657
+ `);
658
+ });
659
+
660
+ it('should pass headers', async () => {
661
+ prepareJsonResponse({ content: '' });
662
+
663
+ const provider = createOpenAI({
664
+ apiKey: 'test-api-key',
665
+ organization: 'test-organization',
666
+ project: 'test-project',
667
+ headers: {
668
+ 'Custom-Provider-Header': 'provider-header-value',
669
+ },
670
+ });
671
+
672
+ await provider.chat('gpt-3.5-turbo').doGenerate({
673
+ prompt: TEST_PROMPT,
674
+ headers: {
675
+ 'Custom-Request-Header': 'request-header-value',
676
+ },
677
+ });
678
+
679
+ expect(server.calls[0].requestHeaders).toStrictEqual({
680
+ authorization: 'Bearer test-api-key',
681
+ 'content-type': 'application/json',
682
+ 'custom-provider-header': 'provider-header-value',
683
+ 'custom-request-header': 'request-header-value',
684
+ 'openai-organization': 'test-organization',
685
+ 'openai-project': 'test-project',
686
+ });
687
+ expect(server.calls[0].requestUserAgent).toContain(
688
+ `ai-sdk/openai/0.0.0-test`,
689
+ );
690
+ });
691
+
692
+ it('should parse tool results', async () => {
693
+ prepareJsonResponse({
694
+ tool_calls: [
695
+ {
696
+ id: 'call_O17Uplv4lJvD6DVdIvFFeRMw',
697
+ type: 'function',
698
+ function: {
699
+ name: 'test-tool',
700
+ arguments: '{"value":"Spark"}',
701
+ },
702
+ },
703
+ ],
704
+ });
705
+
706
+ const result = await model.doGenerate({
707
+ tools: [
708
+ {
709
+ type: 'function',
710
+ name: 'test-tool',
711
+ inputSchema: {
712
+ type: 'object',
713
+ properties: { value: { type: 'string' } },
714
+ required: ['value'],
715
+ additionalProperties: false,
716
+ $schema: 'http://json-schema.org/draft-07/schema#',
717
+ },
718
+ },
719
+ ],
720
+ toolChoice: {
721
+ type: 'tool',
722
+ toolName: 'test-tool',
723
+ },
724
+ prompt: TEST_PROMPT,
725
+ });
726
+
727
+ expect(result.content).toMatchInlineSnapshot(`
728
+ [
729
+ {
730
+ "input": "{"value":"Spark"}",
731
+ "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
732
+ "toolName": "test-tool",
733
+ "type": "tool-call",
734
+ },
735
+ ]
736
+ `);
737
+ });
738
+
739
+ it('should parse annotations/citations', async () => {
740
+ prepareJsonResponse({
741
+ content: 'Based on the search results [doc1], I found information.',
742
+ annotations: [
743
+ {
744
+ type: 'url_citation',
745
+ url_citation: {
746
+ start_index: 24,
747
+ end_index: 29,
748
+ url: 'https://example.com/doc1.pdf',
749
+ title: 'Document 1',
750
+ },
751
+ },
752
+ ],
753
+ });
754
+
755
+ const result = await model.doGenerate({
756
+ prompt: TEST_PROMPT,
757
+ });
758
+
759
+ expect(result.content).toEqual([
760
+ {
761
+ text: 'Based on the search results [doc1], I found information.',
762
+ type: 'text',
763
+ },
764
+ {
765
+ id: expect.any(String),
766
+ sourceType: 'url',
767
+ title: 'Document 1',
768
+ type: 'source',
769
+ url: 'https://example.com/doc1.pdf',
770
+ },
771
+ ]);
772
+ });
773
+
774
+ describe('response format', () => {
775
+ it('should not send a response_format when response format is text', async () => {
776
+ prepareJsonResponse({ content: '{"value":"Spark"}' });
777
+
778
+ const model = provider.chat('gpt-4o-2024-08-06');
779
+
780
+ await model.doGenerate({
781
+ prompt: TEST_PROMPT,
782
+ responseFormat: { type: 'text' },
783
+ });
784
+
785
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
786
+ model: 'gpt-4o-2024-08-06',
787
+ messages: [{ role: 'user', content: 'Hello' }],
788
+ });
789
+ });
790
+
791
+ it('should forward json response format as "json_object" without schema', async () => {
792
+ prepareJsonResponse({ content: '{"value":"Spark"}' });
793
+
794
+ const model = provider.chat('gpt-4o-2024-08-06');
795
+
796
+ await model.doGenerate({
797
+ prompt: TEST_PROMPT,
798
+ responseFormat: { type: 'json' },
799
+ });
800
+
801
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
802
+ model: 'gpt-4o-2024-08-06',
803
+ messages: [{ role: 'user', content: 'Hello' }],
804
+ response_format: { type: 'json_object' },
805
+ });
806
+ });
807
+
808
+ it('should forward json response format as "json_object" and include schema', async () => {
809
+ prepareJsonResponse({ content: '{"value":"Spark"}' });
810
+
811
+ const model = provider.chat('gpt-4o-2024-08-06');
812
+
813
+ const { warnings } = await model.doGenerate({
814
+ prompt: TEST_PROMPT,
815
+ responseFormat: {
816
+ type: 'json',
817
+ schema: {
818
+ type: 'object',
819
+ properties: { value: { type: 'string' } },
820
+ required: ['value'],
821
+ additionalProperties: false,
822
+ $schema: 'http://json-schema.org/draft-07/schema#',
823
+ },
824
+ },
825
+ });
826
+
827
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
828
+ {
829
+ "messages": [
830
+ {
831
+ "content": "Hello",
832
+ "role": "user",
833
+ },
834
+ ],
835
+ "model": "gpt-4o-2024-08-06",
836
+ "response_format": {
837
+ "json_schema": {
838
+ "name": "response",
839
+ "schema": {
840
+ "$schema": "http://json-schema.org/draft-07/schema#",
841
+ "additionalProperties": false,
842
+ "properties": {
843
+ "value": {
844
+ "type": "string",
845
+ },
846
+ },
847
+ "required": [
848
+ "value",
849
+ ],
850
+ "type": "object",
851
+ },
852
+ "strict": true,
853
+ },
854
+ "type": "json_schema",
855
+ },
856
+ }
857
+ `);
858
+
859
+ expect(warnings).toEqual([]);
860
+ });
861
+
862
+ it('should use json_schema & strict with responseFormat json', async () => {
863
+ prepareJsonResponse({ content: '{"value":"Spark"}' });
864
+
865
+ const model = provider.chat('gpt-4o-2024-08-06');
866
+
867
+ await model.doGenerate({
868
+ responseFormat: {
869
+ type: 'json',
870
+ schema: {
871
+ type: 'object',
872
+ properties: { value: { type: 'string' } },
873
+ required: ['value'],
874
+ additionalProperties: false,
875
+ $schema: 'http://json-schema.org/draft-07/schema#',
876
+ },
877
+ },
878
+ prompt: TEST_PROMPT,
879
+ });
880
+
881
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
882
+ {
883
+ "messages": [
884
+ {
885
+ "content": "Hello",
886
+ "role": "user",
887
+ },
888
+ ],
889
+ "model": "gpt-4o-2024-08-06",
890
+ "response_format": {
891
+ "json_schema": {
892
+ "name": "response",
893
+ "schema": {
894
+ "$schema": "http://json-schema.org/draft-07/schema#",
895
+ "additionalProperties": false,
896
+ "properties": {
897
+ "value": {
898
+ "type": "string",
899
+ },
900
+ },
901
+ "required": [
902
+ "value",
903
+ ],
904
+ "type": "object",
905
+ },
906
+ "strict": true,
907
+ },
908
+ "type": "json_schema",
909
+ },
910
+ }
911
+ `);
912
+ });
913
+
914
+ it('should set name & description with responseFormat json', async () => {
915
+ prepareJsonResponse({ content: '{"value":"Spark"}' });
916
+
917
+ const model = provider.chat('gpt-4o-2024-08-06');
918
+
919
+ await model.doGenerate({
920
+ responseFormat: {
921
+ type: 'json',
922
+ name: 'test-name',
923
+ description: 'test description',
924
+ schema: {
925
+ type: 'object',
926
+ properties: { value: { type: 'string' } },
927
+ required: ['value'],
928
+ additionalProperties: false,
929
+ $schema: 'http://json-schema.org/draft-07/schema#',
930
+ },
931
+ },
932
+ prompt: TEST_PROMPT,
933
+ });
934
+
935
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
936
+ {
937
+ "messages": [
938
+ {
939
+ "content": "Hello",
940
+ "role": "user",
941
+ },
942
+ ],
943
+ "model": "gpt-4o-2024-08-06",
944
+ "response_format": {
945
+ "json_schema": {
946
+ "description": "test description",
947
+ "name": "test-name",
948
+ "schema": {
949
+ "$schema": "http://json-schema.org/draft-07/schema#",
950
+ "additionalProperties": false,
951
+ "properties": {
952
+ "value": {
953
+ "type": "string",
954
+ },
955
+ },
956
+ "required": [
957
+ "value",
958
+ ],
959
+ "type": "object",
960
+ },
961
+ "strict": true,
962
+ },
963
+ "type": "json_schema",
964
+ },
965
+ }
966
+ `);
967
+ });
968
+
969
+ it('should allow for undefined schema with responseFormat json when structuredOutputs are enabled', async () => {
970
+ prepareJsonResponse({ content: '{"value":"Spark"}' });
971
+
972
+ const model = provider.chat('gpt-4o-2024-08-06');
973
+
974
+ await model.doGenerate({
975
+ responseFormat: {
976
+ type: 'json',
977
+ name: 'test-name',
978
+ description: 'test description',
979
+ },
980
+ prompt: TEST_PROMPT,
981
+ });
982
+
983
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
984
+ model: 'gpt-4o-2024-08-06',
985
+ messages: [{ role: 'user', content: 'Hello' }],
986
+ response_format: {
987
+ type: 'json_object',
988
+ },
989
+ });
990
+ });
991
+
992
+ it('should set strict with tool call', async () => {
993
+ prepareJsonResponse({
994
+ tool_calls: [
995
+ {
996
+ id: 'call_O17Uplv4lJvD6DVdIvFFeRMw',
997
+ type: 'function',
998
+ function: {
999
+ name: 'test-tool',
1000
+ arguments: '{"value":"Spark"}',
1001
+ },
1002
+ },
1003
+ ],
1004
+ });
1005
+
1006
+ const model = provider.chat('gpt-4o-2024-08-06');
1007
+
1008
+ const result = await model.doGenerate({
1009
+ tools: [
1010
+ {
1011
+ type: 'function',
1012
+ name: 'test-tool',
1013
+ description: 'test description',
1014
+ inputSchema: {
1015
+ type: 'object',
1016
+ properties: { value: { type: 'string' } },
1017
+ required: ['value'],
1018
+ additionalProperties: false,
1019
+ $schema: 'http://json-schema.org/draft-07/schema#',
1020
+ },
1021
+ },
1022
+ ],
1023
+ toolChoice: { type: 'required' },
1024
+ prompt: TEST_PROMPT,
1025
+ });
1026
+
1027
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
1028
+ {
1029
+ "messages": [
1030
+ {
1031
+ "content": "Hello",
1032
+ "role": "user",
1033
+ },
1034
+ ],
1035
+ "model": "gpt-4o-2024-08-06",
1036
+ "tool_choice": "required",
1037
+ "tools": [
1038
+ {
1039
+ "function": {
1040
+ "description": "test description",
1041
+ "name": "test-tool",
1042
+ "parameters": {
1043
+ "$schema": "http://json-schema.org/draft-07/schema#",
1044
+ "additionalProperties": false,
1045
+ "properties": {
1046
+ "value": {
1047
+ "type": "string",
1048
+ },
1049
+ },
1050
+ "required": [
1051
+ "value",
1052
+ ],
1053
+ "type": "object",
1054
+ },
1055
+ },
1056
+ "type": "function",
1057
+ },
1058
+ ],
1059
+ }
1060
+ `);
1061
+
1062
+ expect(result.content).toMatchInlineSnapshot(`
1063
+ [
1064
+ {
1065
+ "input": "{"value":"Spark"}",
1066
+ "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
1067
+ "toolName": "test-tool",
1068
+ "type": "tool-call",
1069
+ },
1070
+ ]
1071
+ `);
1072
+ });
1073
+ });
1074
+
1075
+ it('should set strict for tool usage', async () => {
1076
+ prepareJsonResponse({
1077
+ tool_calls: [
1078
+ {
1079
+ id: 'call_O17Uplv4lJvD6DVdIvFFeRMw',
1080
+ type: 'function',
1081
+ function: {
1082
+ name: 'test-tool',
1083
+ arguments: '{"value":"Spark"}',
1084
+ },
1085
+ },
1086
+ ],
1087
+ });
1088
+
1089
+ const model = provider.chat('gpt-4o-2024-08-06');
1090
+
1091
+ const result = await model.doGenerate({
1092
+ tools: [
1093
+ {
1094
+ type: 'function',
1095
+ name: 'test-tool',
1096
+ inputSchema: {
1097
+ type: 'object',
1098
+ properties: { value: { type: 'string' } },
1099
+ required: ['value'],
1100
+ additionalProperties: false,
1101
+ $schema: 'http://json-schema.org/draft-07/schema#',
1102
+ },
1103
+ },
1104
+ ],
1105
+ toolChoice: {
1106
+ type: 'tool',
1107
+ toolName: 'test-tool',
1108
+ },
1109
+ prompt: TEST_PROMPT,
1110
+ });
1111
+
1112
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
1113
+ {
1114
+ "messages": [
1115
+ {
1116
+ "content": "Hello",
1117
+ "role": "user",
1118
+ },
1119
+ ],
1120
+ "model": "gpt-4o-2024-08-06",
1121
+ "tool_choice": {
1122
+ "function": {
1123
+ "name": "test-tool",
1124
+ },
1125
+ "type": "function",
1126
+ },
1127
+ "tools": [
1128
+ {
1129
+ "function": {
1130
+ "name": "test-tool",
1131
+ "parameters": {
1132
+ "$schema": "http://json-schema.org/draft-07/schema#",
1133
+ "additionalProperties": false,
1134
+ "properties": {
1135
+ "value": {
1136
+ "type": "string",
1137
+ },
1138
+ },
1139
+ "required": [
1140
+ "value",
1141
+ ],
1142
+ "type": "object",
1143
+ },
1144
+ },
1145
+ "type": "function",
1146
+ },
1147
+ ],
1148
+ }
1149
+ `);
1150
+
1151
+ expect(result.content).toMatchInlineSnapshot(`
1152
+ [
1153
+ {
1154
+ "input": "{"value":"Spark"}",
1155
+ "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
1156
+ "toolName": "test-tool",
1157
+ "type": "tool-call",
1158
+ },
1159
+ ]
1160
+ `);
1161
+ });
1162
+
1163
+ it('should return cached_tokens in prompt_details_tokens', async () => {
1164
+ prepareJsonResponse({
1165
+ usage: {
1166
+ prompt_tokens: 15,
1167
+ completion_tokens: 20,
1168
+ total_tokens: 35,
1169
+ prompt_tokens_details: {
1170
+ cached_tokens: 1152,
1171
+ },
1172
+ },
1173
+ });
1174
+
1175
+ const model = provider.chat('gpt-4o-mini');
1176
+
1177
+ const result = await model.doGenerate({
1178
+ prompt: TEST_PROMPT,
1179
+ });
1180
+
1181
+ expect(result.usage).toMatchInlineSnapshot(`
1182
+ {
1183
+ "inputTokens": {
1184
+ "cacheRead": 1152,
1185
+ "cacheWrite": undefined,
1186
+ "noCache": -1137,
1187
+ "total": 15,
1188
+ },
1189
+ "outputTokens": {
1190
+ "reasoning": 0,
1191
+ "text": 20,
1192
+ "total": 20,
1193
+ },
1194
+ "raw": {
1195
+ "completion_tokens": 20,
1196
+ "prompt_tokens": 15,
1197
+ "prompt_tokens_details": {
1198
+ "cached_tokens": 1152,
1199
+ },
1200
+ "total_tokens": 35,
1201
+ },
1202
+ }
1203
+ `);
1204
+ });
1205
+
1206
+ it('should return accepted_prediction_tokens and rejected_prediction_tokens in completion_details_tokens', async () => {
1207
+ prepareJsonResponse({
1208
+ usage: {
1209
+ prompt_tokens: 15,
1210
+ completion_tokens: 20,
1211
+ total_tokens: 35,
1212
+ completion_tokens_details: {
1213
+ accepted_prediction_tokens: 123,
1214
+ rejected_prediction_tokens: 456,
1215
+ },
1216
+ },
1217
+ });
1218
+
1219
+ const model = provider.chat('gpt-4o-mini');
1220
+
1221
+ const result = await model.doGenerate({
1222
+ prompt: TEST_PROMPT,
1223
+ });
1224
+
1225
+ expect(result.providerMetadata).toStrictEqual({
1226
+ openai: {
1227
+ acceptedPredictionTokens: 123,
1228
+ rejectedPredictionTokens: 456,
1229
+ },
1230
+ });
1231
+ });
1232
+
1233
+ describe('reasoning models', () => {
1234
+ it('should clear out temperature, top_p, frequency_penalty, presence_penalty and return warnings', async () => {
1235
+ prepareJsonResponse();
1236
+
1237
+ const model = provider.chat('o4-mini');
1238
+
1239
+ const result = await model.doGenerate({
1240
+ prompt: TEST_PROMPT,
1241
+ temperature: 0.5,
1242
+ topP: 0.7,
1243
+ frequencyPenalty: 0.2,
1244
+ presencePenalty: 0.3,
1245
+ });
1246
+
1247
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1248
+ model: 'o4-mini',
1249
+ messages: [{ role: 'user', content: 'Hello' }],
1250
+ });
1251
+
1252
+ expect(result.warnings).toMatchInlineSnapshot(`
1253
+ [
1254
+ {
1255
+ "details": "temperature is not supported for reasoning models",
1256
+ "feature": "temperature",
1257
+ "type": "unsupported",
1258
+ },
1259
+ {
1260
+ "details": "topP is not supported for reasoning models",
1261
+ "feature": "topP",
1262
+ "type": "unsupported",
1263
+ },
1264
+ {
1265
+ "details": "frequencyPenalty is not supported for reasoning models",
1266
+ "feature": "frequencyPenalty",
1267
+ "type": "unsupported",
1268
+ },
1269
+ {
1270
+ "details": "presencePenalty is not supported for reasoning models",
1271
+ "feature": "presencePenalty",
1272
+ "type": "unsupported",
1273
+ },
1274
+ ]
1275
+ `);
1276
+ });
1277
+
1278
+ it('should convert maxOutputTokens to max_completion_tokens', async () => {
1279
+ prepareJsonResponse();
1280
+
1281
+ const model = provider.chat('o4-mini');
1282
+
1283
+ await model.doGenerate({
1284
+ prompt: TEST_PROMPT,
1285
+ maxOutputTokens: 1000,
1286
+ });
1287
+
1288
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1289
+ model: 'o4-mini',
1290
+ messages: [{ role: 'user', content: 'Hello' }],
1291
+ max_completion_tokens: 1000,
1292
+ });
1293
+ });
1294
+ });
1295
+
1296
+ it('should allow forcing reasoning behavior for unrecognized model IDs via providerOptions', async () => {
1297
+ prepareJsonResponse();
1298
+
1299
+ const model = provider.chat('stealth-reasoning-model');
1300
+
1301
+ const result = await model.doGenerate({
1302
+ prompt: TEST_PROMPT,
1303
+ temperature: 0.5,
1304
+ topP: 0.7,
1305
+ providerOptions: {
1306
+ openai: {
1307
+ forceReasoning: true,
1308
+ },
1309
+ },
1310
+ });
1311
+
1312
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1313
+ model: 'stealth-reasoning-model',
1314
+ messages: [{ role: 'user', content: 'Hello' }],
1315
+ });
1316
+
1317
+ expect(result.warnings).toMatchInlineSnapshot(`
1318
+ [
1319
+ {
1320
+ "details": "temperature is not supported for reasoning models",
1321
+ "feature": "temperature",
1322
+ "type": "unsupported",
1323
+ },
1324
+ {
1325
+ "details": "topP is not supported for reasoning models",
1326
+ "feature": "topP",
1327
+ "type": "unsupported",
1328
+ },
1329
+ ]
1330
+ `);
1331
+ });
1332
+
1333
+ it('should default systemMessageMode to developer when forcing reasoning', async () => {
1334
+ prepareJsonResponse();
1335
+
1336
+ const model = provider.chat('stealth-reasoning-model');
1337
+
1338
+ const result = await model.doGenerate({
1339
+ prompt: [
1340
+ { role: 'system', content: 'You are a helpful assistant.' },
1341
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
1342
+ ],
1343
+ providerOptions: {
1344
+ openai: {
1345
+ forceReasoning: true,
1346
+ },
1347
+ },
1348
+ });
1349
+
1350
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1351
+ model: 'stealth-reasoning-model',
1352
+ messages: [
1353
+ { role: 'developer', content: 'You are a helpful assistant.' },
1354
+ { role: 'user', content: 'Hello' },
1355
+ ],
1356
+ });
1357
+
1358
+ expect(result.warnings).toStrictEqual([]);
1359
+ });
1360
+
1361
+ it('should use developer messages for o1', async () => {
1362
+ prepareJsonResponse();
1363
+
1364
+ const model = provider.chat('o1');
1365
+
1366
+ const result = await model.doGenerate({
1367
+ prompt: [
1368
+ { role: 'system', content: 'You are a helpful assistant.' },
1369
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
1370
+ ],
1371
+ });
1372
+
1373
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1374
+ model: 'o1',
1375
+ messages: [
1376
+ { role: 'developer', content: 'You are a helpful assistant.' },
1377
+ { role: 'user', content: 'Hello' },
1378
+ ],
1379
+ });
1380
+
1381
+ expect(result.warnings).toStrictEqual([]);
1382
+ });
1383
+
1384
+ it('should allow overriding systemMessageMode via providerOptions', async () => {
1385
+ prepareJsonResponse();
1386
+
1387
+ const model = provider.chat('gpt-4o');
1388
+
1389
+ const result = await model.doGenerate({
1390
+ prompt: [
1391
+ { role: 'system', content: 'You are a helpful assistant.' },
1392
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
1393
+ ],
1394
+ providerOptions: {
1395
+ openai: {
1396
+ systemMessageMode: 'developer',
1397
+ },
1398
+ },
1399
+ });
1400
+
1401
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1402
+ model: 'gpt-4o',
1403
+ messages: [
1404
+ { role: 'developer', content: 'You are a helpful assistant.' },
1405
+ { role: 'user', content: 'Hello' },
1406
+ ],
1407
+ });
1408
+
1409
+ expect(result.warnings).toStrictEqual([]);
1410
+ });
1411
+
1412
+ it('should use default systemMessageMode when not overridden', async () => {
1413
+ prepareJsonResponse();
1414
+
1415
+ const model = provider.chat('gpt-4o');
1416
+
1417
+ const result = await model.doGenerate({
1418
+ prompt: [
1419
+ { role: 'system', content: 'You are a helpful assistant.' },
1420
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
1421
+ ],
1422
+ });
1423
+
1424
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1425
+ model: 'gpt-4o',
1426
+ messages: [
1427
+ { role: 'system', content: 'You are a helpful assistant.' },
1428
+ { role: 'user', content: 'Hello' },
1429
+ ],
1430
+ });
1431
+
1432
+ expect(result.warnings).toStrictEqual([]);
1433
+ });
1434
+
1435
+ it('should return the reasoning tokens in the provider metadata', async () => {
1436
+ prepareJsonResponse({
1437
+ usage: {
1438
+ prompt_tokens: 15,
1439
+ completion_tokens: 20,
1440
+ total_tokens: 35,
1441
+ completion_tokens_details: {
1442
+ reasoning_tokens: 10,
1443
+ },
1444
+ },
1445
+ });
1446
+
1447
+ const model = provider.chat('o4-mini');
1448
+
1449
+ const result = await model.doGenerate({
1450
+ prompt: TEST_PROMPT,
1451
+ });
1452
+
1453
+ expect(result.usage).toMatchInlineSnapshot(`
1454
+ {
1455
+ "inputTokens": {
1456
+ "cacheRead": 0,
1457
+ "cacheWrite": undefined,
1458
+ "noCache": 15,
1459
+ "total": 15,
1460
+ },
1461
+ "outputTokens": {
1462
+ "reasoning": 10,
1463
+ "text": 10,
1464
+ "total": 20,
1465
+ },
1466
+ "raw": {
1467
+ "completion_tokens": 20,
1468
+ "completion_tokens_details": {
1469
+ "reasoning_tokens": 10,
1470
+ },
1471
+ "prompt_tokens": 15,
1472
+ "total_tokens": 35,
1473
+ },
1474
+ }
1475
+ `);
1476
+ });
1477
+
1478
+ it('should send max_completion_tokens extension setting', async () => {
1479
+ prepareJsonResponse({ model: 'o4-mini' });
1480
+
1481
+ const model = provider.chat('o4-mini');
1482
+
1483
+ await model.doGenerate({
1484
+ prompt: TEST_PROMPT,
1485
+ providerOptions: {
1486
+ openai: {
1487
+ maxCompletionTokens: 255,
1488
+ },
1489
+ },
1490
+ });
1491
+
1492
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1493
+ model: 'o4-mini',
1494
+ messages: [{ role: 'user', content: 'Hello' }],
1495
+ max_completion_tokens: 255,
1496
+ });
1497
+ });
1498
+
1499
+ it('should send prediction extension setting', async () => {
1500
+ prepareJsonResponse({ content: '' });
1501
+
1502
+ await model.doGenerate({
1503
+ prompt: TEST_PROMPT,
1504
+ providerOptions: {
1505
+ openai: {
1506
+ prediction: {
1507
+ type: 'content',
1508
+ content: 'Hello, World!',
1509
+ },
1510
+ },
1511
+ },
1512
+ });
1513
+
1514
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1515
+ model: 'gpt-3.5-turbo',
1516
+ messages: [{ role: 'user', content: 'Hello' }],
1517
+ prediction: {
1518
+ type: 'content',
1519
+ content: 'Hello, World!',
1520
+ },
1521
+ });
1522
+ });
1523
+
1524
+ it('should send store extension setting', async () => {
1525
+ prepareJsonResponse({ content: '' });
1526
+
1527
+ await model.doGenerate({
1528
+ prompt: TEST_PROMPT,
1529
+ providerOptions: {
1530
+ openai: {
1531
+ store: true,
1532
+ },
1533
+ },
1534
+ });
1535
+
1536
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1537
+ model: 'gpt-3.5-turbo',
1538
+ messages: [{ role: 'user', content: 'Hello' }],
1539
+ store: true,
1540
+ });
1541
+ });
1542
+
1543
+ it('should send metadata extension values', async () => {
1544
+ prepareJsonResponse({ content: '' });
1545
+
1546
+ await model.doGenerate({
1547
+ prompt: TEST_PROMPT,
1548
+ providerOptions: {
1549
+ openai: {
1550
+ metadata: {
1551
+ custom: 'value',
1552
+ },
1553
+ },
1554
+ },
1555
+ });
1556
+
1557
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1558
+ model: 'gpt-3.5-turbo',
1559
+ messages: [{ role: 'user', content: 'Hello' }],
1560
+ metadata: {
1561
+ custom: 'value',
1562
+ },
1563
+ });
1564
+ });
1565
+
1566
+ it('should send promptCacheKey extension value', async () => {
1567
+ prepareJsonResponse({ content: '' });
1568
+
1569
+ await model.doGenerate({
1570
+ prompt: TEST_PROMPT,
1571
+ providerOptions: {
1572
+ openai: {
1573
+ promptCacheKey: 'test-cache-key-123',
1574
+ },
1575
+ },
1576
+ });
1577
+
1578
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1579
+ model: 'gpt-3.5-turbo',
1580
+ messages: [{ role: 'user', content: 'Hello' }],
1581
+ prompt_cache_key: 'test-cache-key-123',
1582
+ });
1583
+ });
1584
+
1585
+ it('should send promptCacheRetention extension value', async () => {
1586
+ prepareJsonResponse({ content: '' });
1587
+
1588
+ await model.doGenerate({
1589
+ prompt: TEST_PROMPT,
1590
+ providerOptions: {
1591
+ openai: {
1592
+ promptCacheRetention: '24h',
1593
+ },
1594
+ },
1595
+ });
1596
+
1597
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1598
+ model: 'gpt-3.5-turbo',
1599
+ messages: [{ role: 'user', content: 'Hello' }],
1600
+ prompt_cache_retention: '24h',
1601
+ });
1602
+ });
1603
+
1604
+ it('should send safetyIdentifier extension value', async () => {
1605
+ prepareJsonResponse({ content: '' });
1606
+
1607
+ await model.doGenerate({
1608
+ prompt: TEST_PROMPT,
1609
+ providerOptions: {
1610
+ openai: {
1611
+ safetyIdentifier: 'test-safety-identifier-123',
1612
+ },
1613
+ },
1614
+ });
1615
+
1616
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
1617
+ model: 'gpt-3.5-turbo',
1618
+ messages: [{ role: 'user', content: 'Hello' }],
1619
+ safety_identifier: 'test-safety-identifier-123',
1620
+ });
1621
+ });
1622
+
1623
+ it('should remove temperature setting for gpt-4o-search-preview and add warning', async () => {
1624
+ prepareJsonResponse();
1625
+
1626
+ const model = provider.chat('gpt-4o-search-preview');
1627
+
1628
+ const result = await model.doGenerate({
1629
+ prompt: TEST_PROMPT,
1630
+ temperature: 0.7,
1631
+ });
1632
+
1633
+ const requestBody = await server.calls[0].requestBodyJson;
1634
+ expect(requestBody.model).toBe('gpt-4o-search-preview');
1635
+ expect(requestBody.temperature).toBeUndefined();
1636
+
1637
+ expect(result.warnings).toMatchInlineSnapshot(`
1638
+ [
1639
+ {
1640
+ "details": "temperature is not supported for the search preview models and has been removed.",
1641
+ "feature": "temperature",
1642
+ "type": "unsupported",
1643
+ },
1644
+ ]
1645
+ `);
1646
+ });
1647
+
1648
+ it('should remove temperature setting for gpt-4o-mini-search-preview and add warning', async () => {
1649
+ prepareJsonResponse();
1650
+
1651
+ const model = provider.chat('gpt-4o-mini-search-preview');
1652
+
1653
+ const result = await model.doGenerate({
1654
+ prompt: TEST_PROMPT,
1655
+ temperature: 0.7,
1656
+ });
1657
+
1658
+ const requestBody = await server.calls[0].requestBodyJson;
1659
+ expect(requestBody.model).toBe('gpt-4o-mini-search-preview');
1660
+ expect(requestBody.temperature).toBeUndefined();
1661
+
1662
+ expect(result.warnings).toMatchInlineSnapshot(`
1663
+ [
1664
+ {
1665
+ "details": "temperature is not supported for the search preview models and has been removed.",
1666
+ "feature": "temperature",
1667
+ "type": "unsupported",
1668
+ },
1669
+ ]
1670
+ `);
1671
+ });
1672
+
1673
+ it('should remove temperature setting for gpt-4o-mini-search-preview-2025-03-11 and add warning', async () => {
1674
+ prepareJsonResponse();
1675
+
1676
+ const model = provider.chat('gpt-4o-mini-search-preview-2025-03-11');
1677
+
1678
+ const result = await model.doGenerate({
1679
+ prompt: TEST_PROMPT,
1680
+ temperature: 0.7,
1681
+ });
1682
+
1683
+ const requestBody = await server.calls[0].requestBodyJson;
1684
+ expect(requestBody.model).toBe('gpt-4o-mini-search-preview-2025-03-11');
1685
+ expect(requestBody.temperature).toBeUndefined();
1686
+
1687
+ expect(result.warnings).toMatchInlineSnapshot(`
1688
+ [
1689
+ {
1690
+ "details": "temperature is not supported for the search preview models and has been removed.",
1691
+ "feature": "temperature",
1692
+ "type": "unsupported",
1693
+ },
1694
+ ]
1695
+ `);
1696
+ });
1697
+
1698
+ it('should send serviceTier flex processing setting', async () => {
1699
+ prepareJsonResponse({ content: '' });
1700
+
1701
+ const model = provider.chat('o4-mini');
1702
+
1703
+ await model.doGenerate({
1704
+ prompt: TEST_PROMPT,
1705
+ providerOptions: {
1706
+ openai: {
1707
+ serviceTier: 'flex',
1708
+ },
1709
+ },
1710
+ });
1711
+
1712
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
1713
+ {
1714
+ "messages": [
1715
+ {
1716
+ "content": "Hello",
1717
+ "role": "user",
1718
+ },
1719
+ ],
1720
+ "model": "o4-mini",
1721
+ "service_tier": "flex",
1722
+ }
1723
+ `);
1724
+ });
1725
+
1726
+ it('should show warning when using flex processing with unsupported model', async () => {
1727
+ prepareJsonResponse();
1728
+
1729
+ const model = provider.chat('gpt-4o-mini');
1730
+
1731
+ const result = await model.doGenerate({
1732
+ prompt: TEST_PROMPT,
1733
+ providerOptions: {
1734
+ openai: {
1735
+ serviceTier: 'flex',
1736
+ },
1737
+ },
1738
+ });
1739
+
1740
+ const requestBody = await server.calls[0].requestBodyJson;
1741
+ expect(requestBody.service_tier).toBeUndefined();
1742
+
1743
+ expect(result.warnings).toMatchInlineSnapshot(`
1744
+ [
1745
+ {
1746
+ "details": "flex processing is only available for o3, o4-mini, and gpt-5 models",
1747
+ "feature": "serviceTier",
1748
+ "type": "unsupported",
1749
+ },
1750
+ ]
1751
+ `);
1752
+ });
1753
+
1754
+ it('should allow flex processing with o4-mini model without warnings', async () => {
1755
+ prepareJsonResponse();
1756
+
1757
+ const model = provider.chat('o4-mini');
1758
+
1759
+ const result = await model.doGenerate({
1760
+ prompt: TEST_PROMPT,
1761
+ providerOptions: {
1762
+ openai: {
1763
+ serviceTier: 'flex',
1764
+ },
1765
+ },
1766
+ });
1767
+
1768
+ const requestBody = await server.calls[0].requestBodyJson;
1769
+ expect(requestBody.service_tier).toBe('flex');
1770
+ expect(result.warnings).toEqual([]);
1771
+ });
1772
+
1773
+ it('should send serviceTier priority processing setting', async () => {
1774
+ prepareJsonResponse();
1775
+
1776
+ const model = provider.chat('gpt-4o-mini');
1777
+
1778
+ await model.doGenerate({
1779
+ prompt: TEST_PROMPT,
1780
+ providerOptions: {
1781
+ openai: {
1782
+ serviceTier: 'priority',
1783
+ },
1784
+ },
1785
+ });
1786
+
1787
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
1788
+ {
1789
+ "messages": [
1790
+ {
1791
+ "content": "Hello",
1792
+ "role": "user",
1793
+ },
1794
+ ],
1795
+ "model": "gpt-4o-mini",
1796
+ "service_tier": "priority",
1797
+ }
1798
+ `);
1799
+ });
1800
+
1801
+ it('should show warning when using priority processing with unsupported model', async () => {
1802
+ prepareJsonResponse();
1803
+
1804
+ const model = provider.chat('gpt-3.5-turbo');
1805
+
1806
+ const result = await model.doGenerate({
1807
+ prompt: TEST_PROMPT,
1808
+ providerOptions: {
1809
+ openai: {
1810
+ serviceTier: 'priority',
1811
+ },
1812
+ },
1813
+ });
1814
+
1815
+ const requestBody = await server.calls[0].requestBodyJson;
1816
+ expect(requestBody.service_tier).toBeUndefined();
1817
+
1818
+ expect(result.warnings).toMatchInlineSnapshot(`
1819
+ [
1820
+ {
1821
+ "details": "priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported",
1822
+ "feature": "serviceTier",
1823
+ "type": "unsupported",
1824
+ },
1825
+ ]
1826
+ `);
1827
+ });
1828
+
1829
+ it('should allow priority processing with gpt-4o model without warnings', async () => {
1830
+ prepareJsonResponse();
1831
+
1832
+ const model = provider.chat('gpt-4o');
1833
+
1834
+ const result = await model.doGenerate({
1835
+ prompt: TEST_PROMPT,
1836
+ providerOptions: {
1837
+ openai: {
1838
+ serviceTier: 'priority',
1839
+ },
1840
+ },
1841
+ });
1842
+
1843
+ const requestBody = await server.calls[0].requestBodyJson;
1844
+ expect(requestBody.service_tier).toBe('priority');
1845
+ expect(result.warnings).toEqual([]);
1846
+ });
1847
+
1848
+ it('should allow priority processing with o3 model without warnings', async () => {
1849
+ prepareJsonResponse();
1850
+
1851
+ const model = provider.chat('o4-mini');
1852
+
1853
+ const result = await model.doGenerate({
1854
+ prompt: TEST_PROMPT,
1855
+ providerOptions: {
1856
+ openai: {
1857
+ serviceTier: 'priority',
1858
+ },
1859
+ },
1860
+ });
1861
+
1862
+ const requestBody = await server.calls[0].requestBodyJson;
1863
+ expect(requestBody.service_tier).toBe('priority');
1864
+ expect(result.warnings).toEqual([]);
1865
+ });
1866
+ });
1867
+
1868
+ describe('doStream', () => {
1869
+ function prepareStreamResponse({
1870
+ content = [],
1871
+ usage = {
1872
+ prompt_tokens: 17,
1873
+ total_tokens: 244,
1874
+ completion_tokens: 227,
1875
+ },
1876
+ logprobs = null,
1877
+ finish_reason = 'stop',
1878
+ model = 'gpt-3.5-turbo-0613',
1879
+ headers,
1880
+ }: {
1881
+ content?: string[];
1882
+ usage?: {
1883
+ prompt_tokens: number;
1884
+ total_tokens: number;
1885
+ completion_tokens: number;
1886
+ prompt_tokens_details?: {
1887
+ cached_tokens?: number;
1888
+ };
1889
+ completion_tokens_details?: {
1890
+ reasoning_tokens?: number;
1891
+ accepted_prediction_tokens?: number;
1892
+ rejected_prediction_tokens?: number;
1893
+ };
1894
+ };
1895
+ logprobs?: {
1896
+ content:
1897
+ | {
1898
+ token: string;
1899
+ logprob: number;
1900
+ top_logprobs: { token: string; logprob: number }[];
1901
+ }[]
1902
+ | null;
1903
+ } | null;
1904
+ finish_reason?: string;
1905
+ model?: string;
1906
+ headers?: Record<string, string>;
1907
+ }) {
1908
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
1909
+ type: 'stream-chunks',
1910
+ headers,
1911
+ chunks: [
1912
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` +
1913
+ `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`,
1914
+ ...content.map(text => {
1915
+ return (
1916
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` +
1917
+ `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"${text}"},"finish_reason":null}]}\n\n`
1918
+ );
1919
+ }),
1920
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` +
1921
+ `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"${finish_reason}","logprobs":${JSON.stringify(
1922
+ logprobs,
1923
+ )}}]}\n\n`,
1924
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"${model}",` +
1925
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":${JSON.stringify(
1926
+ usage,
1927
+ )}}\n\n`,
1928
+ 'data: [DONE]\n\n',
1929
+ ],
1930
+ };
1931
+ }
1932
+
1933
+ it('should stream text deltas', async () => {
1934
+ prepareStreamResponse({
1935
+ content: ['Hello', ', ', 'World!'],
1936
+ finish_reason: 'stop',
1937
+ usage: {
1938
+ prompt_tokens: 17,
1939
+ total_tokens: 244,
1940
+ completion_tokens: 227,
1941
+ },
1942
+ logprobs: TEST_LOGPROBS,
1943
+ });
1944
+
1945
+ const { stream } = await model.doStream({
1946
+ prompt: TEST_PROMPT,
1947
+ includeRawChunks: false,
1948
+ });
1949
+
1950
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1951
+ [
1952
+ {
1953
+ "type": "stream-start",
1954
+ "warnings": [],
1955
+ },
1956
+ {
1957
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
1958
+ "modelId": "gpt-3.5-turbo-0613",
1959
+ "timestamp": 2023-12-15T16:17:00.000Z,
1960
+ "type": "response-metadata",
1961
+ },
1962
+ {
1963
+ "id": "0",
1964
+ "type": "text-start",
1965
+ },
1966
+ {
1967
+ "delta": "",
1968
+ "id": "0",
1969
+ "type": "text-delta",
1970
+ },
1971
+ {
1972
+ "delta": "Hello",
1973
+ "id": "0",
1974
+ "type": "text-delta",
1975
+ },
1976
+ {
1977
+ "delta": ", ",
1978
+ "id": "0",
1979
+ "type": "text-delta",
1980
+ },
1981
+ {
1982
+ "delta": "World!",
1983
+ "id": "0",
1984
+ "type": "text-delta",
1985
+ },
1986
+ {
1987
+ "id": "0",
1988
+ "type": "text-end",
1989
+ },
1990
+ {
1991
+ "finishReason": {
1992
+ "raw": "stop",
1993
+ "unified": "stop",
1994
+ },
1995
+ "providerMetadata": {
1996
+ "openai": {
1997
+ "logprobs": [
1998
+ {
1999
+ "logprob": -0.0009994634,
2000
+ "token": "Hello",
2001
+ "top_logprobs": [
2002
+ {
2003
+ "logprob": -0.0009994634,
2004
+ "token": "Hello",
2005
+ },
2006
+ ],
2007
+ },
2008
+ {
2009
+ "logprob": -0.13410144,
2010
+ "token": "!",
2011
+ "top_logprobs": [
2012
+ {
2013
+ "logprob": -0.13410144,
2014
+ "token": "!",
2015
+ },
2016
+ ],
2017
+ },
2018
+ {
2019
+ "logprob": -0.0009250381,
2020
+ "token": " How",
2021
+ "top_logprobs": [
2022
+ {
2023
+ "logprob": -0.0009250381,
2024
+ "token": " How",
2025
+ },
2026
+ ],
2027
+ },
2028
+ {
2029
+ "logprob": -0.047709424,
2030
+ "token": " can",
2031
+ "top_logprobs": [
2032
+ {
2033
+ "logprob": -0.047709424,
2034
+ "token": " can",
2035
+ },
2036
+ ],
2037
+ },
2038
+ {
2039
+ "logprob": -0.000009014684,
2040
+ "token": " I",
2041
+ "top_logprobs": [
2042
+ {
2043
+ "logprob": -0.000009014684,
2044
+ "token": " I",
2045
+ },
2046
+ ],
2047
+ },
2048
+ {
2049
+ "logprob": -0.009125131,
2050
+ "token": " assist",
2051
+ "top_logprobs": [
2052
+ {
2053
+ "logprob": -0.009125131,
2054
+ "token": " assist",
2055
+ },
2056
+ ],
2057
+ },
2058
+ {
2059
+ "logprob": -0.0000066306106,
2060
+ "token": " you",
2061
+ "top_logprobs": [
2062
+ {
2063
+ "logprob": -0.0000066306106,
2064
+ "token": " you",
2065
+ },
2066
+ ],
2067
+ },
2068
+ {
2069
+ "logprob": -0.00011093382,
2070
+ "token": " today",
2071
+ "top_logprobs": [
2072
+ {
2073
+ "logprob": -0.00011093382,
2074
+ "token": " today",
2075
+ },
2076
+ ],
2077
+ },
2078
+ {
2079
+ "logprob": -0.00004596782,
2080
+ "token": "?",
2081
+ "top_logprobs": [
2082
+ {
2083
+ "logprob": -0.00004596782,
2084
+ "token": "?",
2085
+ },
2086
+ ],
2087
+ },
2088
+ ],
2089
+ },
2090
+ },
2091
+ "type": "finish",
2092
+ "usage": {
2093
+ "inputTokens": {
2094
+ "cacheRead": 0,
2095
+ "cacheWrite": undefined,
2096
+ "noCache": 17,
2097
+ "total": 17,
2098
+ },
2099
+ "outputTokens": {
2100
+ "reasoning": 0,
2101
+ "text": 227,
2102
+ "total": 227,
2103
+ },
2104
+ "raw": {
2105
+ "completion_tokens": 227,
2106
+ "prompt_tokens": 17,
2107
+ "total_tokens": 244,
2108
+ },
2109
+ },
2110
+ },
2111
+ ]
2112
+ `);
2113
+ });
2114
+
2115
+ it('should stream annotations/citations', async () => {
2116
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2117
+ type: 'stream-chunks',
2118
+ chunks: [
2119
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` +
2120
+ `"system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}\n\n`,
2121
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` +
2122
+ `"system_fingerprint":null,"choices":[{"index":1,"delta":{"content":"Based on search results"},"finish_reason":null}]}\n\n`,
2123
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` +
2124
+ `"system_fingerprint":null,"choices":[{"index":1,"delta":{"annotations":[{"type":"url_citation","url_citation":{"start_index":24,"end_index":29,"url":"https://example.com/doc1.pdf","title":"Document 1"}}]},"finish_reason":null}]}\n\n`,
2125
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` +
2126
+ `"system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n`,
2127
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1702657020,"model":"gpt-3.5-turbo-0125",` +
2128
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":17,"completion_tokens":227,"total_tokens":244}}\n\n`,
2129
+ 'data: [DONE]\n\n',
2130
+ ],
2131
+ };
2132
+
2133
+ const { stream } = await model.doStream({
2134
+ prompt: TEST_PROMPT,
2135
+ includeRawChunks: false,
2136
+ });
2137
+
2138
+ const streamResult = await convertReadableStreamToArray(stream);
2139
+
2140
+ expect(streamResult).toEqual(
2141
+ expect.arrayContaining([
2142
+ { type: 'stream-start', warnings: [] },
2143
+ expect.objectContaining({
2144
+ type: 'response-metadata',
2145
+ id: 'chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP',
2146
+ }),
2147
+ { type: 'text-start', id: '0' },
2148
+ { type: 'text-delta', id: '0', delta: '' },
2149
+ { type: 'text-delta', id: '0', delta: 'Based on search results' },
2150
+ {
2151
+ type: 'source',
2152
+ sourceType: 'url',
2153
+ id: expect.any(String),
2154
+ url: 'https://example.com/doc1.pdf',
2155
+ title: 'Document 1',
2156
+ },
2157
+ { type: 'text-end', id: '0' },
2158
+ expect.objectContaining({
2159
+ type: 'finish',
2160
+ finishReason: { unified: 'stop', raw: 'stop' },
2161
+ }),
2162
+ ]),
2163
+ );
2164
+ });
2165
+
2166
+ it('should stream tool deltas', async () => {
2167
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2168
+ type: 'stream-chunks',
2169
+ chunks: [
2170
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2171
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` +
2172
+ `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},` +
2173
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2174
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2175
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\""}}]},` +
2176
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2177
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2178
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},` +
2179
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2180
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2181
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` +
2182
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2183
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2184
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` +
2185
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2186
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2187
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` +
2188
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2189
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2190
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` +
2191
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2192
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2193
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` +
2194
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2195
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2196
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]}\n\n`,
2197
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2198
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":17,"total_tokens":70}}\n\n`,
2199
+ 'data: [DONE]\n\n',
2200
+ ],
2201
+ };
2202
+
2203
+ const { stream } = await model.doStream({
2204
+ tools: [
2205
+ {
2206
+ type: 'function',
2207
+ name: 'test-tool',
2208
+ inputSchema: {
2209
+ type: 'object',
2210
+ properties: { value: { type: 'string' } },
2211
+ required: ['value'],
2212
+ additionalProperties: false,
2213
+ $schema: 'http://json-schema.org/draft-07/schema#',
2214
+ },
2215
+ },
2216
+ ],
2217
+ prompt: TEST_PROMPT,
2218
+ includeRawChunks: false,
2219
+ });
2220
+
2221
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2222
+ [
2223
+ {
2224
+ "type": "stream-start",
2225
+ "warnings": [],
2226
+ },
2227
+ {
2228
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
2229
+ "modelId": "gpt-3.5-turbo-0125",
2230
+ "timestamp": 2024-03-25T09:06:38.000Z,
2231
+ "type": "response-metadata",
2232
+ },
2233
+ {
2234
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2235
+ "toolName": "test-tool",
2236
+ "type": "tool-input-start",
2237
+ },
2238
+ {
2239
+ "delta": "{"",
2240
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2241
+ "type": "tool-input-delta",
2242
+ },
2243
+ {
2244
+ "delta": "value",
2245
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2246
+ "type": "tool-input-delta",
2247
+ },
2248
+ {
2249
+ "delta": "":"",
2250
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2251
+ "type": "tool-input-delta",
2252
+ },
2253
+ {
2254
+ "delta": "Spark",
2255
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2256
+ "type": "tool-input-delta",
2257
+ },
2258
+ {
2259
+ "delta": "le",
2260
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2261
+ "type": "tool-input-delta",
2262
+ },
2263
+ {
2264
+ "delta": " Day",
2265
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2266
+ "type": "tool-input-delta",
2267
+ },
2268
+ {
2269
+ "delta": ""}",
2270
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2271
+ "type": "tool-input-delta",
2272
+ },
2273
+ {
2274
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2275
+ "type": "tool-input-end",
2276
+ },
2277
+ {
2278
+ "input": "{"value":"Sparkle Day"}",
2279
+ "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2280
+ "toolName": "test-tool",
2281
+ "type": "tool-call",
2282
+ },
2283
+ {
2284
+ "finishReason": {
2285
+ "raw": "tool_calls",
2286
+ "unified": "tool-calls",
2287
+ },
2288
+ "providerMetadata": {
2289
+ "openai": {},
2290
+ },
2291
+ "type": "finish",
2292
+ "usage": {
2293
+ "inputTokens": {
2294
+ "cacheRead": 0,
2295
+ "cacheWrite": undefined,
2296
+ "noCache": 53,
2297
+ "total": 53,
2298
+ },
2299
+ "outputTokens": {
2300
+ "reasoning": 0,
2301
+ "text": 17,
2302
+ "total": 17,
2303
+ },
2304
+ "raw": {
2305
+ "completion_tokens": 17,
2306
+ "prompt_tokens": 53,
2307
+ "total_tokens": 70,
2308
+ },
2309
+ },
2310
+ },
2311
+ ]
2312
+ `);
2313
+ });
2314
+
2315
+ it('should stream tool call deltas when tool call arguments are passed in the first chunk', async () => {
2316
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2317
+ type: 'stream-chunks',
2318
+ chunks: [
2319
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2320
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` +
2321
+ `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\""}}]},` +
2322
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2323
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2324
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"va"}}]},` +
2325
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2326
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2327
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"lue"}}]},` +
2328
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2329
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2330
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\":\\""}}]},` +
2331
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2332
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2333
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},` +
2334
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2335
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2336
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},` +
2337
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2338
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2339
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},` +
2340
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2341
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2342
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"}"}}]},` +
2343
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2344
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2345
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]}\n\n`,
2346
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2347
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":17,"total_tokens":70}}\n\n`,
2348
+ 'data: [DONE]\n\n',
2349
+ ],
2350
+ };
2351
+
2352
+ const { stream } = await model.doStream({
2353
+ tools: [
2354
+ {
2355
+ type: 'function',
2356
+ name: 'test-tool',
2357
+ inputSchema: {
2358
+ type: 'object',
2359
+ properties: { value: { type: 'string' } },
2360
+ required: ['value'],
2361
+ additionalProperties: false,
2362
+ $schema: 'http://json-schema.org/draft-07/schema#',
2363
+ },
2364
+ },
2365
+ ],
2366
+ prompt: TEST_PROMPT,
2367
+ includeRawChunks: false,
2368
+ });
2369
+
2370
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2371
+ [
2372
+ {
2373
+ "type": "stream-start",
2374
+ "warnings": [],
2375
+ },
2376
+ {
2377
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
2378
+ "modelId": "gpt-3.5-turbo-0125",
2379
+ "timestamp": 2024-03-25T09:06:38.000Z,
2380
+ "type": "response-metadata",
2381
+ },
2382
+ {
2383
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2384
+ "toolName": "test-tool",
2385
+ "type": "tool-input-start",
2386
+ },
2387
+ {
2388
+ "delta": "{"",
2389
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2390
+ "type": "tool-input-delta",
2391
+ },
2392
+ {
2393
+ "delta": "va",
2394
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2395
+ "type": "tool-input-delta",
2396
+ },
2397
+ {
2398
+ "delta": "lue",
2399
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2400
+ "type": "tool-input-delta",
2401
+ },
2402
+ {
2403
+ "delta": "":"",
2404
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2405
+ "type": "tool-input-delta",
2406
+ },
2407
+ {
2408
+ "delta": "Spark",
2409
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2410
+ "type": "tool-input-delta",
2411
+ },
2412
+ {
2413
+ "delta": "le",
2414
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2415
+ "type": "tool-input-delta",
2416
+ },
2417
+ {
2418
+ "delta": " Day",
2419
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2420
+ "type": "tool-input-delta",
2421
+ },
2422
+ {
2423
+ "delta": ""}",
2424
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2425
+ "type": "tool-input-delta",
2426
+ },
2427
+ {
2428
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2429
+ "type": "tool-input-end",
2430
+ },
2431
+ {
2432
+ "input": "{"value":"Sparkle Day"}",
2433
+ "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2434
+ "toolName": "test-tool",
2435
+ "type": "tool-call",
2436
+ },
2437
+ {
2438
+ "finishReason": {
2439
+ "raw": "tool_calls",
2440
+ "unified": "tool-calls",
2441
+ },
2442
+ "providerMetadata": {
2443
+ "openai": {},
2444
+ },
2445
+ "type": "finish",
2446
+ "usage": {
2447
+ "inputTokens": {
2448
+ "cacheRead": 0,
2449
+ "cacheWrite": undefined,
2450
+ "noCache": 53,
2451
+ "total": 53,
2452
+ },
2453
+ "outputTokens": {
2454
+ "reasoning": 0,
2455
+ "text": 17,
2456
+ "total": 17,
2457
+ },
2458
+ "raw": {
2459
+ "completion_tokens": 17,
2460
+ "prompt_tokens": 53,
2461
+ "total_tokens": 70,
2462
+ },
2463
+ },
2464
+ },
2465
+ ]
2466
+ `);
2467
+ });
2468
+
2469
+ it('should not duplicate tool calls when there is an additional empty chunk after the tool call has been completed', async () => {
2470
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2471
+ type: 'stream-chunks',
2472
+ chunks: [
2473
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2474
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],` +
2475
+ `"usage":{"prompt_tokens":226,"total_tokens":226,"completion_tokens":0}}\n\n`,
2476
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2477
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",` +
2478
+ `"type":"function","index":0,"function":{"name":"searchGoogle"}}]},"logprobs":null,"finish_reason":null}],` +
2479
+ `"usage":{"prompt_tokens":226,"total_tokens":233,"completion_tokens":7}}\n\n`,
2480
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2481
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` +
2482
+ `"function":{"arguments":"{\\"query\\": \\""}}]},"logprobs":null,"finish_reason":null}],` +
2483
+ `"usage":{"prompt_tokens":226,"total_tokens":241,"completion_tokens":15}}\n\n`,
2484
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2485
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` +
2486
+ `"function":{"arguments":"latest"}}]},"logprobs":null,"finish_reason":null}],` +
2487
+ `"usage":{"prompt_tokens":226,"total_tokens":242,"completion_tokens":16}}\n\n`,
2488
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2489
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` +
2490
+ `"function":{"arguments":" news"}}]},"logprobs":null,"finish_reason":null}],` +
2491
+ `"usage":{"prompt_tokens":226,"total_tokens":243,"completion_tokens":17}}\n\n`,
2492
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2493
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` +
2494
+ `"function":{"arguments":" on"}}]},"logprobs":null,"finish_reason":null}],` +
2495
+ `"usage":{"prompt_tokens":226,"total_tokens":244,"completion_tokens":18}}\n\n`,
2496
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2497
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` +
2498
+ `"function":{"arguments":" ai\\"}"}}]},"logprobs":null,"finish_reason":null}],` +
2499
+ `"usage":{"prompt_tokens":226,"total_tokens":245,"completion_tokens":19}}\n\n`,
2500
+ // empty arguments chunk after the tool call has already been finished:
2501
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2502
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,` +
2503
+ `"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],` +
2504
+ `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`,
2505
+ `data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,` +
2506
+ `"model":"meta/llama-3.1-8b-instruct:fp8","choices":[],` +
2507
+ `"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}\n\n`,
2508
+ `data: [DONE]\n\n`,
2509
+ ],
2510
+ };
2511
+
2512
+ const { stream } = await model.doStream({
2513
+ tools: [
2514
+ {
2515
+ type: 'function',
2516
+ name: 'searchGoogle',
2517
+ inputSchema: {
2518
+ type: 'object',
2519
+ properties: { query: { type: 'string' } },
2520
+ required: ['query'],
2521
+ additionalProperties: false,
2522
+ $schema: 'http://json-schema.org/draft-07/schema#',
2523
+ },
2524
+ },
2525
+ ],
2526
+ prompt: TEST_PROMPT,
2527
+ includeRawChunks: false,
2528
+ });
2529
+
2530
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2531
+ [
2532
+ {
2533
+ "type": "stream-start",
2534
+ "warnings": [],
2535
+ },
2536
+ {
2537
+ "id": "chat-2267f7e2910a4254bac0650ba74cfc1c",
2538
+ "modelId": "meta/llama-3.1-8b-instruct:fp8",
2539
+ "timestamp": 2024-12-02T17:57:21.000Z,
2540
+ "type": "response-metadata",
2541
+ },
2542
+ {
2543
+ "id": "0",
2544
+ "type": "text-start",
2545
+ },
2546
+ {
2547
+ "delta": "",
2548
+ "id": "0",
2549
+ "type": "text-delta",
2550
+ },
2551
+ {
2552
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2553
+ "toolName": "searchGoogle",
2554
+ "type": "tool-input-start",
2555
+ },
2556
+ {
2557
+ "delta": "{"query": "",
2558
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2559
+ "type": "tool-input-delta",
2560
+ },
2561
+ {
2562
+ "delta": "latest",
2563
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2564
+ "type": "tool-input-delta",
2565
+ },
2566
+ {
2567
+ "delta": " news",
2568
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2569
+ "type": "tool-input-delta",
2570
+ },
2571
+ {
2572
+ "delta": " on",
2573
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2574
+ "type": "tool-input-delta",
2575
+ },
2576
+ {
2577
+ "delta": " ai"}",
2578
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2579
+ "type": "tool-input-delta",
2580
+ },
2581
+ {
2582
+ "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2583
+ "type": "tool-input-end",
2584
+ },
2585
+ {
2586
+ "input": "{"query": "latest news on ai"}",
2587
+ "toolCallId": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa",
2588
+ "toolName": "searchGoogle",
2589
+ "type": "tool-call",
2590
+ },
2591
+ {
2592
+ "id": "0",
2593
+ "type": "text-end",
2594
+ },
2595
+ {
2596
+ "finishReason": {
2597
+ "raw": "tool_calls",
2598
+ "unified": "tool-calls",
2599
+ },
2600
+ "providerMetadata": {
2601
+ "openai": {},
2602
+ },
2603
+ "type": "finish",
2604
+ "usage": {
2605
+ "inputTokens": {
2606
+ "cacheRead": 0,
2607
+ "cacheWrite": undefined,
2608
+ "noCache": 226,
2609
+ "total": 226,
2610
+ },
2611
+ "outputTokens": {
2612
+ "reasoning": 0,
2613
+ "text": 20,
2614
+ "total": 20,
2615
+ },
2616
+ "raw": {
2617
+ "completion_tokens": 20,
2618
+ "prompt_tokens": 226,
2619
+ "total_tokens": 246,
2620
+ },
2621
+ },
2622
+ },
2623
+ ]
2624
+ `);
2625
+ });
2626
+
2627
+ it('should stream tool call that is sent in one chunk', async () => {
2628
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2629
+ type: 'stream-chunks',
2630
+ chunks: [
2631
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2632
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` +
2633
+ `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\\"value\\":\\"Sparkle Day\\"}"}}]},` +
2634
+ `"logprobs":null,"finish_reason":null}]}\n\n`,
2635
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2636
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}]}\n\n`,
2637
+ `data: {"id":"chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP","object":"chat.completion.chunk","created":1711357598,"model":"gpt-3.5-turbo-0125",` +
2638
+ `"system_fingerprint":"fp_3bc1b5746c","choices":[],"usage":{"prompt_tokens":53,"completion_tokens":17,"total_tokens":70}}\n\n`,
2639
+ 'data: [DONE]\n\n',
2640
+ ],
2641
+ };
2642
+
2643
+ const { stream } = await model.doStream({
2644
+ tools: [
2645
+ {
2646
+ type: 'function',
2647
+ name: 'test-tool',
2648
+ inputSchema: {
2649
+ type: 'object',
2650
+ properties: { value: { type: 'string' } },
2651
+ required: ['value'],
2652
+ additionalProperties: false,
2653
+ $schema: 'http://json-schema.org/draft-07/schema#',
2654
+ },
2655
+ },
2656
+ ],
2657
+ prompt: TEST_PROMPT,
2658
+ includeRawChunks: false,
2659
+ });
2660
+
2661
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2662
+ [
2663
+ {
2664
+ "type": "stream-start",
2665
+ "warnings": [],
2666
+ },
2667
+ {
2668
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
2669
+ "modelId": "gpt-3.5-turbo-0125",
2670
+ "timestamp": 2024-03-25T09:06:38.000Z,
2671
+ "type": "response-metadata",
2672
+ },
2673
+ {
2674
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2675
+ "toolName": "test-tool",
2676
+ "type": "tool-input-start",
2677
+ },
2678
+ {
2679
+ "delta": "{"value":"Sparkle Day"}",
2680
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2681
+ "type": "tool-input-delta",
2682
+ },
2683
+ {
2684
+ "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2685
+ "type": "tool-input-end",
2686
+ },
2687
+ {
2688
+ "input": "{"value":"Sparkle Day"}",
2689
+ "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
2690
+ "toolName": "test-tool",
2691
+ "type": "tool-call",
2692
+ },
2693
+ {
2694
+ "finishReason": {
2695
+ "raw": "tool_calls",
2696
+ "unified": "tool-calls",
2697
+ },
2698
+ "providerMetadata": {
2699
+ "openai": {},
2700
+ },
2701
+ "type": "finish",
2702
+ "usage": {
2703
+ "inputTokens": {
2704
+ "cacheRead": 0,
2705
+ "cacheWrite": undefined,
2706
+ "noCache": 53,
2707
+ "total": 53,
2708
+ },
2709
+ "outputTokens": {
2710
+ "reasoning": 0,
2711
+ "text": 17,
2712
+ "total": 17,
2713
+ },
2714
+ "raw": {
2715
+ "completion_tokens": 17,
2716
+ "prompt_tokens": 53,
2717
+ "total_tokens": 70,
2718
+ },
2719
+ },
2720
+ },
2721
+ ]
2722
+ `);
2723
+ });
2724
+
2725
+ it('should handle error stream parts', async () => {
2726
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2727
+ type: 'stream-chunks',
2728
+ chunks: [
2729
+ `data: {"error":{"message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our ` +
2730
+ `help center at help.openai.com if you keep seeing this error.","type":"server_error","param":null,"code":null}}\n\n`,
2731
+ 'data: [DONE]\n\n',
2732
+ ],
2733
+ };
2734
+
2735
+ const { stream } = await model.doStream({
2736
+ prompt: TEST_PROMPT,
2737
+ includeRawChunks: false,
2738
+ });
2739
+
2740
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2741
+ [
2742
+ {
2743
+ "type": "stream-start",
2744
+ "warnings": [],
2745
+ },
2746
+ {
2747
+ "error": {
2748
+ "code": null,
2749
+ "message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.",
2750
+ "param": null,
2751
+ "type": "server_error",
2752
+ },
2753
+ "type": "error",
2754
+ },
2755
+ {
2756
+ "finishReason": {
2757
+ "raw": undefined,
2758
+ "unified": "error",
2759
+ },
2760
+ "providerMetadata": {
2761
+ "openai": {},
2762
+ },
2763
+ "type": "finish",
2764
+ "usage": {
2765
+ "inputTokens": {
2766
+ "cacheRead": undefined,
2767
+ "cacheWrite": undefined,
2768
+ "noCache": undefined,
2769
+ "total": undefined,
2770
+ },
2771
+ "outputTokens": {
2772
+ "reasoning": undefined,
2773
+ "text": undefined,
2774
+ "total": undefined,
2775
+ },
2776
+ "raw": undefined,
2777
+ },
2778
+ },
2779
+ ]
2780
+ `);
2781
+ });
2782
+
2783
+ it.skipIf(isNodeVersion(20))(
2784
+ 'should handle unparsable stream parts',
2785
+ async () => {
2786
+ server.urls['https://api.openai.com/v1/chat/completions'].response = {
2787
+ type: 'stream-chunks',
2788
+ chunks: [`data: {unparsable}\n\n`, 'data: [DONE]\n\n'],
2789
+ };
2790
+
2791
+ const { stream } = await model.doStream({
2792
+ prompt: TEST_PROMPT,
2793
+ includeRawChunks: false,
2794
+ });
2795
+
2796
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
2797
+ [
2798
+ {
2799
+ "type": "stream-start",
2800
+ "warnings": [],
2801
+ },
2802
+ {
2803
+ "error": [AI_JSONParseError: JSON parsing failed: Text: {unparsable}.
2804
+ Error message: Expected property name or '}' in JSON at position 1 (line 1 column 2)],
2805
+ "type": "error",
2806
+ },
2807
+ {
2808
+ "finishReason": {
2809
+ "raw": undefined,
2810
+ "unified": "error",
2811
+ },
2812
+ "providerMetadata": {
2813
+ "openai": {},
2814
+ },
2815
+ "type": "finish",
2816
+ "usage": {
2817
+ "inputTokens": {
2818
+ "cacheRead": undefined,
2819
+ "cacheWrite": undefined,
2820
+ "noCache": undefined,
2821
+ "total": undefined,
2822
+ },
2823
+ "outputTokens": {
2824
+ "reasoning": undefined,
2825
+ "text": undefined,
2826
+ "total": undefined,
2827
+ },
2828
+ "raw": undefined,
2829
+ },
2830
+ },
2831
+ ]
2832
+ `);
2833
+ },
2834
+ );
2835
+
2836
+ it('should send request body', async () => {
2837
+ prepareStreamResponse({ content: [] });
2838
+
2839
+ const { request } = await model.doStream({
2840
+ prompt: TEST_PROMPT,
2841
+ includeRawChunks: false,
2842
+ });
2843
+
2844
+ expect(request).toMatchInlineSnapshot(`
2845
+ {
2846
+ "body": {
2847
+ "frequency_penalty": undefined,
2848
+ "logit_bias": undefined,
2849
+ "logprobs": undefined,
2850
+ "max_completion_tokens": undefined,
2851
+ "max_tokens": undefined,
2852
+ "messages": [
2853
+ {
2854
+ "content": "Hello",
2855
+ "role": "user",
2856
+ },
2857
+ ],
2858
+ "metadata": undefined,
2859
+ "model": "gpt-3.5-turbo",
2860
+ "parallel_tool_calls": undefined,
2861
+ "prediction": undefined,
2862
+ "presence_penalty": undefined,
2863
+ "prompt_cache_key": undefined,
2864
+ "prompt_cache_retention": undefined,
2865
+ "reasoning_effort": undefined,
2866
+ "response_format": undefined,
2867
+ "safety_identifier": undefined,
2868
+ "seed": undefined,
2869
+ "service_tier": undefined,
2870
+ "stop": undefined,
2871
+ "store": undefined,
2872
+ "stream": true,
2873
+ "stream_options": {
2874
+ "include_usage": true,
2875
+ },
2876
+ "temperature": undefined,
2877
+ "tool_choice": undefined,
2878
+ "tools": undefined,
2879
+ "top_logprobs": undefined,
2880
+ "top_p": undefined,
2881
+ "user": undefined,
2882
+ "verbosity": undefined,
2883
+ },
2884
+ }
2885
+ `);
2886
+ });
2887
+
2888
+ it('should expose the raw response headers', async () => {
2889
+ prepareStreamResponse({
2890
+ headers: { 'test-header': 'test-value' },
2891
+ });
2892
+
2893
+ const { response } = await model.doStream({
2894
+ prompt: TEST_PROMPT,
2895
+ includeRawChunks: false,
2896
+ });
2897
+
2898
+ expect(response?.headers).toStrictEqual({
2899
+ // default headers:
2900
+ 'content-type': 'text/event-stream',
2901
+ 'cache-control': 'no-cache',
2902
+ connection: 'keep-alive',
2903
+
2904
+ // custom header
2905
+ 'test-header': 'test-value',
2906
+ });
2907
+ });
2908
+
2909
+ it('should pass the messages and the model', async () => {
2910
+ prepareStreamResponse({ content: [] });
2911
+
2912
+ await model.doStream({
2913
+ prompt: TEST_PROMPT,
2914
+ includeRawChunks: false,
2915
+ });
2916
+
2917
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
2918
+ stream: true,
2919
+ stream_options: { include_usage: true },
2920
+ model: 'gpt-3.5-turbo',
2921
+ messages: [{ role: 'user', content: 'Hello' }],
2922
+ });
2923
+ });
2924
+
2925
+ it('should pass headers', async () => {
2926
+ prepareStreamResponse({ content: [] });
2927
+
2928
+ const provider = createOpenAI({
2929
+ apiKey: 'test-api-key',
2930
+ organization: 'test-organization',
2931
+ project: 'test-project',
2932
+ headers: {
2933
+ 'Custom-Provider-Header': 'provider-header-value',
2934
+ },
2935
+ });
2936
+
2937
+ await provider.chat('gpt-3.5-turbo').doStream({
2938
+ prompt: TEST_PROMPT,
2939
+ headers: {
2940
+ 'Custom-Request-Header': 'request-header-value',
2941
+ },
2942
+ includeRawChunks: false,
2943
+ });
2944
+
2945
+ expect(server.calls[0].requestHeaders).toStrictEqual({
2946
+ authorization: 'Bearer test-api-key',
2947
+ 'content-type': 'application/json',
2948
+ 'custom-provider-header': 'provider-header-value',
2949
+ 'custom-request-header': 'request-header-value',
2950
+ 'openai-organization': 'test-organization',
2951
+ 'openai-project': 'test-project',
2952
+ });
2953
+ });
2954
+
2955
+ it('should return cached tokens in providerMetadata', async () => {
2956
+ prepareStreamResponse({
2957
+ content: [],
2958
+ usage: {
2959
+ prompt_tokens: 15,
2960
+ completion_tokens: 20,
2961
+ total_tokens: 35,
2962
+ prompt_tokens_details: {
2963
+ cached_tokens: 1152,
2964
+ },
2965
+ },
2966
+ });
2967
+
2968
+ const { stream } = await model.doStream({
2969
+ prompt: TEST_PROMPT,
2970
+ includeRawChunks: false,
2971
+ });
2972
+
2973
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
2974
+ stream: true,
2975
+ stream_options: { include_usage: true },
2976
+ model: 'gpt-3.5-turbo',
2977
+ messages: [{ role: 'user', content: 'Hello' }],
2978
+ });
2979
+
2980
+ expect((await convertReadableStreamToArray(stream)).at(-1))
2981
+ .toMatchInlineSnapshot(`
2982
+ {
2983
+ "finishReason": {
2984
+ "raw": "stop",
2985
+ "unified": "stop",
2986
+ },
2987
+ "providerMetadata": {
2988
+ "openai": {},
2989
+ },
2990
+ "type": "finish",
2991
+ "usage": {
2992
+ "inputTokens": {
2993
+ "cacheRead": 1152,
2994
+ "cacheWrite": undefined,
2995
+ "noCache": -1137,
2996
+ "total": 15,
2997
+ },
2998
+ "outputTokens": {
2999
+ "reasoning": 0,
3000
+ "text": 20,
3001
+ "total": 20,
3002
+ },
3003
+ "raw": {
3004
+ "completion_tokens": 20,
3005
+ "prompt_tokens": 15,
3006
+ "prompt_tokens_details": {
3007
+ "cached_tokens": 1152,
3008
+ },
3009
+ "total_tokens": 35,
3010
+ },
3011
+ },
3012
+ }
3013
+ `);
3014
+ });
3015
+
3016
+ it('should return accepted_prediction_tokens and rejected_prediction_tokens in providerMetadata', async () => {
3017
+ prepareStreamResponse({
3018
+ content: [],
3019
+ usage: {
3020
+ prompt_tokens: 15,
3021
+ completion_tokens: 20,
3022
+ total_tokens: 35,
3023
+ completion_tokens_details: {
3024
+ accepted_prediction_tokens: 123,
3025
+ rejected_prediction_tokens: 456,
3026
+ },
3027
+ },
3028
+ });
3029
+
3030
+ const { stream } = await model.doStream({
3031
+ prompt: TEST_PROMPT,
3032
+ includeRawChunks: false,
3033
+ });
3034
+
3035
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
3036
+ stream: true,
3037
+ stream_options: { include_usage: true },
3038
+ model: 'gpt-3.5-turbo',
3039
+ messages: [{ role: 'user', content: 'Hello' }],
3040
+ });
3041
+
3042
+ expect((await convertReadableStreamToArray(stream)).at(-1))
3043
+ .toMatchInlineSnapshot(`
3044
+ {
3045
+ "finishReason": {
3046
+ "raw": "stop",
3047
+ "unified": "stop",
3048
+ },
3049
+ "providerMetadata": {
3050
+ "openai": {
3051
+ "acceptedPredictionTokens": 123,
3052
+ "rejectedPredictionTokens": 456,
3053
+ },
3054
+ },
3055
+ "type": "finish",
3056
+ "usage": {
3057
+ "inputTokens": {
3058
+ "cacheRead": 0,
3059
+ "cacheWrite": undefined,
3060
+ "noCache": 15,
3061
+ "total": 15,
3062
+ },
3063
+ "outputTokens": {
3064
+ "reasoning": 0,
3065
+ "text": 20,
3066
+ "total": 20,
3067
+ },
3068
+ "raw": {
3069
+ "completion_tokens": 20,
3070
+ "completion_tokens_details": {
3071
+ "accepted_prediction_tokens": 123,
3072
+ "rejected_prediction_tokens": 456,
3073
+ },
3074
+ "prompt_tokens": 15,
3075
+ "total_tokens": 35,
3076
+ },
3077
+ },
3078
+ }
3079
+ `);
3080
+ });
3081
+
3082
+ it('should send store extension setting', async () => {
3083
+ prepareStreamResponse({ content: [] });
3084
+
3085
+ await model.doStream({
3086
+ prompt: TEST_PROMPT,
3087
+ providerOptions: {
3088
+ openai: {
3089
+ store: true,
3090
+ },
3091
+ },
3092
+ includeRawChunks: false,
3093
+ });
3094
+
3095
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
3096
+ model: 'gpt-3.5-turbo',
3097
+ stream: true,
3098
+ stream_options: { include_usage: true },
3099
+ messages: [{ role: 'user', content: 'Hello' }],
3100
+ store: true,
3101
+ });
3102
+ });
3103
+
3104
+ it('should send metadata extension values', async () => {
3105
+ prepareStreamResponse({ content: [] });
3106
+
3107
+ await model.doStream({
3108
+ prompt: TEST_PROMPT,
3109
+ providerOptions: {
3110
+ openai: {
3111
+ metadata: {
3112
+ custom: 'value',
3113
+ },
3114
+ },
3115
+ },
3116
+ includeRawChunks: false,
3117
+ });
3118
+
3119
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
3120
+ model: 'gpt-3.5-turbo',
3121
+ stream: true,
3122
+ stream_options: { include_usage: true },
3123
+ messages: [{ role: 'user', content: 'Hello' }],
3124
+ metadata: {
3125
+ custom: 'value',
3126
+ },
3127
+ });
3128
+ });
3129
+
3130
+ it('should send serviceTier flex processing setting in streaming', async () => {
3131
+ prepareStreamResponse({ content: [] });
3132
+
3133
+ const model = provider.chat('o4-mini');
3134
+
3135
+ await model.doStream({
3136
+ prompt: TEST_PROMPT,
3137
+ providerOptions: {
3138
+ openai: {
3139
+ serviceTier: 'flex',
3140
+ },
3141
+ },
3142
+ includeRawChunks: false,
3143
+ });
3144
+
3145
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
3146
+ {
3147
+ "messages": [
3148
+ {
3149
+ "content": "Hello",
3150
+ "role": "user",
3151
+ },
3152
+ ],
3153
+ "model": "o4-mini",
3154
+ "service_tier": "flex",
3155
+ "stream": true,
3156
+ "stream_options": {
3157
+ "include_usage": true,
3158
+ },
3159
+ }
3160
+ `);
3161
+ });
3162
+
3163
+ it('should send serviceTier priority processing setting in streaming', async () => {
3164
+ prepareStreamResponse({ content: [] });
3165
+
3166
+ const model = provider.chat('gpt-4o-mini');
3167
+
3168
+ await model.doStream({
3169
+ prompt: TEST_PROMPT,
3170
+ providerOptions: {
3171
+ openai: {
3172
+ serviceTier: 'priority',
3173
+ },
3174
+ },
3175
+ includeRawChunks: false,
3176
+ });
3177
+
3178
+ expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
3179
+ {
3180
+ "messages": [
3181
+ {
3182
+ "content": "Hello",
3183
+ "role": "user",
3184
+ },
3185
+ ],
3186
+ "model": "gpt-4o-mini",
3187
+ "service_tier": "priority",
3188
+ "stream": true,
3189
+ "stream_options": {
3190
+ "include_usage": true,
3191
+ },
3192
+ }
3193
+ `);
3194
+ });
3195
+
3196
+ it('should set .modelId for model-router request', async () => {
3197
+ prepareChunksFixtureResponse('azure-model-router.1');
3198
+
3199
+ const result = await provider.chat('test-azure-model-router').doStream({
3200
+ prompt: TEST_PROMPT,
3201
+ });
3202
+
3203
+ expect(await convertReadableStreamToArray(result.stream)).toMatchSnapshot();
3204
+ });
3205
+
3206
+ describe('reasoning models', () => {
3207
+ it('should stream text delta', async () => {
3208
+ prepareStreamResponse({
3209
+ content: ['Hello, World!'],
3210
+ model: 'o4-mini',
3211
+ });
3212
+
3213
+ const model = provider.chat('o4-mini');
3214
+
3215
+ const { stream } = await model.doStream({
3216
+ prompt: TEST_PROMPT,
3217
+ includeRawChunks: false,
3218
+ });
3219
+
3220
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
3221
+ [
3222
+ {
3223
+ "type": "stream-start",
3224
+ "warnings": [],
3225
+ },
3226
+ {
3227
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3228
+ "modelId": "o4-mini",
3229
+ "timestamp": 2023-12-15T16:17:00.000Z,
3230
+ "type": "response-metadata",
3231
+ },
3232
+ {
3233
+ "id": "0",
3234
+ "type": "text-start",
3235
+ },
3236
+ {
3237
+ "delta": "",
3238
+ "id": "0",
3239
+ "type": "text-delta",
3240
+ },
3241
+ {
3242
+ "delta": "Hello, World!",
3243
+ "id": "0",
3244
+ "type": "text-delta",
3245
+ },
3246
+ {
3247
+ "id": "0",
3248
+ "type": "text-end",
3249
+ },
3250
+ {
3251
+ "finishReason": {
3252
+ "raw": "stop",
3253
+ "unified": "stop",
3254
+ },
3255
+ "providerMetadata": {
3256
+ "openai": {},
3257
+ },
3258
+ "type": "finish",
3259
+ "usage": {
3260
+ "inputTokens": {
3261
+ "cacheRead": 0,
3262
+ "cacheWrite": undefined,
3263
+ "noCache": 17,
3264
+ "total": 17,
3265
+ },
3266
+ "outputTokens": {
3267
+ "reasoning": 0,
3268
+ "text": 227,
3269
+ "total": 227,
3270
+ },
3271
+ "raw": {
3272
+ "completion_tokens": 227,
3273
+ "prompt_tokens": 17,
3274
+ "total_tokens": 244,
3275
+ },
3276
+ },
3277
+ },
3278
+ ]
3279
+ `);
3280
+ });
3281
+
3282
+ it('should send reasoning tokens', async () => {
3283
+ prepareStreamResponse({
3284
+ content: ['Hello, World!'],
3285
+ model: 'o4-mini',
3286
+ usage: {
3287
+ prompt_tokens: 15,
3288
+ completion_tokens: 20,
3289
+ total_tokens: 35,
3290
+ completion_tokens_details: {
3291
+ reasoning_tokens: 10,
3292
+ },
3293
+ },
3294
+ });
3295
+
3296
+ const model = provider.chat('o4-mini');
3297
+
3298
+ const { stream } = await model.doStream({
3299
+ prompt: TEST_PROMPT,
3300
+ includeRawChunks: false,
3301
+ });
3302
+
3303
+ expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
3304
+ [
3305
+ {
3306
+ "type": "stream-start",
3307
+ "warnings": [],
3308
+ },
3309
+ {
3310
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3311
+ "modelId": "o4-mini",
3312
+ "timestamp": 2023-12-15T16:17:00.000Z,
3313
+ "type": "response-metadata",
3314
+ },
3315
+ {
3316
+ "id": "0",
3317
+ "type": "text-start",
3318
+ },
3319
+ {
3320
+ "delta": "",
3321
+ "id": "0",
3322
+ "type": "text-delta",
3323
+ },
3324
+ {
3325
+ "delta": "Hello, World!",
3326
+ "id": "0",
3327
+ "type": "text-delta",
3328
+ },
3329
+ {
3330
+ "id": "0",
3331
+ "type": "text-end",
3332
+ },
3333
+ {
3334
+ "finishReason": {
3335
+ "raw": "stop",
3336
+ "unified": "stop",
3337
+ },
3338
+ "providerMetadata": {
3339
+ "openai": {},
3340
+ },
3341
+ "type": "finish",
3342
+ "usage": {
3343
+ "inputTokens": {
3344
+ "cacheRead": 0,
3345
+ "cacheWrite": undefined,
3346
+ "noCache": 15,
3347
+ "total": 15,
3348
+ },
3349
+ "outputTokens": {
3350
+ "reasoning": 10,
3351
+ "text": 10,
3352
+ "total": 20,
3353
+ },
3354
+ "raw": {
3355
+ "completion_tokens": 20,
3356
+ "completion_tokens_details": {
3357
+ "reasoning_tokens": 10,
3358
+ },
3359
+ "prompt_tokens": 15,
3360
+ "total_tokens": 35,
3361
+ },
3362
+ },
3363
+ },
3364
+ ]
3365
+ `);
3366
+ });
3367
+ });
3368
+
3369
+ describe('raw chunks', () => {
3370
+ it('should include raw chunks when includeRawChunks is enabled', async () => {
3371
+ prepareStreamResponse({
3372
+ content: ['Hello', ' World!'],
3373
+ });
3374
+
3375
+ const { stream } = await model.doStream({
3376
+ prompt: TEST_PROMPT,
3377
+ includeRawChunks: true,
3378
+ });
3379
+
3380
+ const chunks = await convertReadableStreamToArray(stream);
3381
+
3382
+ expect(chunks.filter(chunk => chunk.type === 'raw'))
3383
+ .toMatchInlineSnapshot(`
3384
+ [
3385
+ {
3386
+ "rawValue": {
3387
+ "choices": [
3388
+ {
3389
+ "delta": {
3390
+ "content": "",
3391
+ "role": "assistant",
3392
+ },
3393
+ "finish_reason": null,
3394
+ "index": 0,
3395
+ },
3396
+ ],
3397
+ "created": 1702657020,
3398
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3399
+ "model": "gpt-3.5-turbo-0613",
3400
+ "object": "chat.completion.chunk",
3401
+ "system_fingerprint": null,
3402
+ },
3403
+ "type": "raw",
3404
+ },
3405
+ {
3406
+ "rawValue": {
3407
+ "choices": [
3408
+ {
3409
+ "delta": {
3410
+ "content": "Hello",
3411
+ },
3412
+ "finish_reason": null,
3413
+ "index": 1,
3414
+ },
3415
+ ],
3416
+ "created": 1702657020,
3417
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3418
+ "model": "gpt-3.5-turbo-0613",
3419
+ "object": "chat.completion.chunk",
3420
+ "system_fingerprint": null,
3421
+ },
3422
+ "type": "raw",
3423
+ },
3424
+ {
3425
+ "rawValue": {
3426
+ "choices": [
3427
+ {
3428
+ "delta": {
3429
+ "content": " World!",
3430
+ },
3431
+ "finish_reason": null,
3432
+ "index": 1,
3433
+ },
3434
+ ],
3435
+ "created": 1702657020,
3436
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3437
+ "model": "gpt-3.5-turbo-0613",
3438
+ "object": "chat.completion.chunk",
3439
+ "system_fingerprint": null,
3440
+ },
3441
+ "type": "raw",
3442
+ },
3443
+ {
3444
+ "rawValue": {
3445
+ "choices": [
3446
+ {
3447
+ "delta": {},
3448
+ "finish_reason": "stop",
3449
+ "index": 0,
3450
+ "logprobs": null,
3451
+ },
3452
+ ],
3453
+ "created": 1702657020,
3454
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3455
+ "model": "gpt-3.5-turbo-0613",
3456
+ "object": "chat.completion.chunk",
3457
+ "system_fingerprint": null,
3458
+ },
3459
+ "type": "raw",
3460
+ },
3461
+ {
3462
+ "rawValue": {
3463
+ "choices": [],
3464
+ "created": 1702657020,
3465
+ "id": "chatcmpl-96aZqmeDpA9IPD6tACY8djkMsJCMP",
3466
+ "model": "gpt-3.5-turbo-0613",
3467
+ "object": "chat.completion.chunk",
3468
+ "system_fingerprint": "fp_3bc1b5746c",
3469
+ "usage": {
3470
+ "completion_tokens": 227,
3471
+ "prompt_tokens": 17,
3472
+ "total_tokens": 244,
3473
+ },
3474
+ },
3475
+ "type": "raw",
3476
+ },
3477
+ ]
3478
+ `);
3479
+ });
3480
+
3481
+ it('should not include raw chunks when includeRawChunks is false', async () => {
3482
+ prepareStreamResponse({
3483
+ content: ['Hello', ' World!'],
3484
+ });
3485
+
3486
+ const { stream } = await model.doStream({
3487
+ prompt: TEST_PROMPT,
3488
+ includeRawChunks: false,
3489
+ });
3490
+
3491
+ const chunks = await convertReadableStreamToArray(stream);
3492
+
3493
+ expect(chunks.filter(chunk => chunk.type === 'raw')).toHaveLength(0);
3494
+ });
3495
+ });
3496
+ });