@ai-sdk/openai 3.0.80 → 3.0.82

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.80",
3
+ "version": "3.0.82",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@ai-sdk/provider": "3.0.13",
40
- "@ai-sdk/provider-utils": "4.0.35"
40
+ "@ai-sdk/provider-utils": "4.0.37"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -200,7 +200,7 @@ export function convertToOpenAIChatMessages({
200
200
  contentValue = output.value;
201
201
  break;
202
202
  case 'execution-denied':
203
- contentValue = output.reason ?? 'Tool execution denied.';
203
+ contentValue = output.reason ?? 'Tool call execution denied.';
204
204
  break;
205
205
  case 'content':
206
206
  case 'json':
@@ -15,7 +15,6 @@ import {
15
15
  createEventSourceResponseHandler,
16
16
  createJsonResponseHandler,
17
17
  generateId,
18
- isParsableJson,
19
18
  parseProviderOptions,
20
19
  postJsonToApi,
21
20
  type FetchFunction,
@@ -23,6 +22,7 @@ import {
23
22
  } from '@ai-sdk/provider-utils';
24
23
  import { openaiFailedResponseHandler } from '../openai-error';
25
24
  import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
25
+ import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
26
26
  import {
27
27
  convertOpenAIChatUsage,
28
28
  type OpenAIChatUsage,
@@ -414,11 +414,13 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
414
414
  },
415
415
  };
416
416
 
417
+ const url = this.config.url({
418
+ path: '/chat/completions',
419
+ modelId: this.modelId,
420
+ });
421
+
417
422
  const { responseHeaders, value: response } = await postJsonToApi({
418
- url: this.config.url({
419
- path: '/chat/completions',
420
- modelId: this.modelId,
421
- }),
423
+ url,
422
424
  headers: combineHeaders(this.config.headers(), options.headers),
423
425
  body,
424
426
  failedResponseHandler: openaiFailedResponseHandler,
@@ -429,6 +431,15 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
429
431
  fetch: this.config.fetch,
430
432
  });
431
433
 
434
+ const checkedResponse = await throwIfOpenAIStreamErrorBeforeOutput({
435
+ stream: response,
436
+ getError: chunk => ('error' in chunk ? chunk.error : undefined),
437
+ isOutputChunk: isOpenAIChatOutputChunk,
438
+ url,
439
+ requestBodyValues: body,
440
+ responseHeaders,
441
+ });
442
+
432
443
  const toolCalls: Array<{
433
444
  id: string;
434
445
  type: 'function';
@@ -449,8 +460,8 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
449
460
 
450
461
  const providerMetadata: SharedV3ProviderMetadata = { openai: {} };
451
462
 
452
- return {
453
- stream: response.pipeThrough(
463
+ const result = {
464
+ stream: checkedResponse.pipeThrough(
454
465
  new TransformStream<
455
466
  ParseResult<OpenAIChatChunk>,
456
467
  LanguageModelV3StreamPart
@@ -605,23 +616,6 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
605
616
  delta: toolCall.function.arguments,
606
617
  });
607
618
  }
608
-
609
- // check if tool call is complete
610
- // (some providers send the full tool call in one chunk):
611
- if (isParsableJson(toolCall.function.arguments)) {
612
- controller.enqueue({
613
- type: 'tool-input-end',
614
- id: toolCall.id,
615
- });
616
-
617
- controller.enqueue({
618
- type: 'tool-call',
619
- toolCallId: toolCall.id ?? generateId(),
620
- toolName: toolCall.function.name,
621
- input: toolCall.function.arguments,
622
- });
623
- toolCall.hasFinished = true;
624
- }
625
619
  }
626
620
 
627
621
  continue;
@@ -645,26 +639,6 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
645
639
  id: toolCall.id,
646
640
  delta: toolCallDelta.function.arguments ?? '',
647
641
  });
648
-
649
- // check if tool call is complete
650
- if (
651
- toolCall.function?.name != null &&
652
- toolCall.function?.arguments != null &&
653
- isParsableJson(toolCall.function.arguments)
654
- ) {
655
- controller.enqueue({
656
- type: 'tool-input-end',
657
- id: toolCall.id,
658
- });
659
-
660
- controller.enqueue({
661
- type: 'tool-call',
662
- toolCallId: toolCall.id ?? generateId(),
663
- toolName: toolCall.function.name,
664
- input: toolCall.function.arguments,
665
- });
666
- toolCall.hasFinished = true;
667
- }
668
642
  }
