@ai-sdk/openai 4.0.103 → 4.0.105

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.
@@ -58,14 +58,43 @@ type OpenAIRealtimeTranscriptionEvent = {
58
58
  };
59
59
 
60
60
  /**
61
- * Realtime transcription model IDs stream over the realtime WebSocket
62
- * and do not support the REST transcription endpoint. Prefix matching
63
- * keeps dated snapshots (e.g. `gpt-realtime-whisper-2026-01-01`) working.
61
+ * Whether `modelId` is `base` or one of its dated snapshots
62
+ * (e.g. `gpt-realtime-whisper-2026-01-01`).
63
+ */
64
+ function isModelOrSnapshot(modelId: string, base: string): boolean {
65
+ return modelId === base || modelId.startsWith(`${base}-`);
66
+ }
67
+
68
+ /**
69
+ * Model IDs that stream over the realtime transcription WebSocket and do not
70
+ * support the REST transcription endpoint.
64
71
  */
65
72
  function isRealtimeTranscriptionModelId(modelId: string): boolean {
66
73
  return (
67
- modelId === 'gpt-realtime-whisper' ||
68
- modelId.startsWith('gpt-realtime-whisper-')
74
+ isModelOrSnapshot(modelId, 'gpt-realtime-whisper') ||
75
+ isModelOrSnapshot(modelId, 'gpt-live-transcribe')
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Model IDs that support streaming transcription. `gpt-transcribe` also
81
+ * supports the REST transcription endpoint.
82
+ */
83
+ function supportsStreamingTranscription(modelId: string): boolean {
84
+ return (
85
+ isRealtimeTranscriptionModelId(modelId) ||
86
+ isModelOrSnapshot(modelId, 'gpt-transcribe')
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Models whose realtime transcription session takes a `languages` list
92
+ * instead of the singular `language` field.
93
+ */
94
+ function usesLanguagesList(modelId: string): boolean {
95
+ return (
96
+ isModelOrSnapshot(modelId, 'gpt-live-transcribe') ||
97
+ isModelOrSnapshot(modelId, 'gpt-transcribe')
69
98
  );
70
99
  }
71
100
 
@@ -197,54 +226,45 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
197
226
  `audio.${fileExtension}`,
198
227
  );
199
228
 
200
- if (this.modelId === 'whisper-1') {
201
- formData.append('response_format', 'verbose_json');
202
- }
203
-
204
229
  const isDiarizationModel = this.modelId === 'gpt-4o-transcribe-diarize';
230
+
231
+ const responseFormat =
232
+ openAIOptions?.responseFormat ??
233
+ (isDiarizationModel
234
+ ? 'diarized_json'
235
+ : this.modelId === 'whisper-1'
236
+ ? 'verbose_json'
237
+ : undefined);
238
+
239
+ // Timestamps are only returned with verbose_json, so only default to
240
+ // segments when that format is used.
241
+ const timestampGranularities =
242
+ openAIOptions?.timestampGranularities ??
243
+ (responseFormat === 'verbose_json' ? ['segment'] : undefined);
244
+
205
245
  const chunkingStrategy =
206
246
  openAIOptions?.chunkingStrategy ??
207
247
  (isDiarizationModel ? 'auto' : undefined);
208
248
 
209
- // Add provider-specific options
210
- if (openAIOptions) {
211
- const isGpt4oTranscribeModel = [
212
- 'gpt-4o-transcribe',
213
- 'gpt-4o-mini-transcribe',
214
- ].includes(this.modelId);
215
-
216
- const transcriptionModelOptions = {
217
- include: openAIOptions.include,
218
- language: openAIOptions.language,
219
- prompt: openAIOptions.prompt,
220
- // https://platform.openai.com/docs/api-reference/audio/createTranscription#audio_createtranscription-response_format
221
- // prefer verbose_json to get segments for models that support it
222
- ...(this.modelId !== 'whisper-1' && {
223
- response_format:
224
- openAIOptions.responseFormat ??
225
- (isDiarizationModel
226
- ? 'diarized_json'
227
- : isGpt4oTranscribeModel
228
- ? 'json'
229
- : 'verbose_json'),
230
- }),
231
- temperature: openAIOptions.temperature,
232
- timestamp_granularities: openAIOptions.timestampGranularities,
233
- };
249
+ const transcriptionModelOptions = {
250
+ response_format: responseFormat,
251
+ include: openAIOptions?.include,
252
+ language: openAIOptions?.language,
253
+ prompt: openAIOptions?.prompt,
254
+ temperature: openAIOptions?.temperature,
255
+ timestamp_granularities: timestampGranularities,
256
+ };
234
257
 
235
- for (const [key, value] of Object.entries(transcriptionModelOptions)) {
236
- if (value != null) {
237
- if (Array.isArray(value)) {
238
- for (const item of value) {
239
- formData.append(`${key}[]`, String(item));
240
- }
241
- } else {
242
- formData.append(key, String(value));
258
+ for (const [key, value] of Object.entries(transcriptionModelOptions)) {
259
+ if (value != null) {
260
+ if (Array.isArray(value)) {
261
+ for (const item of value) {
262
+ formData.append(`${key}[]`, String(item));
243
263
  }
264
+ } else {
265
+ formData.append(key, String(value));
244
266
  }
245
267
  }
246
- } else if (isDiarizationModel) {
247
- formData.append('response_format', 'diarized_json');
248
268
  }
249
269
 
250
270
  if (chunkingStrategy != null) {
@@ -344,7 +364,13 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
344
364
  })) ??
345
365
  [],
346
366
  language,
347
- durationInSeconds: response.duration ?? undefined,
367
+ // Duration-billed json responses report the duration only in `usage`:
368
+ durationInSeconds:
369
+ response.duration ??
370
+ (response.usage?.type === 'duration' &&
371
+ typeof response.usage.seconds === 'number'
372
+ ? response.usage.seconds
373
+ : undefined),
348
374
  warnings,
349
375
  ...(response.usage != null && { usage: response.usage }),
350
376
  response: {
@@ -375,7 +401,7 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
375
401
  ): Promise<
376
402
  Awaited<ReturnType<NonNullable<TranscriptionModelV4['doStream']>>>
377
403
  > {
378
- if (!isRealtimeTranscriptionModelId(this.modelId)) {
404
+ if (!supportsStreamingTranscription(this.modelId)) {
379
405
  throw new UnsupportedFunctionalityError({
380
406
  functionality: `streaming transcription with ${this.modelId}`,
381
407
  });
@@ -407,11 +433,22 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
407
433
  }
408
434
  }
409
435
 
436
+ // gpt-transcribe transcribes each committed turn, so it has no delay setting:
437
+ const supportsDelay = !isModelOrSnapshot(this.modelId, 'gpt-transcribe');
438
+ if (!supportsDelay && openAIOptions?.streaming?.delay != null) {
439
+ warnings.push({
440
+ type: 'unsupported',
441
+ feature: 'providerOptions.openai.streaming.delay',
442
+ details: `OpenAI streaming transcription with ${this.modelId} does not support delay.`,
443
+ });
444
+ }
445
+
410
446
  const headers = combineHeaders(this.config.headers?.(), options.headers);
411
447
  const sessionUpdate = buildOpenAIRealtimeTranscriptionSession({
412
448
  modelId: this.modelId,
413
449
  inputAudioFormat: options.inputAudioFormat,
414
450
  providerOptions: openAIOptions,
451
+ supportsDelay,
415
452
  });
416
453
 
417
454
  return {
@@ -600,10 +637,12 @@ function buildOpenAIRealtimeTranscriptionSession({
600
637
  modelId,
601
638
  inputAudioFormat,
602
639
  providerOptions,
640
+ supportsDelay,
603
641
  }: {
604
642
  modelId: string;
605
643
  inputAudioFormat: TranscriptionModelV4StreamOptions['inputAudioFormat'];
606
644
  providerOptions: OpenAITranscriptionModelOptions | undefined;
645
+ supportsDelay: boolean;
607
646
  }) {
608
647
  return {
609
648
  type: 'session.update',
@@ -620,9 +659,11 @@ function buildOpenAIRealtimeTranscriptionSession({
620
659
  transcription: {
621
660
  model: modelId,
622
661
  ...(providerOptions?.language != null
623
- ? { language: providerOptions.language }
662
+ ? usesLanguagesList(modelId)
663
+ ? { languages: [providerOptions.language] }
664
+ : { language: providerOptions.language }
624
665
  : {}),
625
- ...(providerOptions?.streaming?.delay != null
666
+ ...(supportsDelay && providerOptions?.streaming?.delay != null
626
667
  ? { delay: providerOptions.streaming.delay }
627
668
  : {}),
628
669
  },