@ai-sdk/openai 3.0.17 → 3.0.19

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 (70) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.d.mts +20 -1
  3. package/dist/index.d.ts +20 -1
  4. package/dist/index.js +15 -17
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +15 -17
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.d.mts +20 -1
  9. package/dist/internal/index.d.ts +20 -1
  10. package/dist/internal/index.js +14 -16
  11. package/dist/internal/index.js.map +1 -1
  12. package/dist/internal/index.mjs +14 -16
  13. package/dist/internal/index.mjs.map +1 -1
  14. package/docs/03-openai.mdx +67 -11
  15. package/package.json +8 -4
  16. package/src/index.ts +2 -0
  17. package/src/responses/openai-responses-language-model.ts +24 -26
  18. package/src/responses/openai-responses-provider-metadata.ts +23 -1
  19. package/src/chat/__fixtures__/azure-model-router.1.chunks.txt +0 -8
  20. package/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap +0 -88
  21. package/src/chat/convert-to-openai-chat-messages.test.ts +0 -516
  22. package/src/chat/openai-chat-language-model.test.ts +0 -3496
  23. package/src/chat/openai-chat-prepare-tools.test.ts +0 -322
  24. package/src/completion/openai-completion-language-model.test.ts +0 -752
  25. package/src/embedding/__snapshots__/openai-embedding-model.test.ts.snap +0 -43
  26. package/src/embedding/openai-embedding-model.test.ts +0 -146
  27. package/src/image/openai-image-model.test.ts +0 -722
  28. package/src/openai-error.test.ts +0 -34
  29. package/src/openai-language-model-capabilities.test.ts +0 -93
  30. package/src/openai-provider.test.ts +0 -98
  31. package/src/responses/__fixtures__/openai-apply-patch-tool-delete.1.chunks.txt +0 -5
  32. package/src/responses/__fixtures__/openai-apply-patch-tool.1.chunks.txt +0 -38
  33. package/src/responses/__fixtures__/openai-apply-patch-tool.1.json +0 -69
  34. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.chunks.txt +0 -393
  35. package/src/responses/__fixtures__/openai-code-interpreter-tool.1.json +0 -137
  36. package/src/responses/__fixtures__/openai-error.1.chunks.txt +0 -4
  37. package/src/responses/__fixtures__/openai-error.1.json +0 -8
  38. package/src/responses/__fixtures__/openai-file-search-tool.1.chunks.txt +0 -94
  39. package/src/responses/__fixtures__/openai-file-search-tool.1.json +0 -89
  40. package/src/responses/__fixtures__/openai-file-search-tool.2.chunks.txt +0 -93
  41. package/src/responses/__fixtures__/openai-file-search-tool.2.json +0 -112
  42. package/src/responses/__fixtures__/openai-image-generation-tool.1.chunks.txt +0 -16
  43. package/src/responses/__fixtures__/openai-image-generation-tool.1.json +0 -96
  44. package/src/responses/__fixtures__/openai-local-shell-tool.1.chunks.txt +0 -7
  45. package/src/responses/__fixtures__/openai-local-shell-tool.1.json +0 -70
  46. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.chunks.txt +0 -11
  47. package/src/responses/__fixtures__/openai-mcp-tool-approval.1.json +0 -169
  48. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.chunks.txt +0 -123
  49. package/src/responses/__fixtures__/openai-mcp-tool-approval.2.json +0 -176
  50. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.chunks.txt +0 -11
  51. package/src/responses/__fixtures__/openai-mcp-tool-approval.3.json +0 -169
  52. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.chunks.txt +0 -84
  53. package/src/responses/__fixtures__/openai-mcp-tool-approval.4.json +0 -182
  54. package/src/responses/__fixtures__/openai-mcp-tool.1.chunks.txt +0 -373
  55. package/src/responses/__fixtures__/openai-mcp-tool.1.json +0 -159
  56. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.chunks.txt +0 -110
  57. package/src/responses/__fixtures__/openai-reasoning-encrypted-content.1.json +0 -117
  58. package/src/responses/__fixtures__/openai-shell-tool.1.chunks.txt +0 -182
  59. package/src/responses/__fixtures__/openai-shell-tool.1.json +0 -73
  60. package/src/responses/__fixtures__/openai-web-search-tool.1.chunks.txt +0 -185
  61. package/src/responses/__fixtures__/openai-web-search-tool.1.json +0 -266
  62. package/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap +0 -10955
  63. package/src/responses/convert-to-openai-responses-input.test.ts +0 -3223
  64. package/src/responses/openai-responses-api.test.ts +0 -89
  65. package/src/responses/openai-responses-language-model.test.ts +0 -6927
  66. package/src/responses/openai-responses-prepare-tools.test.ts +0 -924
  67. package/src/speech/openai-speech-model.test.ts +0 -202
  68. package/src/tool/local-shell.test-d.ts +0 -20
  69. package/src/tool/web-search.test-d.ts +0 -13
  70. package/src/transcription/openai-transcription-model.test.ts +0 -507
@@ -1,507 +0,0 @@
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
- });