669
643
  }
670
644
 
@@ -687,6 +661,26 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
687
661
  controller.enqueue({ type: 'text-end', id: '0' });
688
662
  }
689
663
 
664
+ // Finalize any unfinished tool calls. Tool calls are only
665
+ // finalized here (on stream end) to prevent premature
666
+ // execution when partial JSON is coincidentally parsable.
667
+ for (const toolCall of toolCalls) {
668
+ if (!toolCall.hasFinished) {
669
+ controller.enqueue({
670
+ type: 'tool-input-end',
671
+ id: toolCall.id,
672
+ });
673
+
674
+ controller.enqueue({
675
+ type: 'tool-call',
676
+ toolCallId: toolCall.id ?? generateId(),
677
+ toolName: toolCall.function.name,
678
+ input: toolCall.function.arguments,
679
+ });
680
+ toolCall.hasFinished = true;
681
+ }
682
+ }
683
+
690
684
  controller.enqueue({
691
685
  type: 'finish',
692
686
  finishReason,
@@ -699,5 +693,23 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
699
693
  request: { body },
700
694
  response: { headers: responseHeaders },
701
695
  };
696
+
697
+ return result;
698
+ }
699
+ }
700
+
701
+ function isOpenAIChatOutputChunk(chunk: OpenAIChatChunk): boolean {
702
+ if ('error' in chunk) {
703
+ return false;
702
704
  }
705
+
706
+ return chunk.choices.some(choice => {
707
+ const delta = choice.delta;
708
+
709
+ return (
710
+ (delta?.content != null && delta.content.length > 0) ||
711
+ (delta?.tool_calls != null && delta.tool_calls.length > 0) ||
712
+ (delta?.annotations != null && delta.annotations.length > 0)
713
+ );
714
+ });
703
715
  }
@@ -18,6 +18,7 @@ import {
18
18
  type ParseResult,
19
19
  } from '@ai-sdk/provider-utils';
20
20
  import { openaiFailedResponseHandler } from '../openai-error';
21
+ import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
21
22
  import {
22
23
  convertOpenAICompletionUsage,
23
24
  type OpenAICompletionUsage,
@@ -224,11 +225,13 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
224
225
  },
225
226
  };
226
227
 
228
+ const url = this.config.url({
229
+ path: '/completions',
230
+ modelId: this.modelId,
231
+ });
232
+
227
233
  const { responseHeaders, value: response } = await postJsonToApi({
228
- url: this.config.url({
229
- path: '/completions',
230
- modelId: this.modelId,
231
- }),
234
+ url,
232
235
  headers: combineHeaders(this.config.headers(), options.headers),
233
236
  body,
234
237
  failedResponseHandler: openaiFailedResponseHandler,
@@ -239,6 +242,15 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
239
242
  fetch: this.config.fetch,
240
243
  });
241
244
 
