@ai-sdk/openai 4.0.47 → 4.0.49

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.
@@ -1956,40 +1956,60 @@ The metadata includes the following fields:
1956
1956
  ### Text Batches
1957
1957
 
1958
1958
  <Note type="warning">
1959
- Text batch APIs are experimental and may change in future releases.
1959
+ Text batch support is experimental and the API may change in patch releases.
1960
1960
  </Note>
1961
1961
 
1962
- The OpenAI provider supports the [Batch API](https://developers.openai.com/api/docs/guides/batch) for text generation.
1962
+ The OpenAI provider supports asynchronous text generation through the
1963
+ [Batch API](https://developers.openai.com/api/docs/guides/batch). Use the
1964
+ experimental text batch APIs to start a batch, poll its status, and stream its
1965
+ results:
1963
1966
 
1964
1967
  ```ts
1965
1968
  import { openai } from '@ai-sdk/openai';
1966
1969
  import {
1967
- experimental_startTextBatch as startTextBatch,
1968
1970
  experimental_getBatchResults as getBatchResults,
1969
1971
  experimental_getBatchStatus as getBatchStatus,
1972
+ experimental_startTextBatch as startTextBatch,
1970
1973
  } from 'ai';
1974
+ import { setTimeout } from 'node:timers/promises';
1971
1975
 
1972
1976
  const model = openai('gpt-4.1-nano');
1973
1977
 
1974
1978
  const batch = await startTextBatch({
1975
1979
  model,
1976
1980
  requests: [
1977
- { id: 'france', prompt: 'What is the capital of France?' },
1978
- { id: 'germany', prompt: 'What is the capital of Germany?' },
1981
+ { id: 'capital-france', prompt: 'What is the capital of France?' },
1982
+ { id: 'capital-germany', prompt: 'What is the capital of Germany?' },
1979
1983
  ],
1980
1984
  });
1981
1985
 
1982
- // Persist `batch` and check its status later, or poll for status updates.
1983
- const { status } = await getBatchStatus({ model, batch });
1986
+ let status = batch.status;
1987
+ while (status === 'pending') {
1988
+ await setTimeout(60_000);
1989
+ ({ status } = await getBatchStatus({ model, batch }));
1990
+ }
1984
1991
 
1985
- if (status !== 'pending') {
1986
- for await (const result of getBatchResults({ model, batch })) {
1987
- console.log(result);
1992
+ for await (const item of getBatchResults({ model, batch })) {
1993
+ if (item.status === 'succeeded') {
1994
+ console.log(item.id, item.text);
1995
+ } else {
1996
+ console.error(item.id, item.error);
1988
1997
  }
1989
1998
  }
1990
1999
  ```
1991
2000
 
1992
- Starting a batch returns a serializable reference that can be persisted and used to retrieve the batch results later, or poll for status updates.
2001
+ `startTextBatch` returns a serializable batch reference. Persist this reference
2002
+ to check the batch status or retrieve its results from another process. Results
2003
+ can arrive in a different order from the input requests, so match each result by
2004
+ its `id`.
2005
+
2006
+ #### Webhooks
2007
+
2008
+ <Note>
2009
+ The OpenAI Batch API does not support per-batch webhooks. When you provide a
2010
+ `webhookUrl`, the provider returns an unsupported warning and starts the batch
2011
+ without a webhook.
2012
+ </Note>
1993
2013
 
1994
2014
  ### Chat Models
1995
2015
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.47",
3
+ "version": "4.0.49",
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.8",
39
- "@ai-sdk/provider-utils": "5.0.30"
39
+ "@ai-sdk/provider-utils": "5.0.32"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.1",
@@ -13,7 +13,11 @@ import {
13
13
  } from '@ai-sdk/provider-utils';
14
14
 
15
15
  function serializeToolCallArguments(input: unknown): string {
16
- return JSON.stringify(input === undefined ? {} : input);
16
+ return JSON.stringify(
17
+ typeof input === 'object' && input !== null && !Array.isArray(input)
18
+ ? input
19
+ : {},
20
+ );
17
21
  }
18
22
 
19
23
  type OpenAIPromptCacheBreakpoint = { mode: 'explicit' };
@@ -26,7 +26,10 @@ import {
26
26
  } from '@ai-sdk/provider-utils';
27
27
  import { openaiFailedResponseHandler } from '../openai-error';
28
28
  import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
29
- import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
29
+ import {
30
+ createOpenAIProviderStreamError,
31
+ throwIfOpenAIStreamErrorBeforeOutput,
32
+ } from '../openai-stream-error';
30
33
  import {
31
34
  convertOpenAIChatUsage,
32
35
  type OpenAIChatUsage,
@@ -509,7 +512,11 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
509
512
  // handle error chunks:
510
513
  if ('error' in value) {
511
514
  finishReason = { unified: 'error', raw: undefined };
512
- controller.enqueue({ type: 'error', error: value.error });
515
+ controller.enqueue({
516
+ type: 'error',
517
+ error:
518
+ createOpenAIProviderStreamError(value.error) ?? value.error,
519
+ });
513
520
  return;
514
521
  }
515
522
 
@@ -21,7 +21,10 @@ import {
21
21
  type ParseResult,
22
22
  } from '@ai-sdk/provider-utils';
23
23
  import { openaiFailedResponseHandler } from '../openai-error';
24
- import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
24
+ import {
25
+ createOpenAIProviderStreamError,
26
+ throwIfOpenAIStreamErrorBeforeOutput,
27
+ } from '../openai-stream-error';
25
28
  import {
26
29
  convertOpenAICompletionUsage,
27
30
  type OpenAICompletionUsage,
@@ -302,7 +305,11 @@ export class OpenAICompletionLanguageModel implements LanguageModelV4 {
302
305
  // handle error chunks:
303
306
  if ('error' in value) {
304
307
  finishReason = { unified: 'error', raw: undefined };
305
- controller.enqueue({ type: 'error', error: value.error });
308
+ controller.enqueue({
309
+ type: 'error',
310
+ error:
311
+ createOpenAIProviderStreamError(value.error) ?? value.error,
312
+ });
306
313
  return;
307
314
  }
308
315
 
@@ -5,6 +5,7 @@ import {
5
5
  import {
6
6
  combineHeaders,
7
7
  createJsonResponseHandler,
8
+ EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL,
8
9
  parseProviderOptions,
9
10
  postJsonToApi,
10
11
  serializeModelOptions,
@@ -23,6 +24,7 @@ export class OpenAIEmbeddingModel implements EmbeddingModelV4 {
23
24
  readonly specificationVersion = 'v4';
24
25
  readonly modelId: OpenAIEmbeddingModelId;
25
26
  readonly maxEmbeddingsPerCall = 2048;
27
+ readonly [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL] = 300_000;
26
28
  readonly supportsParallelCalls = true;
27
29
 
28
30
  private readonly config: OpenAIConfig;
@@ -1,6 +1,6 @@
1
1
  import {
2
- EmptyResponseBodyError,
3
2
  InvalidArgumentError,
3
+ InvalidResponseDataError,
4
4
  type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
5
5
  type Experimental_BatchV4StartOptions as BatchV4StartOptions,
6
6
  type Experimental_BatchV4StartResult as BatchV4StartResult,
@@ -15,19 +15,18 @@ import {
15
15
  import {
16
16
  combineHeaders,
17
17
  convertAsyncIteratorToReadableStream,
18
+ createJsonLinesResponseHandler,
18
19
  createJsonResponseHandler,
19
20
  getFromApi,
20
21
  lazySchema,
21
- parseJSON,
22
+ normalizeBatchRequestCounts,
22
23
  postJsonToApi,
23
24
  postToApi,
24
25
  safeValidateTypes,
25
- validateTypes,
26
26
  WORKFLOW_DESERIALIZE,
27
27
  WORKFLOW_SERIALIZE,
28
28
  zodSchema,
29
29
  type InferSchema,
30
- type ResponseHandler,
31
30
  } from '@ai-sdk/provider-utils';
32
31
  import { z } from 'zod/v4';
33
32
  import {
@@ -44,6 +43,7 @@ import {
44
43
  } from './responses/openai-responses-api';
45
44
  import { OpenAIResponsesLanguageModel } from './responses/openai-responses-language-model';
46
45
  import type { OpenAIResponsesModelId } from './responses/openai-responses-language-model-options';
46
+ import type { ResponsesReasoningProviderMetadata } from './responses/openai-responses-provider-metadata';
47
47
 
48
48
  const openaiBatchEndpoint = '/v1/responses';
49
49
  const openaiBatchInputFileExpiresAfterSeconds = 48 * 60 * 60;
@@ -241,7 +241,9 @@ class OpenAIResponsesBatch {
241
241
  ): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
242
242
  const batch = await this.retrieveBatch(options);
243
243
 
244
- if (convertOpenAIBatchStatus(batch).status === 'pending') {
244
+ const batchStatus = convertOpenAIBatchStatus(batch);
245
+
246
+ if (batchStatus.status === 'pending') {
245
247
  throw new InvalidArgumentError({
246
248
  argument: 'batchId',
247
249
  message: `OpenAI batch "${options.batchId}" is not complete.`,
@@ -251,6 +253,14 @@ class OpenAIResponsesBatch {
251
253
  const fileIds = [batch.output_file_id, batch.error_file_id].filter(
252
254
  (fileId): fileId is string => fileId != null,
253
255
  );
256
+
257
+ if (batchStatus.status === 'completed' && fileIds.length === 0) {
258
+ throw new InvalidResponseDataError({
259
+ data: batch,
260
+ message: `OpenAI batch "${options.batchId}" completed without batch output.`,
261
+ });
262
+ }
263
+
254
264
  const iterator = this.iterateBatchResults({ fileIds, options });
255
265
 
256
266
  return convertAsyncIteratorToReadableStream(iterator);
@@ -282,20 +292,22 @@ class OpenAIResponsesBatch {
282
292
  options: BatchV4OperationOptions;
283
293
  }): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
284
294
  for (const fileId of fileIds) {
285
- const { value: stream } = await getFromApi({
295
+ const { value: lines } = await getFromApi({
286
296
  url: this.getUrl(`/files/${encodeURIComponent(fileId)}/content`),
287
297
  headers: combineHeaders(
288
298
  this.options.config.headers?.(),
289
299
  options.headers,
290
300
  ),
291
301
  failedResponseHandler: openaiFailedResponseHandler,
292
- successfulResponseHandler: rawStreamResponseHandler,
302
+ successfulResponseHandler: createJsonLinesResponseHandler(
303
+ openaiBatchResultLineSchema,
304
+ ),
293
305
  abortSignal: options.abortSignal,
294
306
  fetch: this.options.config.fetch,
295
307
  validateUrl: false,
296
308
  });
297
309
 
298
- for await (const line of parseJsonLines(stream)) {
310
+ for await (const line of lines) {
299
311
  yield await this.convertResultLine(line);
300
312
  }
301
313
  }
@@ -468,24 +480,15 @@ function convertOpenAIRequestCounts(
468
480
  const completed = counts?.completed;
469
481
  const failed = counts?.failed;
470
482
 
471
- if (
472
- total == null ||
473
- completed == null ||
474
- failed == null ||
475
- total < 0 ||
476
- completed < 0 ||
477
- failed < 0 ||
478
- completed + failed > total
479
- ) {
480
- return undefined;
481
- }
482
-
483
- return {
483
+ return normalizeBatchRequestCounts({
484
484
  total,
485
- pending: total - completed - failed,
485
+ pending:
486
+ total != null && completed != null && failed != null
487
+ ? total - completed - failed
488
+ : undefined,
486
489
  completed,
487
490
  failed,
488
- };
491
+ });
489
492
  }
490
493
 
491
494
  function convertUnixTimestamp(value: number | null | undefined) {
@@ -530,11 +533,23 @@ async function convertOpenAIErrorResponse({
530
533
  async function convertOpenAIResponsesBatchResponse(
531
534
  body: unknown,
532
535
  ): Promise<OpenAIBatchResponseConversion> {
533
- const response = await validateTypes({
536
+ const validation = await safeValidateTypes({
534
537
  value: body,
535
538
  schema: openaiResponsesResponseSchema,
536
539
  });
537
540
 
541
+ if (!validation.success) {
542
+ return {
543
+ success: false,
544
+ error: {
545
+ message: 'OpenAI returned an invalid Responses batch result.',
546
+ code: 'invalid_response',
547
+ },
548
+ };
549
+ }
550
+
551
+ const response = validation.value;
552
+
538
553
  if (response.error != null) {
539
554
  return {
540
555
  success: false,
@@ -564,25 +579,59 @@ async function convertOpenAIResponsesBatchResponse(
564
579
  const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
565
580
 
566
581
  for (const part of response.output) {
567
- if (part.type === 'message') {
568
- for (const contentPart of part.content) {
569
- content.push({ type: 'text', text: contentPart.text });
570
- if (contentPart.logprobs != null) {
571
- logprobs.push(contentPart.logprobs);
582
+ switch (part.type) {
583
+ case 'reasoning': {
584
+ const summaries =
585
+ part.summary.length > 0
586
+ ? part.summary
587
+ : [{ type: 'summary_text' as const, text: '' }];
588
+
589
+ for (const summary of summaries) {
590
+ content.push({
591
+ type: 'reasoning',
592
+ text: summary.text,
593
+ providerMetadata: {
594
+ openai: {
595
+ itemId: part.id,
596
+ reasoningEncryptedContent: part.encrypted_content ?? null,
597
+ } satisfies ResponsesReasoningProviderMetadata,
598
+ },
599
+ });
572
600
  }
601
+ break;
573
602
  }
574
- } else if (
575
- part.type === 'function_call' ||
576
- part.type === 'custom_tool_call'
577
- ) {
578
- return {
579
- success: false,
580
- error: {
581
- message:
582
- 'OpenAI returned a tool call, but tool calls are not supported in AI SDK text batches.',
583
- code: 'unsupported_tool_call',
584
- },
585
- };
603
+
604
+ case 'message': {
605
+ for (const contentPart of part.content) {
606
+ content.push({ type: 'text', text: contentPart.text });
607
+ if (contentPart.logprobs != null) {
608
+ logprobs.push(contentPart.logprobs);
609
+ }
610
+ }
611
+ break;
612
+ }
613
+
614
+ case 'function_call':
615
+ case 'custom_tool_call':
616
+ return {
617
+ success: false,
618
+ error: {
619
+ message:
620
+ 'OpenAI returned a tool call, but tool calls are not supported in AI SDK text batches.',
621
+ code: 'unsupported_content',
622
+ },
623
+ };
624
+
625
+ default:
626
+ return {
627
+ success: false,
628
+ error: {
629
+ message:
630
+ `OpenAI returned an unsupported "${part.type}" output item ` +
631
+ 'in an AI SDK text batch.',
632
+ code: 'unsupported_content',
633
+ },
634
+ };
586
635
  }
587
636
  }
588
637
 
@@ -624,64 +673,3 @@ async function convertOpenAIResponsesBatchResponse(
624
673
  },
625
674
  };
626
675
  }
627
-
628
- const rawStreamResponseHandler: ResponseHandler<
629
- ReadableStream<Uint8Array>
630
- > = async ({ response }) => {
631
- if (response.body == null) {
632
- throw new EmptyResponseBodyError();
633
- }
634
-
635
- return { value: response.body };
636
- };
637
-
638
- async function* parseJsonLines(
639
- stream: ReadableStream<Uint8Array>,
640
- ): AsyncGenerator<OpenAIBatchResultLine> {
641
- const reader = stream.getReader();
642
- const decoder = new TextDecoder();
643
- let buffer = '';
644
- let finished = false;
645
-
646
- try {
647
- while (true) {
648
- const { done, value } = await reader.read();
649
-
650
- if (done) {
651
- finished = true;
652
- buffer += decoder.decode();
653
- break;
654
- }
655
-
656
- buffer += decoder.decode(value, { stream: true });
657
-
658
- let lineEnd = buffer.indexOf('\n');
659
- while (lineEnd !== -1) {
660
- const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
661
- buffer = buffer.slice(lineEnd + 1);
662
-
663
- if (line.trim().length > 0) {
664
- yield await parseJSON({
665
- text: line,
666
- schema: openaiBatchResultLineSchema,
667
- });
668
- }
669
-
670
- lineEnd = buffer.indexOf('\n');
671
- }
672
- }
673
-
674
- const finalLine = buffer.replace(/\r$/, '');
675
- if (finalLine.trim().length > 0) {
676
- yield await parseJSON({
677
- text: finalLine,
678
- schema: openaiBatchResultLineSchema,
679
- });
680
- }
681
- } finally {
682
- if (!finished) {
683
- await reader.cancel().catch(() => {});
684
- }
685
- reader.releaseLock();
686
- }
687
- }
@@ -1,13 +1,41 @@
1
1
  import { APICallError } from '@ai-sdk/provider';
2
- import type { ParseResult } from '@ai-sdk/provider-utils';
2
+ import {
3
+ createProviderStreamError,
4
+ type ParseResult,
5
+ type ProviderStreamError,
6
+ } from '@ai-sdk/provider-utils';
3
7
 
4
8
  type StreamError = {
5
9
  message: string;
6
10
  code?: string | number | null;
7
11
  type?: string | null;
8
- frame: unknown;
9
12
  };
10
13
 
14
+ /**
15
+ * Converts an OpenAI stream error frame into provider-owned metadata that AI
16
+ * SDK Core can normalize without duplicating OpenAI error-code semantics.
17
+ */
18
+ export function createOpenAIProviderStreamError(
19
+ frame: unknown,
20
+ ): ProviderStreamError | undefined {
21
+ const streamError = parseStreamError(frame);
22
+
23
+ if (streamError == null) {
24
+ return undefined;
25
+ }
26
+
27
+ const statusCode = getStatusCode(streamError);
28
+
29
+ return createProviderStreamError({
30
+ message: streamError.message,
31
+ type: streamError.type ?? undefined,
32
+ code: streamError.code ?? undefined,
33
+ statusCode,
34
+ isRetryable: isRetryableStreamError(streamError, statusCode),
35
+ data: frame,
36
+ });
37
+ }
38
+
11
39
  export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
12
40
  stream,
13
41
  getError,
@@ -154,17 +182,18 @@ function createOpenAIStreamError({
154
182
  requestBodyValues: unknown;
155
183
  responseHeaders?: Record<string, string>;
156
184
  }): APICallError {
157
- const streamError = parseStreamError(frame);
185
+ const streamError = createOpenAIProviderStreamError(frame);
158
186
  return new APICallError({
159
187
  message:
160
188
  streamError?.message ??
161
189
  'OpenAI stream failed before any output was generated',
162
190
  url,
163
191
  requestBodyValues,
164
- statusCode: streamError == null ? 500 : getStatusCode(streamError),
192
+ statusCode: streamError?.statusCode ?? 500,
165
193
  responseHeaders,
166
194
  responseBody: JSON.stringify(frame),
167
195
  data: frame,
196
+ isRetryable: streamError?.isRetryable,
168
197
  });
169
198
  }
170
199
 
@@ -184,7 +213,6 @@ function parseStreamError(frame: unknown): StreamError | undefined {
184
213
  message: responseError.message,
185
214
  code: getStringOrNumber(responseError.code),
186
215
  type: 'response.failed',
187
- frame,
188
216
  }
189
217
  : undefined;
190
218
  }
@@ -200,21 +228,14 @@ function parseStreamError(frame: unknown): StreamError | undefined {
200
228
  message: error.message,
201
229
  code: getStringOrNumber(error.code),
202
230
  type: typeof error.type === 'string' ? error.type : undefined,
203
- frame,
204
231
  }
205
232
  : undefined;
206
233
  }
207
234
 
208
235
  function getStatusCode(error: StreamError): number {
209
- if (typeof error.code === 'number' && isHttpErrorStatusCode(error.code)) {
210
- return error.code;
211
- }
212
-
213
- if (typeof error.code === 'string' && /^\d{3}$/.test(error.code)) {
214
- const numericCode = Number(error.code);
215
- if (isHttpErrorStatusCode(numericCode)) {
216
- return numericCode;
217
- }
236
+ const explicitStatusCode = getHttpStatusCode(error.code);
237
+ if (explicitStatusCode != null) {
238
+ return explicitStatusCode;
218
239
  }
219
240
 
220
241
  const discriminator = [error.code, error.type]
@@ -260,3 +281,35 @@ function getStringOrNumber(value: unknown): string | number | undefined {
260
281
  function isHttpErrorStatusCode(value: number): boolean {
261
282
  return Number.isInteger(value) && value >= 400 && value <= 599;
262
283
  }
284
+
285
+ function getHttpStatusCode(value: string | number | null | undefined) {
286
+ const statusCode =
287
+ typeof value === 'string' && /^\d{3}$/.test(value) ? Number(value) : value;
288
+
289
+ return typeof statusCode === 'number' && isHttpErrorStatusCode(statusCode)
290
+ ? statusCode
291
+ : undefined;
292
+ }
293
+
294
+ function isRetryableStatusCode(statusCode: number): boolean {
295
+ return (
296
+ statusCode === 408 ||
297
+ statusCode === 409 ||
298
+ statusCode === 429 ||
299
+ statusCode >= 500
300
+ );
301
+ }
302
+
303
+ function isRetryableStreamError(
304
+ error: StreamError,
305
+ statusCode: number,
306
+ ): boolean {
307
+ if (
308
+ error.code === 'insufficient_quota' ||
309
+ error.type === 'insufficient_quota'
310
+ ) {
311
+ return false;
312
+ }
313
+
314
+ return isRetryableStatusCode(statusCode);
315
+ }