@ai-sdk/openai 4.0.53 → 4.0.55

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": "4.0.53",
3
+ "version": "4.0.55",
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.9",
39
- "@ai-sdk/provider-utils": "5.0.34"
38
+ "@ai-sdk/provider": "4.0.10",
39
+ "@ai-sdk/provider-utils": "5.0.35"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.1",
@@ -15,3 +15,13 @@ export const openaiFilesResponseSchema = lazySchema(() =>
15
15
  }),
16
16
  ),
17
17
  );
18
+
19
+ export const openaiFileDeleteResponseSchema = lazySchema(() =>
20
+ zodSchema(
21
+ z.object({
22
+ id: z.string(),
23
+ object: z.string().nullish(),
24
+ deleted: z.boolean(),
25
+ }),
26
+ ),
27
+ );
@@ -1,22 +1,40 @@
1
- import type {
2
- FilesV4,
3
- FilesV4UploadFileCallOptions,
4
- FilesV4UploadFileResult,
1
+ import {
2
+ InvalidArgumentError,
3
+ type FilesV4,
4
+ type FilesV4DeleteFileCallOptions,
5
+ type FilesV4DeleteFileResult,
6
+ type FilesV4DownloadFileCallOptions,
7
+ type FilesV4DownloadFileResult,
8
+ type FilesV4GetFileMetadataCallOptions,
9
+ type FilesV4GetFileMetadataResult,
10
+ type FilesV4UploadFileCallOptions,
11
+ type FilesV4UploadFileResult,
12
+ type SharedV4ProviderReference,
5
13
  } from '@ai-sdk/provider';
6
14
  import {
7
15
  combineHeaders,
8
16
  convertInlineFileDataToUint8Array,
17
+ createBinaryStreamResponseHandler,
9
18
  createJsonResponseHandler,
19
+ deleteFromApi,
20
+ getFromApi,
10
21
  parseProviderOptions,
11
22
  postFormDataToApi,
23
+ postMultipartStreamToApi,
12
24
  type FetchFunction,
25
+ type InferSchema,
26
+ type MultipartStreamPart,
13
27
  } from '@ai-sdk/provider-utils';
14
28
  import { openaiFailedResponseHandler } from '../openai-error';
15
- import { openaiFilesResponseSchema } from './openai-files-api';
29
+ import {
30
+ openaiFileDeleteResponseSchema,
31
+ openaiFilesResponseSchema,
32
+ } from './openai-files-api';
16
33
  import {
17
34
  openaiFilesOptionsSchema,
18
35
  type OpenAIFilesOptions,
19
36
  } from './openai-files-options';
37
+
20
38
  interface OpenAIFilesConfig {
21
39
  provider: string;
22
40
  baseURL: string;
@@ -24,6 +42,19 @@ interface OpenAIFilesConfig {
24
42
  fetch?: FetchFunction;
25
43
  }
26
44
 
45
+ type OpenAIFilesResponse = InferSchema<typeof openaiFilesResponseSchema>;
46
+
47
+ function encodePathSegment(value: string): string {
48
+ const encodedValue = encodeURIComponent(value);
49
+
50
+ // URL parsing normalizes both literal and percent-encoded dot segments.
51
+ return encodedValue === '.'
52
+ ? '%252E'
53
+ : encodedValue === '..'
54
+ ? '%252E%252E'
55
+ : encodedValue;
56
+ }
57
+
27
58
  export class OpenAIFiles implements FilesV4 {
28
59
  readonly specificationVersion = 'v4';
29
60
 
@@ -33,72 +64,245 @@ export class OpenAIFiles implements FilesV4 {
33
64
 
34
65
  constructor(private readonly config: OpenAIFilesConfig) {}
35
66
 
67
+ private getFileId(file: SharedV4ProviderReference): string {
68
+ const fileId = file.openai;
69
+ if (fileId == null || fileId.trim() === '') {
70
+ throw new InvalidArgumentError({
71
+ argument: 'file',
72
+ message: "file reference is missing an 'openai' file id.",
73
+ });
74
+ }
75
+ return fileId;
76
+ }
77
+
78
+ private getHeaders(
79
+ headers: Record<string, string | undefined> | undefined,
80
+ ): Record<string, string | undefined> {
81
+ return combineHeaders(this.config.headers(), headers);
82
+ }
83
+
36
84
  async uploadFile({
37
85
  data,
38
86
  mediaType,
39
87
  filename,
88
+ abortSignal,
89
+ headers,
40
90
  providerOptions,
41
91
  }: FilesV4UploadFileCallOptions): Promise<FilesV4UploadFileResult> {
42
- const openaiOptions = (await parseProviderOptions({
43
- provider: 'openai',
44
- providerOptions,
45
- schema: openaiFilesOptionsSchema,
46
- })) as OpenAIFilesOptions | undefined;
92
+ let openaiOptions: OpenAIFilesOptions | undefined;
93
+ try {
94
+ openaiOptions = (await parseProviderOptions({
95
+ provider: 'openai',
96
+ providerOptions,
97
+ schema: openaiFilesOptionsSchema,
98
+ })) as OpenAIFilesOptions | undefined;
99
+ } catch (error) {
100
+ // rejected before any request: release the caller's stream
101
+ if (data.type === 'stream') {
102
+ await data.stream.cancel(error).catch(() => {});
103
+ }
104
+ throw error;
105
+ }
47
106
 
48
- const fileBytes = convertInlineFileDataToUint8Array(data);
107
+ const purpose = openaiOptions?.purpose ?? 'assistants';
108
+ const requestHeaders = this.getHeaders(headers);
109
+ const url = `${this.config.baseURL}/files`;
49
110
 
50
- const blob = new Blob([fileBytes], {
51
- type: mediaType,
52
- });
111
+ let response: OpenAIFilesResponse;
53
112
 
54
- const formData = new FormData();
55
- if (filename != null) {
56
- formData.append('file', blob, filename);
113
+ if (data.type === 'stream') {
114
+ const parts: Array<MultipartStreamPart> = [
115
+ { type: 'field', name: 'purpose', value: purpose },
116
+ ];
117
+
118
+ if (openaiOptions?.expiresAfter != null) {
119
+ parts.push(
120
+ { type: 'field', name: 'expires_after[anchor]', value: 'created_at' },
121
+ {
122
+ type: 'field',
123
+ name: 'expires_after[seconds]',
124
+ value: String(openaiOptions.expiresAfter),
125
+ },
126
+ );
127
+ }
128
+
129
+ parts.push({
130
+ type: 'file',
131
+ name: 'file',
132
+ filename,
133
+ mediaType,
134
+ content: data.stream,
135
+ });
136
+
137
+ ({ value: response } = await postMultipartStreamToApi({
138
+ url,
139
+ headers: requestHeaders,
140
+ parts,
141
+ failedResponseHandler: openaiFailedResponseHandler,
142
+ successfulResponseHandler: createJsonResponseHandler(
143
+ openaiFilesResponseSchema,
144
+ ),
145
+ abortSignal,
146
+ fetch: this.config.fetch,
147
+ }));
57
148
  } else {
58
- formData.append('file', blob);
59
- }
60
- formData.append('purpose', openaiOptions?.purpose ?? 'assistants');
61
-
62
- if (openaiOptions?.expiresAfter != null) {
63
- formData.append('expires_after[anchor]', 'created_at');
64
- formData.append(
65
- 'expires_after[seconds]',
66
- String(openaiOptions.expiresAfter),
67
- );
149
+ const fileBytes = convertInlineFileDataToUint8Array(data);
150
+
151
+ const blob = new Blob([fileBytes], {
152
+ type: mediaType,
153
+ });
154
+
155
+ const formData = new FormData();
156
+ if (filename != null) {
157
+ formData.append('file', blob, filename);
158
+ } else {
159
+ formData.append('file', blob);
160
+ }
161
+ formData.append('purpose', purpose);
162
+
163
+ if (openaiOptions?.expiresAfter != null) {
164
+ formData.append('expires_after[anchor]', 'created_at');
165
+ formData.append(
166
+ 'expires_after[seconds]',
167
+ String(openaiOptions.expiresAfter),
168
+ );
169
+ }
170
+
171
+ ({ value: response } = await postFormDataToApi({
172
+ url,
173
+ headers: requestHeaders,
174
+ formData,
175
+ failedResponseHandler: openaiFailedResponseHandler,
176
+ successfulResponseHandler: createJsonResponseHandler(
177
+ openaiFilesResponseSchema,
178
+ ),
179
+ abortSignal,
180
+ fetch: this.config.fetch,
181
+ }));
68
182
  }
69
183
 
70
- const { value: response } = await postFormDataToApi({
71
- url: `${this.config.baseURL}/files`,
72
- headers: combineHeaders(this.config.headers()),
73
- formData,
184
+ return {
185
+ warnings: [],
186
+ providerReference: { openai: response.id },
187
+ ...((response.filename ?? filename)
188
+ ? { filename: response.filename ?? filename }
189
+ : {}),
190
+ ...(mediaType != null ? { mediaType } : {}),
191
+ ...(response.bytes != null ? { byteSize: response.bytes } : {}),
192
+ ...(response.created_at != null
193
+ ? { createdAt: new Date(response.created_at * 1000) }
194
+ : {}),
195
+ ...(response.expires_at != null
196
+ ? { expiresAt: new Date(response.expires_at * 1000) }
197
+ : {}),
198
+ providerMetadata: {
199
+ openai: this.toFileMetadata(response),
200
+ },
201
+ };
202
+ }
203
+
204
+ async getFileMetadata({
205
+ file,
206
+ abortSignal,
207
+ headers,
208
+ }: FilesV4GetFileMetadataCallOptions): Promise<FilesV4GetFileMetadataResult> {
209
+ const fileId = this.getFileId(file);
210
+
211
+ const { value: response } = await getFromApi({
212
+ url: `${this.config.baseURL}/files/${encodePathSegment(fileId)}`,
213
+ headers: this.getHeaders(headers),
74
214
  failedResponseHandler: openaiFailedResponseHandler,
75
215
  successfulResponseHandler: createJsonResponseHandler(
76
216
  openaiFilesResponseSchema,
77
217
  ),
218
+ abortSignal,
78
219
  fetch: this.config.fetch,
220
+ validateUrl: false,
79
221
  });
80
222
 
81
223
  return {
82
224
  warnings: [],
83
225
  providerReference: { openai: response.id },
84
- ...((response.filename ?? filename)
85
- ? { filename: response.filename ?? filename }
226
+ ...(response.filename != null ? { filename: response.filename } : {}),
227
+ ...(response.bytes != null ? { byteSize: response.bytes } : {}),
228
+ ...(response.created_at != null
229
+ ? { createdAt: new Date(response.created_at * 1000) }
230
+ : {}),
231
+ ...(response.expires_at != null
232
+ ? { expiresAt: new Date(response.expires_at * 1000) }
86
233
  : {}),
87
- ...(mediaType != null ? { mediaType } : {}),
88
234
  providerMetadata: {
89
- openai: {
90
- ...(response.filename != null ? { filename: response.filename } : {}),
91
- ...(response.purpose != null ? { purpose: response.purpose } : {}),
92
- ...(response.bytes != null ? { bytes: response.bytes } : {}),
93
- ...(response.created_at != null
94
- ? { createdAt: response.created_at }
95
- : {}),
96
- ...(response.status != null ? { status: response.status } : {}),
97
- ...(response.expires_at != null
98
- ? { expiresAt: response.expires_at }
99
- : {}),
100
- },
235
+ openai: this.toFileMetadata(response),
101
236
  },
102
237
  };
103
238
  }
239
+
240
+ async downloadFile({
241
+ file,
242
+ abortSignal,
243
+ headers,
244
+ }: FilesV4DownloadFileCallOptions): Promise<FilesV4DownloadFileResult> {
245
+ const fileId = this.getFileId(file);
246
+
247
+ const { value: content, responseHeaders } = await getFromApi({
248
+ url: `${this.config.baseURL}/files/${encodePathSegment(fileId)}/content`,
249
+ headers: this.getHeaders(headers),
250
+ failedResponseHandler: openaiFailedResponseHandler,
251
+ successfulResponseHandler: createBinaryStreamResponseHandler(),
252
+ abortSignal,
253
+ fetch: this.config.fetch,
254
+ validateUrl: false,
255
+ });
256
+
257
+ // media type from the content endpoint's Content-Type, without parameters
258
+ const mediaType = responseHeaders?.['content-type']?.split(';')[0].trim();
259
+
260
+ return {
261
+ warnings: [],
262
+ content,
263
+ ...(mediaType ? { mediaType } : {}),
264
+ };
265
+ }
266
+
267
+ async deleteFile({
268
+ file,
269
+ abortSignal,
270
+ headers,
271
+ }: FilesV4DeleteFileCallOptions): Promise<FilesV4DeleteFileResult> {
272
+ const fileId = this.getFileId(file);
273
+
274
+ const { value: response } = await deleteFromApi({
275
+ url: `${this.config.baseURL}/files/${encodePathSegment(fileId)}`,
276
+ headers: this.getHeaders(headers),
277
+ failedResponseHandler: openaiFailedResponseHandler,
278
+ successfulResponseHandler: createJsonResponseHandler(
279
+ openaiFileDeleteResponseSchema,
280
+ ),
281
+ abortSignal,
282
+ fetch: this.config.fetch,
283
+ });
284
+
285
+ return {
286
+ warnings: [],
287
+ providerReference: { openai: response.id },
288
+ deleted: response.deleted,
289
+ };
290
+ }
291
+
292
+ private toFileMetadata(
293
+ response: OpenAIFilesResponse,
294
+ ): Record<string, string | number> {
295
+ return {
296
+ ...(response.filename != null ? { filename: response.filename } : {}),
297
+ ...(response.purpose != null ? { purpose: response.purpose } : {}),
298
+ ...(response.bytes != null ? { bytes: response.bytes } : {}),
299
+ ...(response.created_at != null
300
+ ? { createdAt: response.created_at }
301
+ : {}),
302
+ ...(response.status != null ? { status: response.status } : {}),
303
+ ...(response.expires_at != null
304
+ ? { expiresAt: response.expires_at }
305
+ : {}),
306
+ };
307
+ }
104
308
  }
@@ -44,7 +44,10 @@ import {
44
44
  openaiResponsesResponseSchema,
45
45
  type OpenAIResponsesLogprobs,
46
46
  } from './responses/openai-responses-api';
47
- import { OpenAIResponsesLanguageModel } from './responses/openai-responses-language-model';
47
+ import {
48
+ mapWebSearchOutput,
49
+ OpenAIResponsesLanguageModel,
50
+ } from './responses/openai-responses-language-model';
48
51
  import type { OpenAIResponsesModelId } from './responses/openai-responses-language-model-options';
49
52
  import type { ResponsesReasoningProviderMetadata } from './responses/openai-responses-provider-metadata';
50
53
 
@@ -166,6 +169,23 @@ class OpenAIResponsesBatch {
166
169
  for (const warning of preparedRequest.warnings) {
167
170
  warnings.push({ requestId: request.id, warning });
168
171
  }
172
+
173
+ for (const tool of request.options.tools ?? []) {
174
+ if (
175
+ tool.type === 'provider' &&
176
+ !openAIBatchConvertibleProviderToolIds.has(tool.id)
177
+ ) {
178
+ warnings.push({
179
+ requestId: request.id,
180
+ warning: {
181
+ type: 'unsupported',
182
+ feature: `batch result conversion for tool "${tool.name}"`,
183
+ details:
184
+ 'OpenAI may return output for this tool that AI SDK text batches cannot currently convert.',
185
+ },
186
+ });
187
+ }
188
+ }
169
189
  }
170
190
 
171
191
  const filename = 'batch.jsonl';
@@ -385,6 +405,14 @@ class OpenAIResponsesBatch {
385
405
  }
386
406
  }
387
407
 
408
+ const openAIBatchConvertibleProviderToolIds = new Set([
409
+ 'openai.code_interpreter',
410
+ 'openai.custom',
411
+ 'openai.file_search',
412
+ 'openai.web_search',
413
+ 'openai.web_search_preview',
414
+ ]);
415
+
388
416
  export class OpenAIResponsesBatchLanguageModel
389
417
  extends OpenAIResponsesLanguageModel
390
418
  implements BatchLanguageModelV4
@@ -580,6 +608,7 @@ async function convertOpenAIResponsesBatchResponse(
580
608
 
581
609
  const content: LanguageModelV4GenerateResult['content'] = [];
582
610
  const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
611
+ let hasFunctionCall = false;
583
612
 
584
613
  for (const part of response.output) {
585
614
  switch (part.type) {
@@ -615,15 +644,104 @@ async function convertOpenAIResponsesBatchResponse(
615
644
  }
616
645
 
617
646
  case 'function_call':
647
+ hasFunctionCall = true;
648
+ content.push({
649
+ type: 'tool-call',
650
+ toolCallId: part.call_id,
651
+ toolName: part.name,
652
+ input: part.arguments,
653
+ providerMetadata: {
654
+ openai: {
655
+ itemId: part.id,
656
+ ...(part.namespace != null && { namespace: part.namespace }),
657
+ ...(part.caller != null && {
658
+ caller:
659
+ part.caller.type === 'program'
660
+ ? { type: 'program', callerId: part.caller.caller_id }
661
+ : part.caller,
662
+ }),
663
+ },
664
+ },
665
+ });
666
+ break;
667
+
618
668
  case 'custom_tool_call':
619
- return {
620
- success: false,
621
- error: {
622
- message:
623
- 'OpenAI returned a tool call, but tool calls are not supported in AI SDK text batches.',
624
- code: 'unsupported_content',
669
+ hasFunctionCall = true;
670
+ content.push({
671
+ type: 'tool-call',
672
+ toolCallId: part.call_id,
673
+ toolName: part.name,
674
+ input: JSON.stringify(part.input),
675
+ providerMetadata: { openai: { itemId: part.id } },
676
+ });
677
+ break;
678
+
679
+ case 'web_search_call':
680
+ content.push({
681
+ type: 'tool-call',
682
+ toolCallId: part.id,
683
+ toolName: 'web_search',
684
+ input: '{}',
685
+ providerExecuted: true,
686
+ dynamic: true,
687
+ });
688
+ content.push({
689
+ type: 'tool-result',
690
+ toolCallId: part.id,
691
+ toolName: 'web_search',
692
+ result: mapWebSearchOutput(part.action),
693
+ dynamic: true,
694
+ });
695
+ break;
696
+
697
+ case 'file_search_call':
698
+ content.push({
699
+ type: 'tool-call',
700
+ toolCallId: part.id,
701
+ toolName: 'file_search',
702
+ input: '{}',
703
+ providerExecuted: true,
704
+ dynamic: true,
705
+ });
706
+ content.push({
707
+ type: 'tool-result',
708
+ toolCallId: part.id,
709
+ toolName: 'file_search',
710
+ result: {
711
+ queries: part.queries,
712
+ results:
713
+ part.results?.map(result => ({
714
+ attributes: result.attributes,
715
+ fileId: result.file_id,
716
+ filename: result.filename,
717
+ score: result.score,
718
+ text: result.text,
719
+ })) ?? null,
625
720
  },
626
- };
721
+ dynamic: true,
722
+ });
723
+ break;
724
+
725
+ case 'code_interpreter_call':
726
+ content.push({
727
+ type: 'tool-call',
728
+ toolCallId: part.id,
729
+ toolName: 'code_interpreter',
730
+ input: JSON.stringify({
731
+ code: part.code,
732
+ containerId: part.container_id,
733
+ }),
734
+ providerExecuted: true,
735
+ dynamic: true,
736
+ });
737
+ content.push({
738
+ type: 'tool-result',
739
+ toolCallId: part.id,
740
+ toolName: 'code_interpreter',
741
+ result: { outputs: part.outputs },
742
+ dynamic: true,
743
+ });
744
+ break;
627
745
 
628
746
  default:
629
747
  return {
@@ -658,7 +776,7 @@ async function convertOpenAIResponsesBatchResponse(
658
776
  finishReason: {
659
777
  unified: mapOpenAIResponseFinishReason({
660
778
  finishReason: response.incomplete_details?.reason,
661
- hasFunctionCall: false,
779
+ hasFunctionCall,
662
780
  }),
663
781
  raw: response.incomplete_details?.reason ?? undefined,
664
782
  },
@@ -831,24 +831,26 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
831
831
  type: z.enum(['response.completed', 'response.incomplete']),
832
832
  response: z.object({
833
833
  incomplete_details: z.object({ reason: z.string() }).nullish(),
834
- usage: z.object({
835
- input_tokens: z.number(),
836
- input_tokens_details: z
837
- .object({
838
- cached_tokens: z.number().nullish(),
839
- cache_write_tokens: z.number().nullish(),
840
- orchestration_input_tokens: z.number().nullish(),
841
- orchestration_input_cached_tokens: z.number().nullish(),
842
- })
843
- .nullish(),
844
- output_tokens: z.number(),
845
- output_tokens_details: z
846
- .object({
847
- reasoning_tokens: z.number().nullish(),
848
- orchestration_output_tokens: z.number().nullish(),
849
- })
850
- .nullish(),
851
- }),
834
+ usage: z
835
+ .object({
836
+ input_tokens: z.number(),
837
+ input_tokens_details: z
838
+ .object({
839
+ cached_tokens: z.number().nullish(),
840
+ cache_write_tokens: z.number().nullish(),
841
+ orchestration_input_tokens: z.number().nullish(),
842
+ orchestration_input_cached_tokens: z.number().nullish(),
843
+ })
844
+ .nullish(),
845
+ output_tokens: z.number(),
846
+ output_tokens_details: z
847
+ .object({
848
+ reasoning_tokens: z.number().nullish(),
849
+ orchestration_output_tokens: z.number().nullish(),
850
+ })
851
+ .nullish(),
852
+ })
853
+ .nullish(),
852
854
  reasoning: z
853
855
  .object({
854
856
  context: z.string().nullish(),
@@ -1762,7 +1764,7 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
1762
1764
  })
1763
1765
  .nullish(),
1764
1766
  })
1765
- .optional(),
1767
+ .nullish(),
1766
1768
  }),
1767
1769
  ),
1768
1770
  );
@@ -2584,7 +2584,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
2584
2584
  raw: value.response.incomplete_details?.reason ?? undefined,
2585
2585
  };
2586
2586
  }
2587
- usage = value.response.usage;
2587
+ usage = value.response.usage ?? undefined;
2588
2588
  if (typeof value.response.service_tier === 'string') {
2589
2589
  serviceTier = value.response.service_tier;
2590
2590
  }
@@ -2915,7 +2915,7 @@ function isResponseOutputChunk(chunk: OpenAIResponsesChunk): boolean {
2915
2915
  );
2916
2916
  }
2917
2917
 
2918
- function mapWebSearchOutput(
2918
+ export function mapWebSearchOutput(
2919
2919
  action: OpenAIResponsesWebSearchAction | null | undefined,
2920
2920
  ): InferSchema<typeof webSearchOutputSchema> {
2921
2921
  if (action == null) {