245
+ const checkedResponse = await throwIfOpenAIStreamErrorBeforeOutput({
246
+ stream: response,
247
+ getError: chunk => ('error' in chunk ? chunk.error : undefined),
248
+ isOutputChunk: isOpenAICompletionOutputChunk,
249
+ url,
250
+ requestBodyValues: body,
251
+ responseHeaders,
252
+ });
253
+
242
254
  let finishReason: LanguageModelV3FinishReason = {
243
255
  unified: 'other',
244
256
  raw: undefined,
@@ -247,8 +259,8 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
247
259
  let usage: OpenAICompletionUsage | undefined = undefined;
248
260
  let isFirstChunk = true;
249
261
 
250
- return {
251
- stream: response.pipeThrough(
262
+ const result = {
263
+ stream: checkedResponse.pipeThrough(
252
264
  new TransformStream<
253
265
  ParseResult<OpenAICompletionChunk>,
254
266
  LanguageModelV3StreamPart
@@ -332,5 +344,13 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
332
344
  request: { body },
333
345
  response: { headers: responseHeaders },
334
346
  };
347
+
348
+ return result;
335
349
  }
336
350
  }
351
+
352
+ function isOpenAICompletionOutputChunk(chunk: OpenAICompletionChunk): boolean {
353
+ return (
354
+ !('error' in chunk) && chunk.choices.some(choice => choice.text.length > 0)
355
+ );
356
+ }
@@ -0,0 +1,181 @@
1
+ import { APICallError } from '@ai-sdk/provider';
2
+ import type { ParseResult } from '@ai-sdk/provider-utils';
3
+
4
+ type StreamError = {
5
+ message: string;
6
+ code?: string | number | null;
7
+ type?: string | null;
8
+ frame: unknown;
9
+ };
10
+
11
+ export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
12
+ stream,
13
+ getError,
14
+ isOutputChunk,
15
+ url,
16
+ requestBodyValues,
17
+ responseHeaders,
18
+ }: {
19
+ stream: ReadableStream<ParseResult<T>>;
20
+ getError: (chunk: T) => unknown | undefined;
21
+ isOutputChunk: (chunk: T) => boolean;
22
+ url: string;
23
+ requestBodyValues: unknown;
24
+ responseHeaders?: Record<string, string>;
25
+ }): Promise<ReadableStream<ParseResult<T>>> {
26
+ const [streamForEarlyError, streamForConsumer] = stream.tee();
27
+ const reader = streamForEarlyError.getReader();
28
+
29
+ try {
30
+ while (true) {
31
+ const result = await reader.read();
32
+
33
+ if (result.done) {
34
+ return streamForConsumer;
35
+ }
36
+
37
+ const chunk = result.value;
38
+
39
+ if (!chunk.success) {
40
+ return streamForConsumer;
41
+ }
42
+
43
+ const errorFrame = getError(chunk.value);
44
+
45
+ if (errorFrame != null) {
46
+ streamForConsumer.cancel().catch(() => {});
47
+ throw createOpenAIStreamError({
48
+ frame: errorFrame,
49
+ url,
50
+ requestBodyValues,
51
+ responseHeaders,
52
+ });
53
+ }
54
+
55
+ if (isOutputChunk(chunk.value)) {
56
+ return streamForConsumer;
57
+ }
58
+ }
59
+ } finally {
60
+ reader.cancel().catch(() => {});
61
+ reader.releaseLock();
62
+ }
63
+ }
64
+
65
+ function createOpenAIStreamError({
66
+ frame,
67
+ url,
68
+ requestBodyValues,
69
+ responseHeaders,
70
+ }: {
71
+ frame: unknown;
72
+ url: string;
73
+ requestBodyValues: unknown;
74
+ responseHeaders?: Record<string, string>;
75
+ }): APICallError {
76
+ const streamError = parseStreamError(frame);
77
+ return new APICallError({
78
+ message:
79
+ streamError?.message ??
80
+ 'OpenAI stream failed before any output was generated',
81
+ url,
82
+ requestBodyValues,
83
+ statusCode: streamError == null ? 500 : getStatusCode(streamError),
84
+ responseHeaders,
85
+ responseBody: JSON.stringify(frame),
86
+ data: frame,
87
+ });
88
+ }
89
+
90
+ function parseStreamError(frame: unknown): StreamError | undefined {
91
+ const value = asRecord(frame);
92
+
93
+ if (value == null) {
94
+ return undefined;
95
+ }
96
+
97
+ if (value.type === 'response.failed') {
98
+ const response = asRecord(value.response);
99
+ const responseError = asRecord(response?.error);
100
+
101
+ return typeof responseError?.message === 'string'
102
+ ? {
103
+ message: responseError.message,
104
+ code: getStringOrNumber(responseError.code),
105
+ type: 'response.failed',
106
+ frame,
107
+ }
108
+ : undefined;
109
+ }
110
+
111
+ const error = asRecord(value.error) ?? value;
112
+
113
+ return typeof error.message === 'string' &&
114
+ (asRecord(value.error) != null ||
115
+ typeof error.type === 'string' ||
116
+ 'code' in error ||
117
+ 'param' in error)
118
+ ? {
119
+ message: error.message,
120
+ code: getStringOrNumber(error.code),
121
+ type: typeof error.type === 'string' ? error.type : undefined,
122
+ frame,
123
+ }
124
+ : undefined;
125
+ }
126
+
127
+ function getStatusCode(error: StreamError): number {
128
+ if (typeof error.code === 'number' && isHttpErrorStatusCode(error.code)) {
129
+ return error.code;
130
+ }
131
+
132
+ if (typeof error.code === 'string' && /^\d{3}$/.test(error.code)) {
133
+ const numericCode = Number(error.code);
134
+ if (isHttpErrorStatusCode(numericCode)) {
135
+ return numericCode;
136
+ }
137
+ }
138
+
139
+ const discriminator = [error.code, error.type]
140
+ .filter(value => typeof value === 'string' || typeof value === 'number')
141
+ .join(' ')
142
+ .toLowerCase();
143
+
144
+ if (
145
+ ['insufficient_quota', 'rate_limit'].some(term =>
146
+ discriminator.includes(term),
147
+ )
148
+ ) {
149
+ return 429;
150
+ }
151
+ if (discriminator.includes('authentication')) return 401;
152
+ if (discriminator.includes('permission')) return 403;
153
+ if (discriminator.includes('not_found')) return 404;
154
+ if (
155
+ ['invalid', 'bad_request', 'context_length'].some(term =>
156
+ discriminator.includes(term),
157
+ )
158
+ ) {
159
+ return 400;
160
+ }
161
+ if (discriminator.includes('overload')) return 503;
162
+ if (discriminator.includes('timeout')) return 504;
163
+
164
+ return 500;
165
+ }
166
+
167
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
168
+ return typeof value === 'object' && value != null
169
+ ? (value as Record<string, unknown>)
170
+ : undefined;
171
+ }
172
+
173
+ function getStringOrNumber(value: unknown): string | number | undefined {
174
+ return typeof value === 'string' || typeof value === 'number'
175
+ ? value
176
+ : undefined;
177
+ }
178
+
179
+ function isHttpErrorStatusCode(value: number): boolean {
180
+ return Number.isInteger(value) && value >= 400 && value <= 599;
181
+ }
@@ -734,7 +734,7 @@ export async function convertToOpenAIResponsesInput({
734
734
  outputValue = output.value;
735
735
  break;
736
736
  case 'execution-denied':
737
- outputValue = output.reason ?? 'Tool execution denied.';
737
+ outputValue = output.reason ?? 'Tool call execution denied.';
738
738
  break;
739
739
  case 'json':
740
740
  case 'error-json':
@@ -801,7 +801,7 @@ export async function convertToOpenAIResponsesInput({
801
801
  contentValue = output.value;
802
802
  break;
803
803
  case 'execution-denied':
804
- contentValue = output.reason ?? 'Tool execution denied.';
804
+ contentValue = output.reason ?? 'Tool call execution denied.';
805
805
  break;
806
806
  case 'json':
807
807
  case 'error-json':
@@ -472,6 +472,30 @@ export type OpenAIResponsesReasoning = {
472
472
  }>;
473
473
  };
474
474
 
475
+ // Captured from the Responses API when OpenAI returned an early
476
+ // insufficient_quota stream error after HTTP 200. This shape differs from the
477
+ // currently documented ResponseErrorEvent below.
478
+ const openaiResponsesNestedErrorChunkSchema = z.object({
479
+ type: z.literal('error'),
480
+ sequence_number: z.number(),
481
+ error: z.object({
482
+ type: z.string(),
483
+ code: z.string(),
484
+ message: z.string(),
485
+ param: z.string().nullish(),
486
+ }),
487
+ });
488
+
489
+ // Current OpenAI OpenAPI docs define ResponseErrorEvent with top-level
490
+ // code/message/param fields.
491
+ const openaiResponsesErrorChunkSchema = z.object({
492
+ type: z.literal('error'),
493
+ sequence_number: z.number(),
494
+ code: z.string().nullish(),
495
+ message: z.string(),
496
+ param: z.string().nullish(),
497
+ });
498
+
475
499
  export const openaiResponsesChunkSchema = lazySchema(() =>
476
500
  zodSchema(
477
501
  z.union([
@@ -520,6 +544,7 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
520
544
  }),
521
545
  z.object({
522
546
  type: z.literal('response.failed'),
547
+ sequence_number: z.number(),
523
548
  response: z.object({
524
549
  error: z
525
550
  .object({
@@ -1034,16 +1059,8 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
1034
1059
  output_index: z.number(),
1035
1060
  diff: z.string(),
1036
1061
  }),
1037
- z.object({
1038
- type: z.literal('error'),
1039
- sequence_number: z.number(),
1040
- error: z.object({
1041
- type: z.string(),
1042
- code: z.string(),
1043
- message: z.string(),
1044
- param: z.string().nullish(),
1045
- }),
1046
- }),
1062
+ openaiResponsesNestedErrorChunkSchema,
1063
+ openaiResponsesErrorChunkSchema,
1047
1064
  z
1048
1065
  .object({ type: z.string() })
1049
1066
  .loose()
@@ -28,6 +28,7 @@ import {
28
28
  import type { OpenAIConfig } from '../openai-config';
29
29
  import { openaiFailedResponseHandler } from '../openai-error';
30
30
  import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
31
+ import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
31
32
  import type { applyPatchInputSchema } from '../tool/apply-patch';
32
33
  import type {
33
34
  codeInterpreterInputSchema,
@@ -1057,6 +1058,19 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1057
1058
  fetch: this.config.fetch,
1058
1059
  });
1059
1060
 
1061
+ const checkedResponse = await throwIfOpenAIStreamErrorBeforeOutput({
1062
+ stream: response,
1063
+ getError: chunk =>
1064
+ isErrorChunk(chunk) ||
1065
+ (isResponseFailedChunk(chunk) && chunk.response.error != null)
1066
+ ? chunk
1067
+ : undefined,
1068
+ isOutputChunk: isResponseOutputChunk,
1069
+ url,
1070
+ requestBodyValues: body,
1071
+ responseHeaders,
1072
+ });
1073
+
1060
1074
  const self = this;
1061
1075
 
1062
1076
  const approvalRequestIdToDummyToolCallIdFromPrompt =
@@ -1117,9 +1131,10 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1117
1131
 
1118
1132
  let serviceTier: string | undefined;
1119
1133
  const hostedToolSearchCallIds: string[] = [];
1134
+ let encounteredStreamError = false;
1120
1135
 
1121
- return {
1122
- stream: response.pipeThrough(
1136
+ const result = {
1137
+ stream: checkedResponse.pipeThrough(
1123
1138
  new TransformStream<
1124
1139
  ParseResult<OpenAIResponsesChunk>,
1125
1140
  LanguageModelV3StreamPart
@@ -2046,6 +2061,22 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
2046
2061
  raw: incompleteReason ?? 'error',
2047
2062
  };
2048
2063
  usage = value.response.usage ?? undefined;
2064
+
2065
+ if (!encounteredStreamError && value.response.error != null) {
2066
+ encounteredStreamError = true;
2067
+ controller.enqueue({
2068
+ type: 'error',
2069
+ error: {
2070
+ type: 'response.failed',
2071
+ sequence_number: value.sequence_number,
2072
+ response: {
2073
+ error: value.response.error,
2074
+ incomplete_details: value.response.incomplete_details,
2075
+ service_tier: value.response.service_tier,
2076
+ },
2077
+ },
2078
+ });
2079
+ }
2049
2080
  } else if (isResponseAnnotationAddedChunk(value)) {
2050
2081
  ongoingAnnotations.push(value.annotation);
2051
2082
  if (value.annotation.type === 'url_citation') {
@@ -2115,6 +2146,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
2115
2146
  });
2116
2147
  }
2117
2148
  } else if (isErrorChunk(value)) {
2149
+ encounteredStreamError = true;
2150
+ finishReason = { unified: 'error', raw: 'error' };
2118
2151
  controller.enqueue({ type: 'error', error: value });
2119
2152
  }
2120
2153
  },
@@ -2140,6 +2173,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
2140
2173
  request: { body },
2141
2174
  response: { headers: responseHeaders },
2142
2175
  };
2176
+
2177
+ return result;
2143
2178
  }
2144
2179
  }
2145
2180
 
@@ -2293,6 +2328,15 @@ function isErrorChunk(
2293
2328
  return chunk.type === 'error';
2294
2329
  }
2295
2330
 
2331
+ function isResponseOutputChunk(chunk: OpenAIResponsesChunk): boolean {
2332
+ return !(
2333
+ chunk.type === 'response.created' ||
2334
+ chunk.type === 'response.failed' ||
2335
+ chunk.type === 'error' ||
2336
+ chunk.type === 'unknown_chunk'
2337
+ );
2338
+ }
2339
+
2296
2340
  function mapWebSearchOutput(
2297
2341
  action: OpenAIResponsesWebSearchAction | null | undefined,
2298
2342
  ): InferSchema<typeof webSearchOutputSchema> {