@ai-sdk/openai 4.0.103 → 4.0.104

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.
@@ -245,9 +245,9 @@ The following provider options are available:
245
245
  Reasoning effort for reasoning models. Defaults to `medium`. If you use `providerOptions` to set the `reasoningEffort` option, this model setting will be ignored.
246
246
 
247
247
  <Note>
248
- Supported reasoning efforts vary by model. GPT-5.6, GPT-6 Sol, and GPT-6 Luna
249
- support `'none'`, `'low'`, `'medium'`, `'high'`, `'xhigh'`, and `'max'`. GPT-6
250
- Astra and GPT-6.1 Sol do not support `'none'`.
248
+ Supported reasoning efforts vary by model. GPT-5.6, GPT-6 Sol, GPT-6 Luna, and
249
+ GPT-6.1 Terra support `'none'`, `'low'`, `'medium'`, `'high'`, `'xhigh'`, and
250
+ `'max'`. GPT-6 Astra and GPT-6.1 Sol do not support `'none'`.
251
251
  </Note>
252
252
 
253
253
  <Note type="warning">
@@ -3069,6 +3069,7 @@ The following optional provider options are available for OpenAI completion mode
3069
3069
  | --------------------- | ----------- | ----------- | ----------------- | ---------- |
3070
3070
  | `gpt-6-astra` | <Check /> | <Cross /> | <Check /> | <Check /> |
3071
3071
  | `gpt-6.1-sol` | <Check /> | <Cross /> | <Check /> | <Check /> |
3072
+ | `gpt-6.1-terra` | <Check /> | <Cross /> | <Check /> | <Check /> |
3072
3073
  | `gpt-6-luna` | <Check /> | <Cross /> | <Check /> | <Check /> |
3073
3074
  | `gpt-6-sol` | <Check /> | <Cross /> | <Check /> | <Check /> |
3074
3075
  | `gpt-5.6` | <Check /> | <Cross /> | <Check /> | <Check /> |
@@ -3508,12 +3509,13 @@ The following provider options are available:
3508
3509
 
3509
3510
  - **timestampGranularities** _string[]_
3510
3511
  The granularity of the timestamps in the transcription.
3511
- Defaults to `['segment']`.
3512
+ Defaults to `['segment']` when `responseFormat` is `verbose_json`.
3512
3513
  Possible values are `['word']`, `['segment']`, and `['word', 'segment']`.
3513
3514
  Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency.
3514
3515
 
3515
3516
  - **language** _string_
3516
3517
  The language of the input audio. Supplying the input language in ISO-639-1 format (e.g. 'en') will improve accuracy and latency.
3518
+ For streaming with `gpt-transcribe` and `gpt-live-transcribe`, it is sent as a single-entry `languages` list.
3517
3519
  Optional.
3518
3520
 
3519
3521
  - **prompt** _string_
@@ -3529,13 +3531,13 @@ The following provider options are available:
3529
3531
  Additional information to include in the transcription response.
3530
3532
 
3531
3533
  - **responseFormat** _'json' | 'verbose_json' | 'diarized_json'_
3532
- The format of the transcription response. `gpt-4o-transcribe-diarize` defaults to `diarized_json`.
3534
+ The format of the transcription response. `whisper-1` defaults to `verbose_json` and `gpt-4o-transcribe-diarize` defaults to `diarized_json`. Other models use the service default unless this option is set.
3533
3535
 
3534
3536
  - **chunkingStrategy** _'auto' | object_
3535
3537
  Controls how the audio is split into chunks. `gpt-4o-transcribe-diarize` defaults to `'auto'`; provide this option to override the default. The object form configures OpenAI server-side VAD with `type: 'server_vad'` and optional `threshold`, `prefixPaddingMs`, and `silenceDurationMs` fields.
3536
3538
 
3537
3539
  - **streaming** _object_
3538
- Options for streaming transcription models such as `gpt-realtime-whisper`.
3540
+ Options for streaming transcription models such as `gpt-live-transcribe`.
3539
3541
  Use with `experimental_streamTranscribe`.
3540
3542
  - **delay** _'minimal' | 'low' | 'medium' | 'high' | 'xhigh'_
3541
3543
  Latency/accuracy tradeoff for realtime transcription.
@@ -3551,6 +3553,8 @@ The following provider options are available:
3551
3553
  | `gpt-4o-mini-transcribe` | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
3552
3554
  | `gpt-4o-transcribe` | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
3553
3555
  | `gpt-4o-transcribe-diarize` | <Check /> | <Cross /> | <Check /> | <Check /> | <Cross /> |
