@ai-sdk/openai 4.0.54 → 4.0.56

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.54",
3
+ "version": "4.0.56",
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.36"
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
  }