@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,507 @@
1
+ import { createTestServer } from '@ai-sdk/test-server/with-vitest';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createOpenAI } from '../openai-provider';
5
+ import { OpenAITranscriptionModel } from './openai-transcription-model';
6
+ import { describe, it, expect, vi } from 'vitest';
7
+
8
+ vi.mock('../version', () => ({
9
+ VERSION: '0.0.0-test',
10
+ }));
11
+
12
+ const audioData = await readFile(
13
+ path.join(__dirname, 'transcription-test.mp3'),
14
+ );
15
+ const provider = createOpenAI({ apiKey: 'test-api-key' });
16
+ const model = provider.transcription('whisper-1');
17
+
18
+ const server = createTestServer({
19
+ 'https://api.openai.com/v1/audio/transcriptions': {},
20
+ });
21
+
22
+ describe('doGenerate', () => {
23
+ function prepareJsonResponse({
24
+ headers,
25
+ }: {
26
+ headers?: Record<string, string>;
27
+ } = {}) {
28
+ server.urls['https://api.openai.com/v1/audio/transcriptions'].response = {
29
+ type: 'json-value',
30
+ headers,
31
+ body: {
32
+ task: 'transcribe',
33
+ text: 'Hello from the Vercel AI SDK!',
34
+ words: [
35
+ {
36
+ word: 'Hello',
37
+ start: 0,
38
+ end: 5,
39
+ },
40
+ {
41
+ word: 'from',
42
+ start: 5,
43
+ end: 10,
44
+ },
45
+ {
46
+ word: 'the',
47
+ start: 10,
48
+ end: 15,
49
+ },
50
+ {
51
+ word: 'Vercel',
52
+ start: 15,
53
+ end: 20,
54
+ },
55
+ {
56
+ word: 'AI',
57
+ start: 20,
58
+ end: 25,
59
+ },
60
+ {
61
+ word: 'SDK',
62
+ start: 25,
63
+ end: 30,
64
+ },
65
+ {
66
+ word: '!',
67
+ start: 30,
68
+ end: 35,
69
+ },
70
+ ],
71
+ durationInSeconds: 35,
72
+ language: 'en',
73
+ _request_id: 'req_1234',
74
+ },
75
+ };
76
+ }
77
+
78
+ it('should pass the model', async () => {
79
+ prepareJsonResponse();
80
+
81
+ await model.doGenerate({
82
+ audio: audioData,
83
+ mediaType: 'audio/wav',
84
+ });
85
+
86
+ expect(await server.calls[0].requestBodyMultipart).toMatchObject({
87
+ model: 'whisper-1',
88
+ });
89
+ });
90
+
91
+ it('should pass headers', async () => {
92
+ prepareJsonResponse();
93
+
94
+ const provider = createOpenAI({
95
+ apiKey: 'test-api-key',
96
+ organization: 'test-organization',
97
+ project: 'test-project',
98
+ headers: {
99
+ 'Custom-Provider-Header': 'provider-header-value',
100
+ },
101
+ });
102
+
103
+ await provider.transcription('whisper-1').doGenerate({
104
+ audio: audioData,
105
+ mediaType: 'audio/wav',
106
+ headers: {
107
+ 'Custom-Request-Header': 'request-header-value',
108
+ },
109
+ });
110
+
111
+ expect(server.calls[0].requestHeaders).toMatchObject({
112
+ authorization: 'Bearer test-api-key',
113
+ 'content-type': expect.stringMatching(
114
+ /^multipart\/form-data; boundary=----formdata-undici-\d+$/,
115
+ ),
116
+ 'custom-provider-header': 'provider-header-value',
117
+ 'custom-request-header': 'request-header-value',
118
+ 'openai-organization': 'test-organization',
119
+ 'openai-project': 'test-project',
120
+ });
121
+
122
+ expect(server.calls[0].requestUserAgent).toContain(
123
+ `ai-sdk/openai/0.0.0-test`,
124
+ );
125
+ });
126
+
127
+ it('should extract the transcription text', async () => {
128
+ prepareJsonResponse();
129
+
130
+ const result = await model.doGenerate({
131
+ audio: audioData,
132
+ mediaType: 'audio/wav',
133
+ });
134
+
135
+ expect(result.text).toBe('Hello from the Vercel AI SDK!');
136
+ });
137
+
138
+ it('should include response data with timestamp, modelId and headers', async () => {
139
+ prepareJsonResponse({
140
+ headers: {
141
+ 'x-request-id': 'test-request-id',
142
+ 'x-ratelimit-remaining': '123',
143
+ },
144
+ });
145
+
146
+ const testDate = new Date(0);
147
+ const customModel = new OpenAITranscriptionModel('whisper-1', {
148
+ provider: 'test-provider',
149
+ url: () => 'https://api.openai.com/v1/audio/transcriptions',
150
+ headers: () => ({}),
151
+ _internal: {
152
+ currentDate: () => testDate,
153
+ },
154
+ });
155
+
156
+ const result = await customModel.doGenerate({
157
+ audio: audioData,
158
+ mediaType: 'audio/wav',
159
+ });
160
+
161
+ expect(result.response).toMatchObject({
162
+ timestamp: testDate,
163
+ modelId: 'whisper-1',
164
+ headers: {
165
+ 'content-type': 'application/json',
166
+ 'x-request-id': 'test-request-id',
167
+ 'x-ratelimit-remaining': '123',
168
+ },
169
+ });
170
+ });
171
+
172
+ it('should use real date when no custom date provider is specified', async () => {
173
+ prepareJsonResponse();
174
+
175
+ const testDate = new Date(0);
176
+ const customModel = new OpenAITranscriptionModel('whisper-1', {
177
+ provider: 'test-provider',
178
+ url: () => 'https://api.openai.com/v1/audio/transcriptions',
179
+ headers: () => ({}),
180
+ _internal: {
181
+ currentDate: () => testDate,
182
+ },
183
+ });
184
+
185
+ const result = await customModel.doGenerate({
186
+ audio: audioData,
187
+ mediaType: 'audio/wav',
188
+ });
189
+
190
+ expect(result.response.timestamp.getTime()).toEqual(testDate.getTime());
191
+ expect(result.response.modelId).toBe('whisper-1');
192
+ });
193
+
194
+ it('should pass response_format when `providerOptions.openai.timestampGranularities` is set', async () => {
195
+ prepareJsonResponse();
196
+
197
+ await model.doGenerate({
198
+ audio: audioData,
199
+ mediaType: 'audio/wav',
200
+ providerOptions: {
201
+ openai: {
202
+ timestampGranularities: ['word'],
203
+ },
204
+ },
205
+ });
206
+
207
+ expect(await server.calls[0].requestBodyMultipart).toMatchInlineSnapshot(`
208
+ {
209
+ "file": File {
210
+ Symbol(kHandle): Blob {},
211
+ Symbol(kLength): 40169,
212
+ Symbol(kType): "audio/wav",
213
+ },
214
+ "model": "whisper-1",
215
+ "response_format": "verbose_json",
216
+ "temperature": "0",
217
+ "timestamp_granularities[]": "word",
218
+ }
219
+ `);
220
+ });
221
+
222
+ it('should not set pass response_format to "verbose_json" when model is "gpt-4o-transcribe"', async () => {
223
+ prepareJsonResponse();
224
+
225
+ const model = provider.transcription('gpt-4o-transcribe');
226
+ await model.doGenerate({
227
+ audio: audioData,
228
+ mediaType: 'audio/wav',
229
+ providerOptions: {
230
+ openai: {
231
+ timestampGranularities: ['word'],
232
+ },
233
+ },
234
+ });
235
+
236
+ expect(await server.calls[0].requestBodyMultipart).toMatchInlineSnapshot(`
237
+ {
238
+ "file": File {
239
+ Symbol(kHandle): Blob {},
240
+ Symbol(kLength): 40169,
241
+ Symbol(kType): "audio/wav",
242
+ },
243
+ "model": "gpt-4o-transcribe",
244
+ "response_format": "json",
245
+ "temperature": "0",
246
+ "timestamp_granularities[]": "word",
247
+ }
248
+ `);
249
+ });
250
+
251
+ it('should pass timestamp_granularities when specified', async () => {
252
+ prepareJsonResponse();
253
+
254
+ await model.doGenerate({
255
+ audio: audioData,
256
+ mediaType: 'audio/wav',
257
+ providerOptions: {
258
+ openai: {
259
+ timestampGranularities: ['segment'],
260
+ },
261
+ },
262
+ });
263
+
264
+ expect(await server.calls[0].requestBodyMultipart).toMatchInlineSnapshot(`
265
+ {
266
+ "file": File {
267
+ Symbol(kHandle): Blob {},
268
+ Symbol(kLength): 40169,
269
+ Symbol(kType): "audio/wav",
270
+ },
271
+ "model": "whisper-1",
272
+ "response_format": "verbose_json",
273
+ "temperature": "0",
274
+ "timestamp_granularities[]": "segment",
275
+ }
276
+ `);
277
+ });
278
+
279
+ it('should work when no words, language, or duration are returned', async () => {
280
+ server.urls['https://api.openai.com/v1/audio/transcriptions'].response = {
281
+ type: 'json-value',
282
+ body: {
283
+ task: 'transcribe',
284
+ text: 'Hello from the Vercel AI SDK!',
285
+ _request_id: 'req_1234',
286
+ },
287
+ };
288
+
289
+ const testDate = new Date(0);
290
+ const customModel = new OpenAITranscriptionModel('whisper-1', {
291
+ provider: 'test-provider',
292
+ url: () => 'https://api.openai.com/v1/audio/transcriptions',
293
+ headers: () => ({}),
294
+ _internal: {
295
+ currentDate: () => testDate,
296
+ },
297
+ });
298
+
299
+ const result = await customModel.doGenerate({
300
+ audio: audioData,
301
+ mediaType: 'audio/wav',
302
+ });
303
+
304
+ expect(result).toMatchInlineSnapshot(`
305
+ {
306
+ "durationInSeconds": undefined,
307
+ "language": undefined,
308
+ "response": {
309
+ "body": {
310
+ "_request_id": "req_1234",
311
+ "task": "transcribe",
312
+ "text": "Hello from the Vercel AI SDK!",
313
+ },
314
+ "headers": {
315
+ "content-length": "85",
316
+ "content-type": "application/json",
317
+ },
318
+ "modelId": "whisper-1",
319
+ "timestamp": 1970-01-01T00:00:00.000Z,
320
+ },
321
+ "segments": [],
322
+ "text": "Hello from the Vercel AI SDK!",
323
+ "warnings": [],
324
+ }
325
+ `);
326
+ });
327
+
328
+ it('should parse segments when provided in response', async () => {
329
+ server.urls['https://api.openai.com/v1/audio/transcriptions'].response = {
330
+ type: 'json-value',
331
+ body: {
332
+ task: 'transcribe',
333
+ text: 'Hello world. How are you?',
334
+ segments: [
335
+ {
336
+ id: 0,
337
+ seek: 0,
338
+ start: 0.0,
339
+ end: 2.5,
340
+ text: 'Hello world.',
341
+ tokens: [1234, 5678],
342
+ temperature: 0.0,
343
+ avg_logprob: -0.5,
344
+ compression_ratio: 1.2,
345
+ no_speech_prob: 0.1,
346
+ },
347
+ {
348
+ id: 1,
349
+ seek: 250,
350
+ start: 2.5,
351
+ end: 5.0,
352
+ text: ' How are you?',
353
+ tokens: [9012, 3456],
354
+ temperature: 0.0,
355
+ avg_logprob: -0.6,
356
+ compression_ratio: 1.1,
357
+ no_speech_prob: 0.05,
358
+ },
359
+ ],
360
+ language: 'en',
361
+ duration: 5.0,
362
+ _request_id: 'req_1234',
363
+ },
364
+ };
365
+
366
+ const result = await model.doGenerate({
367
+ audio: audioData,
368
+ mediaType: 'audio/wav',
369
+ providerOptions: {
370
+ openai: {
371
+ timestampGranularities: ['segment'],
372
+ },
373
+ },
374
+ });
375
+
376
+ expect(result.segments).toMatchInlineSnapshot(`
377
+ [
378
+ {
379
+ "endSecond": 2.5,
380
+ "startSecond": 0,
381
+ "text": "Hello world.",
382
+ },
383
+ {
384
+ "endSecond": 5,
385
+ "startSecond": 2.5,
386
+ "text": " How are you?",
387
+ },
388
+ ]
389
+ `);
390
+ expect(result.text).toBe('Hello world. How are you?');
391
+ expect(result.durationInSeconds).toBe(5.0);
392
+ });
393
+
394
+ it('should fallback to words when segments are not available', async () => {
395
+ server.urls['https://api.openai.com/v1/audio/transcriptions'].response = {
396
+ type: 'json-value',
397
+ body: {
398
+ task: 'transcribe',
399
+ text: 'Hello world',
400
+ words: [
401
+ {
402
+ word: 'Hello',
403
+ start: 0.0,
404
+ end: 1.0,
405
+ },
406
+ {
407
+ word: 'world',
408
+ start: 1.0,
409
+ end: 2.0,
410
+ },
411
+ ],
412
+ language: 'en',
413
+ duration: 2.0,
414
+ _request_id: 'req_1234',
415
+ },
416
+ };
417
+
418
+ const result = await model.doGenerate({
419
+ audio: audioData,
420
+ mediaType: 'audio/wav',
421
+ providerOptions: {
422
+ openai: {
423
+ timestampGranularities: ['word'],
424
+ },
425
+ },
426
+ });
427
+
428
+ expect(result.segments).toMatchInlineSnapshot(`
429
+ [
430
+ {
431
+ "endSecond": 1,
432
+ "startSecond": 0,
433
+ "text": "Hello",
434
+ },
435
+ {
436
+ "endSecond": 2,
437
+ "startSecond": 1,
438
+ "text": "world",
439
+ },
440
+ ]
441
+ `);
442
+ });
443
+
444
+ it('should handle empty segments array', async () => {
445
+ server.urls['https://api.openai.com/v1/audio/transcriptions'].response = {
446
+ type: 'json-value',
447
+ body: {
448
+ task: 'transcribe',
449
+ text: 'Hello world',
450
+ segments: [],
451
+ language: 'en',
452
+ duration: 2.0,
453
+ _request_id: 'req_1234',
454
+ },
455
+ };
456
+
457
+ const result = await model.doGenerate({
458
+ audio: audioData,
459
+ mediaType: 'audio/wav',
460
+ });
461
+
462
+ expect(result.segments).toEqual([]);
463
+ expect(result.text).toBe('Hello world');
464
+ });
465
+
466
+ it('should handle segments with missing optional fields', async () => {
467
+ server.urls['https://api.openai.com/v1/audio/transcriptions'].response = {
468
+ type: 'json-value',
469
+ body: {
470
+ task: 'transcribe',
471
+ text: 'Test',
472
+ segments: [
473
+ {
474
+ id: 0,
475
+ seek: 0,
476
+ start: 0.0,
477
+ end: 1.0,
478
+ text: 'Test',
479
+ tokens: [1234],
480
+ temperature: 0.0,
481
+ avg_logprob: -0.5,
482
+ compression_ratio: 1.0,
483
+ no_speech_prob: 0.1,
484
+ },
485
+ ],
486
+ _request_id: 'req_1234',
487
+ },
488
+ };
489
+
490
+ const result = await model.doGenerate({
491
+ audio: audioData,
492
+ mediaType: 'audio/wav',
493
+ });
494
+
495
+ expect(result.segments).toMatchInlineSnapshot(`
496
+ [
497
+ {
498
+ "endSecond": 1,
499
+ "startSecond": 0,
500
+ "text": "Test",
501
+ },
502
+ ]
503
+ `);
504
+ expect(result.language).toBeUndefined();
505
+ expect(result.durationInSeconds).toBeUndefined();
506
+ });
507
+ });