@ai-sdk/openai 4.0.5 → 4.0.7

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.
@@ -2698,13 +2698,23 @@ The following provider options are available:
2698
2698
  - **include** _string[]_
2699
2699
  Additional information to include in the transcription response.
2700
2700
 
2701
+ - **streaming** _object_
2702
+ Options for streaming transcription models such as `gpt-realtime-whisper`.
2703
+ Use with `experimental_streamTranscribe`.
2704
+ - **delay** _'minimal' | 'low' | 'medium' | 'high' | 'xhigh'_
2705
+ Latency/accuracy tradeoff for realtime transcription.
2706
+
2707
+ - **include** _string[]_
2708
+ Additional fields to include in realtime transcription events, such as `item.input_audio_transcription.logprobs`.
2709
+
2701
2710
  ### Model Capabilities
2702
2711
 
2703
- | Model | Transcription | Duration | Segments | Language |
2704
- | ------------------------ | ------------------- | ------------------- | ------------------- | ------------------- |
2705
- | `whisper-1` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
2706
- | `gpt-4o-mini-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
2707
- | `gpt-4o-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
2712
+ | Model | Transcription | Streaming | Duration | Segments | Language |
2713
+ | ------------------------ | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
2714
+ | `whisper-1` | <Check size={18} /> | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
2715
+ | `gpt-4o-mini-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
2716
+ | `gpt-4o-transcribe` | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
2717
+ | `gpt-realtime-whisper` | <Cross size={18} /> | <Check size={18} /> | <Cross size={18} /> | <Cross size={18} /> | <Cross size={18} /> |
2708
2718
 
2709
2719
  ## Speech Models
2710
2720
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,8 +35,8 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.1",
39
- "@ai-sdk/provider-utils": "5.0.3"
38
+ "@ai-sdk/provider": "4.0.2",
39
+ "@ai-sdk/provider-utils": "5.0.5"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "22.19.19",
@@ -1,10 +1,14 @@
1
- import type { FetchFunction } from '@ai-sdk/provider-utils';
1
+ import type {
2
+ FetchFunction,
3
+ WebSocketConstructor,
4
+ } from '@ai-sdk/provider-utils';
2
5
 
3
6
  export type OpenAIConfig = {
4
7
  provider: string;
5
8
  url: (options: { modelId: string; path: string }) => string;
6
9
  headers?: () => Record<string, string | undefined>;
7
10
  fetch?: FetchFunction;
11
+ webSocket?: WebSocketConstructor;
8
12
  generateId?: () => string;
9
13
  /**
10
14
  * This is soft-deprecated. Use provider references (e.g. `{ openai: 'file-abc123' }`)
@@ -16,6 +16,7 @@ import {
16
16
  withoutTrailingSlash,
17
17
  withUserAgentSuffix,
18
18
  type FetchFunction,
19
+ type WebSocketConstructor,
19
20
  } from '@ai-sdk/provider-utils';
20
21
  import { OpenAIChatLanguageModel } from './chat/openai-chat-language-model';
21
22
  import type { OpenAIChatModelId } from './chat/openai-chat-language-model-options';
@@ -158,6 +159,12 @@ export interface OpenAIProviderSettings {
158
159
  * or to provide a custom fetch implementation for e.g. testing.
159
160
  */
160
161
  fetch?: FetchFunction;
162
+
163
+ /**
164
+ * Custom WebSocket implementation. This is useful for testing or for
165
+ * runtimes that need a WebSocket constructor with header support.
166
+ */
167
+ webSocket?: WebSocketConstructor;
161
168
  }
162
169
 
163
170
  /**
@@ -229,6 +236,7 @@ export function createOpenAI(
229
236
  url: ({ path }) => `${baseURL}${path}`,
230
237
  headers: getHeaders,
231
238
  fetch: options.fetch,
239
+ webSocket: options.webSocket,
232
240
  });
233
241
 
234
242
  const createSpeechModel = (modelId: OpenAISpeechModelId) =>
@@ -12,6 +12,7 @@ export type OpenAITranscriptionModelId =
12
12
  | 'gpt-4o-mini-transcribe-2025-12-15'
13
13
  | 'gpt-4o-transcribe'
14
14
  | 'gpt-4o-transcribe-diarize'
15
+ | 'gpt-realtime-whisper'
15
16
  | (string & {});
16
17
 
17
18
  // https://platform.openai.com/docs/api-reference/audio/createTranscription
@@ -48,6 +49,25 @@ export const openAITranscriptionModelOptions = lazySchema(() =>
48
49
  .array(z.enum(['word', 'segment']))
49
50
  .default(['segment'])
50
51
  .optional(),
52
+
53
+ /**
54
+ * Options for streaming transcription models such as `gpt-realtime-whisper`.
55
+ */
56
+ streaming: z
57
+ .object({
58
+ /**
59
+ * Latency/accuracy tradeoff for realtime transcription.
60
+ */
61
+ delay: z
62
+ .enum(['minimal', 'low', 'medium', 'high', 'xhigh'])
63
+ .optional(),
64
+
65
+ /**
66
+ * Additional fields to include in realtime transcription events.
67
+ */
68
+ include: z.array(z.string()).optional(),
69
+ })
70
+ .optional(),
51
71
  }),
52
72
  ),
53
73
  );
@@ -1,16 +1,23 @@
1
- import type {
2
- TranscriptionModelV4,
3
- TranscriptionModelV4CallOptions,
4
- SharedV4Warning,
1
+ import {
2
+ UnsupportedFunctionalityError,
3
+ type Experimental_TranscriptionModelV4StreamOptions as TranscriptionModelV4StreamOptions,
4
+ type SharedV4Warning,
5
+ type TranscriptionModelV4,
6
+ type TranscriptionModelV4CallOptions,
5
7
  } from '@ai-sdk/provider';
6
8
  import {
7
9
  combineHeaders,
8
10
  convertBase64ToUint8Array,
11
+ convertToBase64,
9
12
  createJsonResponseHandler,
13
+ getWebSocketConstructor,
10
14
  mediaTypeToExtension,
11
15
  parseProviderOptions,
12
16
  postFormDataToApi,
17
+ readWebSocketMessageText,
18
+ safeParseJSON,
13
19
  serializeModelOptions,
20
+ toWebSocketUrl,
14
21
  WORKFLOW_DESERIALIZE,
15
22
  WORKFLOW_SERIALIZE,
16
23
  } from '@ai-sdk/provider-utils';
@@ -31,6 +38,35 @@ export type OpenAITranscriptionCallOptions = Omit<
31
38
  };
32
39
  };
33
40
 
41
+ export type OpenAITranscriptionStreamOptions = Omit<
42
+ TranscriptionModelV4StreamOptions,
43
+ 'providerOptions'
44
+ > & {
45
+ providerOptions?: {
46
+ openai?: OpenAITranscriptionModelOptions;
47
+ };
48
+ };
49
+
50
+ type OpenAIRealtimeTranscriptionEvent = {
51
+ type?: string;
52
+ item_id?: string;
53
+ delta?: string;
54
+ transcript?: string;
55
+ error?: { message?: string };
56
+ };
57
+
58
+ /**
59
+ * Realtime transcription model IDs stream over the realtime WebSocket
60
+ * and do not support the REST transcription endpoint. Prefix matching
61
+ * keeps dated snapshots (e.g. `gpt-realtime-whisper-2026-01-01`) working.
62
+ */
63
+ function isRealtimeTranscriptionModelId(modelId: string): boolean {
64
+ return (
65
+ modelId === 'gpt-realtime-whisper' ||
66
+ modelId.startsWith('gpt-realtime-whisper-')
67
+ );
68
+ }
69
+
34
70
  interface OpenAITranscriptionModelConfig extends OpenAIConfig {
35
71
  _internal?: {
36
72
  currentDate?: () => Date;
@@ -199,6 +235,12 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
199
235
  async doGenerate(
200
236
  options: OpenAITranscriptionCallOptions,
201
237
  ): Promise<Awaited<ReturnType<TranscriptionModelV4['doGenerate']>>> {
238
+ if (isRealtimeTranscriptionModelId(this.modelId)) {
239
+ throw new UnsupportedFunctionalityError({
240
+ functionality: `non-streaming transcription with ${this.modelId}`,
241
+ });
242
+ }
243
+
202
244
  const currentDate = this.config._internal?.currentDate?.() ?? new Date();
203
245
  const { formData, warnings } = await this.getArgs(options);
204
246
 
@@ -251,4 +293,286 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
251
293
  },
252
294
  };
253
295
  }
296
+
297
+ async doStream(
298
+ options: OpenAITranscriptionStreamOptions,
299
+ ): Promise<
300
+ Awaited<ReturnType<NonNullable<TranscriptionModelV4['doStream']>>>
301
+ > {
302
+ if (!isRealtimeTranscriptionModelId(this.modelId)) {
303
+ throw new UnsupportedFunctionalityError({
304
+ functionality: `streaming transcription with ${this.modelId}`,
305
+ });
306
+ }
307
+
308
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
309
+ const openAIOptions = await parseProviderOptions({
310
+ provider: 'openai',
311
+ providerOptions: options.providerOptions,
312
+ schema: openAITranscriptionModelOptions,
313
+ });
314
+ const warnings: SharedV4Warning[] = [];
315
+
316
+ // options that only apply to the REST transcription endpoint
317
+ // (checked on the raw options because some have schema defaults):
318
+ const rawOpenAIOptions = options.providerOptions?.openai ?? {};
319
+ for (const option of [
320
+ 'include',
321
+ 'prompt',
322
+ 'temperature',
323
+ 'timestampGranularities',
324
+ ]) {
325
+ if (rawOpenAIOptions[option as keyof typeof rawOpenAIOptions] != null) {
326
+ warnings.push({
327
+ type: 'unsupported',
328
+ feature: `providerOptions.openai.${option}`,
329
+ details: `OpenAI streaming transcription does not support ${option}.`,
330
+ });
331
+ }
332
+ }
333
+
334
+ const headers = combineHeaders(this.config.headers?.(), options.headers);
335
+ const sessionUpdate = buildOpenAIRealtimeTranscriptionSession({
336
+ modelId: this.modelId,
337
+ inputAudioFormat: options.inputAudioFormat,
338
+ providerOptions: openAIOptions,
339
+ });
340
+
341
+ return {
342
+ request: { body: sessionUpdate },
343
+ response: {
344
+ timestamp: currentDate,
345
+ modelId: this.modelId,
346
+ },
347
+ stream: createOpenAIRealtimeTranscriptionStream({
348
+ webSocket: this.config.webSocket,
349
+ url: toWebSocketUrl(
350
+ this.config.url({
351
+ path: '/realtime?intent=transcription',
352
+ modelId: this.modelId,
353
+ }),
354
+ ),
355
+ headers,
356
+ sessionUpdate,
357
+ language: openAIOptions?.language,
358
+ warnings,
359
+ audio: options.audio,
360
+ abortSignal: options.abortSignal,
361
+ includeRawChunks: options.includeRawChunks,
362
+ }),
363
+ };
364
+ }
365
+ }
366
+
367
+ function createOpenAIRealtimeTranscriptionStream({
368
+ webSocket,
369
+ url,
370
+ headers,
371
+ sessionUpdate,
372
+ language,
373
+ warnings,
374
+ audio,
375
+ abortSignal,
376
+ includeRawChunks,
377
+ }: {
378
+ webSocket: OpenAIConfig['webSocket'];
379
+ url: URL;
380
+ headers: Record<string, string | undefined>;
381
+ sessionUpdate: unknown;
382
+ language: string | undefined;
383
+ warnings: SharedV4Warning[];
384
+ audio: ReadableStream<Uint8Array | string>;
385
+ abortSignal: AbortSignal | undefined;
386
+ includeRawChunks: boolean | undefined;
387
+ }) {
388
+ let finished = false;
389
+ let cleanup: (closeCode?: number) => void = () => {};
390
+
391
+ return new ReadableStream({
392
+ start: controller => {
393
+ const WebSocketConstructor = getWebSocketConstructor(webSocket);
394
+ const ws = new WebSocketConstructor(
395
+ url,
396
+ getOpenAIRealtimeProtocols(headers),
397
+ { headers },
398
+ );
399
+ let audioReader:
400
+ | ReadableStreamDefaultReader<Uint8Array | string>
401
+ | undefined;
402
+
403
+ cleanup = (closeCode?: number) => {
404
+ abortSignal?.removeEventListener('abort', abort);
405
+ void audioReader?.cancel().catch(() => {});
406
+ try {
407
+ ws.close(closeCode);
408
+ } catch {}
409
+ };
410
+
411
+ const finishWithError = (error: unknown) => {
412
+ if (finished) return;
413
+ finished = true;
414
+ cleanup();
415
+ controller.error(error);
416
+ };
417
+
418
+ const finish = (text: string, id?: string) => {
419
+ if (finished) return;
420
+ finished = true;
421
+ if (id != null) {
422
+ controller.enqueue({ type: 'transcript-final', id, text });
423
+ }
424
+ controller.enqueue({
425
+ type: 'finish',
426
+ text,
427
+ segments: [],
428
+ language,
429
+ });
430
+ controller.close();
431
+ cleanup(1000);
432
+ };
433
+
434
+ const abort = () => {
435
+ finishWithError(abortSignal?.reason ?? new Error('Aborted'));
436
+ };
437
+ if (abortSignal?.aborted) {
438
+ abort();
439
+ return;
440
+ }
441
+ abortSignal?.addEventListener('abort', abort, { once: true });
442
+
443
+ const sendAudio = async () => {
444
+ audioReader = audio.getReader();
445
+ try {
446
+ while (true) {
447
+ const { done, value } = await audioReader.read();
448
+ if (done || finished) break;
449
+ ws.send(
450
+ JSON.stringify({
451
+ type: 'input_audio_buffer.append',
452
+ audio: convertToBase64(value),
453
+ }),
454
+ );
455
+ }
456
+ } finally {
457
+ audioReader.releaseLock();
458
+ }
459
+ if (!finished) {
460
+ ws.send(JSON.stringify({ type: 'input_audio_buffer.commit' }));
461
+ }
462
+ };
463
+
464
+ ws.onopen = () => {
465
+ controller.enqueue({ type: 'stream-start', warnings });
466
+ ws.send(JSON.stringify(sessionUpdate));
467
+ void sendAudio().catch(finishWithError);
468
+ };
469
+
470
+ ws.onmessage = event => {
471
+ void readWebSocketMessageText(event.data)
472
+ .then(async text => {
473
+ const parsed = await safeParseJSON({ text });
474
+ if (!parsed.success) return;
475
+ const raw = parsed.value as OpenAIRealtimeTranscriptionEvent;
476
+
477
+ if (includeRawChunks) {
478
+ controller.enqueue({ type: 'raw', rawValue: raw });
479
+ }
480
+
481
+ switch (raw.type) {
482
+ case 'conversation.item.input_audio_transcription.delta': {
483
+ controller.enqueue({
484
+ type: 'transcript-delta',
485
+ id: raw.item_id,
486
+ delta: raw.delta ?? '',
487
+ });
488
+ break;
489
+ }
490
+
491
+ case 'conversation.item.input_audio_transcription.completed': {
492
+ finish(raw.transcript ?? '', raw.item_id);
493
+ break;
494
+ }
495
+
496
+ case 'error': {
497
+ finishWithError(
498
+ new Error(raw.error?.message ?? 'OpenAI realtime error'),
499
+ );
500
+ break;
501
+ }
502
+ }
503
+ })
504
+ .catch(finishWithError);
505
+ };
506
+
507
+ ws.onerror = () => {
508
+ finishWithError(new Error('OpenAI realtime transcription error'));
509
+ };
510
+
511
+ ws.onclose = () => {
512
+ if (finished) return;
513
+ finished = true;
514
+ cleanup();
515
+ controller.close();
516
+ };
517
+ },
518
+
519
+ cancel: () => {
520
+ if (finished) return;
521
+ finished = true;
522
+ cleanup();
523
+ },
524
+ });
525
+ }
526
+
527
+ function buildOpenAIRealtimeTranscriptionSession({
528
+ modelId,
529
+ inputAudioFormat,
530
+ providerOptions,
531
+ }: {
532
+ modelId: string;
533
+ inputAudioFormat: TranscriptionModelV4StreamOptions['inputAudioFormat'];
534
+ providerOptions: OpenAITranscriptionModelOptions | undefined;
535
+ }) {
536
+ return {
537
+ type: 'session.update',
538
+ session: {
539
+ type: 'transcription',
540
+ audio: {
541
+ input: {
542
+ format: {
543
+ type: inputAudioFormat.type,
544
+ ...(inputAudioFormat.rate != null
545
+ ? { rate: inputAudioFormat.rate }
546
+ : {}),
547
+ },
548
+ transcription: {
549
+ model: modelId,
550
+ ...(providerOptions?.language != null
551
+ ? { language: providerOptions.language }
552
+ : {}),
553
+ ...(providerOptions?.streaming?.delay != null
554
+ ? { delay: providerOptions.streaming.delay }
555
+ : {}),
556
+ },
557
+ turn_detection: null,
558
+ },
559
+ },
560
+ ...(providerOptions?.streaming?.include != null
561
+ ? { include: providerOptions.streaming.include }
562
+ : {}),
563
+ },
564
+ };
565
+ }
566
+
567
+ function getOpenAIRealtimeProtocols(
568
+ headers: Record<string, string | undefined>,
569
+ ): string[] {
570
+ const authorization = headers.Authorization ?? headers.authorization;
571
+ const token = authorization?.startsWith('Bearer ')
572
+ ? authorization.slice('Bearer '.length)
573
+ : undefined;
574
+
575
+ return token == null
576
+ ? ['realtime']
577
+ : ['realtime', `openai-insecure-api-key.${token}`];
254
578
  }