3556
+ | `gpt-transcribe` | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> |
3557
+ | `gpt-live-transcribe` | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> |
3554
3558
  | `gpt-realtime-whisper` | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> |
3555
3559
 
3556
3560
  ## Translation Models
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.103",
3
+ "version": "4.0.104",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.34",
39
- "@ai-sdk/provider-utils": "5.0.69"
39
+ "@ai-sdk/provider-utils": "5.0.70"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.4",
@@ -72,6 +72,7 @@ export type OpenAIChatModelId =
72
72
  | 'gpt-5.6-terra'
73
73
  | 'gpt-6-astra'
74
74
  | 'gpt-6.1-sol'
75
+ | 'gpt-6.1-terra'
75
76
  | 'gpt-6-luna'
76
77
  | 'gpt-6-sol'
77
78
  | (string & {});
@@ -58,9 +58,10 @@ export function normalizeOpenAIJsonSchema(schema: JSONSchema7): {
58
58
  const propertyNames = schema.propertyNames;
59
59
 
60
60
  if (propertyNames != null) {
61
+ // Property names are always strings in JSON, so omitted type is a string schema.
61
62
  if (
62
63
  typeof propertyNames === 'boolean' ||
63
- propertyNames.type !== 'string'
64
+ (propertyNames.type != null && propertyNames.type !== 'string')
64
65
  ) {
65
66
  throw new UnsupportedFunctionalityError({
66
67
  functionality:
@@ -47,7 +47,7 @@ import {
47
47
  type OpenAIResponsesLogprobs,
48
48
  } from './responses/openai-responses-api';
49
49
  import {
50
- mapWebSearchOutput,
50
+ mapWebSearchResult,
51
51
  OpenAIResponsesLanguageModel,
52
52
  openaiResponsesSupportedUrls,
53
53
  } from './responses/openai-responses-language-model';
@@ -812,9 +812,7 @@ async function convertOpenAIBatchResult(
812
812
  type: 'tool-result',
813
813
  toolCallId: part.id,
814
814
  toolName: 'web_search',
815
- ...(part.status === 'failed' || part.status === 'incomplete'
816
- ? { isError: true, result: { status: part.status } }
817
- : { result: mapWebSearchOutput(part.action) }),
815
+ ...mapWebSearchResult(part),
818
816
  dynamic: true,
819
817
  });
820
818
  break;
@@ -24,7 +24,10 @@ export function getOpenAILanguageModelCapabilities(
24
24
  const isGptNanoModel = gptVersion?.variant?.startsWith('nano') ?? false;
25
25
  const isGpt6OrLaterModel = gptVersion != null && gptVersion.major >= 6;
26
26
 
27
- const isGpt6SolOrLuna = modelId === 'gpt-6-sol' || modelId === 'gpt-6-luna';
27
+ const isGpt6OrLaterModelWithNoneEffort =
28
+ modelId === 'gpt-6-sol' ||
29
+ modelId === 'gpt-6-luna' ||
30
+ modelId === 'gpt-6.1-terra';
28
31
 
29
32
  const supportsFlexProcessing =
30
33
  (oSeriesVersion != null && oSeriesVersion >= 3) ||
@@ -45,10 +48,10 @@ export function getOpenAILanguageModelCapabilities(
45
48
  (gptVersion != null && gptVersion.major >= 5 && !isGptChatModel);
46
49
 
47
50
  // https://platform.openai.com/docs/guides/latest-model#gpt-5-1-parameter-compatibility
48
- // GPT-5.1 through GPT-5.x and GPT-6 Sol/Luna support temperature,
49
- // topP, and logProbs when reasoningEffort is none.
51
+ // GPT-5.1 through GPT-5.x, GPT-6 Sol/Luna, and GPT-6.1 Terra support
52
+ // temperature, topP, and logProbs when reasoningEffort is none.
50
53
  const supportsNonReasoningParameters =
51
- isGpt6SolOrLuna ||
54
+ isGpt6OrLaterModelWithNoneEffort ||
52
55
  (!isGpt6OrLaterModel &&
53
56
  gptVersion != null &&
54
57
  (gptVersion.major > 5 ||
@@ -61,7 +64,7 @@ export function getOpenAILanguageModelCapabilities(
61
64
  supportsPriorityProcessing,
62
65
  supportsConfigurationUpdate: isGpt6OrLaterModel,
63
66
  supportsAsyncToolCalling: isGpt6OrLaterModel,
64
- supportedReasoningEfforts: isGpt6SolOrLuna
67
+ supportedReasoningEfforts: isGpt6OrLaterModelWithNoneEffort
65
68
  ? ['none', 'low', 'medium', 'high', 'xhigh', 'max']
66
69
  : isGpt6OrLaterModel
67
70
  ? ['low', 'medium', 'high', 'xhigh', 'max']
@@ -59,6 +59,7 @@ export const openaiResponsesReasoningModelIds = [
59
59
  'gpt-5.6-terra',
60
60
  'gpt-6-astra',
61
61
  'gpt-6.1-sol',
62
+ 'gpt-6.1-terra',
62
63
  'gpt-6-luna',
63
64
  'gpt-6-sol',
64
65
  ] as const;
@@ -135,6 +136,7 @@ export type OpenAIResponsesModelId =
135
136
  | 'gpt-5.6-terra'
136
137
  | 'gpt-6-astra'
137
138
  | 'gpt-6.1-sol'
139
+ | 'gpt-6.1-terra'
138
140
  | 'gpt-6-luna'
139
141
  | 'gpt-6-sol'
140
142
  | 'gpt-5-2025-08-07'
@@ -1275,9 +1275,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
1275
1275
  toolName: toolNameMapping.toCustomToolName(
1276
1276
  webSearchToolName ?? 'web_search',
1277
1277
  ),
1278
- ...(part.status === 'failed' || part.status === 'incomplete'
1279
- ? { isError: true, result: { status: part.status } }
1280
- : { result: mapWebSearchOutput(part.action) }),
1278
+ ...mapWebSearchResult(part),
1281
1279
  });
1282
1280
 
1283
1281
  if (part.action?.type === 'search') {
@@ -2190,10 +2188,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
2190
2188
  toolName: toolNameMapping.toCustomToolName(
2191
2189
  webSearchToolName ?? 'web_search',
2192
2190
  ),
2193
- ...(value.item.status === 'failed' ||
2194
- value.item.status === 'incomplete'
2195
- ? { isError: true, result: { status: value.item.status } }
2196
- : { result: mapWebSearchOutput(value.item.action) }),
2191
+ ...mapWebSearchResult(value.item),
2197
2192
  });
2198
2193
 
2199
2194
  if (value.item.action?.type === 'search') {
@@ -3180,7 +3175,21 @@ function isResponseOutputChunk(chunk: OpenAIResponsesChunk): boolean {
3180
3175
  );
3181
3176
  }
3182
3177
 
3183
- export function mapWebSearchOutput(
3178
+ export function mapWebSearchResult({
3179
+ action,
3180
+ status,
3181
+ }: {
3182
+ action?: OpenAIResponsesWebSearchAction | null;
3183
+ status: string;
3184
+ }):
3185
+ | { isError: true; result: { status: string } }
3186
+ | { result: InferSchema<typeof webSearchOutputSchema> } {
3187
+ return status === 'completed'
3188
+ ? { result: mapWebSearchOutput(action) }
3189
+ : { isError: true, result: { status } };
3190
+ }
3191
+
3192
+ function mapWebSearchOutput(
3184
3193
  action: OpenAIResponsesWebSearchAction | null | undefined,
3185
3194
  ): InferSchema<typeof webSearchOutputSchema> {
3186
3195
  if (action == null) {
@@ -12,7 +12,9 @@ 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-live-transcribe'
15
16
  | 'gpt-realtime-whisper'
17
+ | 'gpt-transcribe'
16
18
  | (string & {});
17
19
 
18
20
  // https://platform.openai.com/docs/api-reference/audio/createTranscription
@@ -43,12 +45,9 @@ export const openAITranscriptionModelOptions = lazySchema(() =>
43
45
 
44
46
  /**
45
47
  * The timestamp granularities to populate for this transcription.
46
- * @default ['segment']
48
+ * Defaults to ['segment'] when the response format is verbose_json.
47
49
  */
48
- timestampGranularities: z
49
- .array(z.enum(['word', 'segment']))
50
- .default(['segment'])
51
- .optional(),
50
+ timestampGranularities: z.array(z.enum(['word', 'segment'])).optional(),
52
51
 
53
52
  /**
54
53
  * The format of the transcription response.
@@ -73,7 +72,7 @@ export const openAITranscriptionModelOptions = lazySchema(() =>
73
72
  .optional(),
74
73
 
75
74
  /**
76
- * Options for streaming transcription models such as `gpt-realtime-whisper`.
75
+ * Options for streaming transcription models such as `gpt-live-transcribe`.
77
76
  */
78
77
  streaming: z
79
78
  .object({
@@ -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
  },