@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/CHANGELOG.md +16 -0
- package/dist/index.js +209 -46
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/files/openai-files-api.ts +10 -0
- package/src/files/openai-files.ts +250 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "4.0.
|
|
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.
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
107
|
+
const purpose = openaiOptions?.purpose ?? 'assistants';
|
|
108
|
+
const requestHeaders = this.getHeaders(headers);
|
|
109
|
+
const url = `${this.config.baseURL}/files`;
|
|
49
110
|
|
|
50
|
-
|
|
51
|
-
type: mediaType,
|
|
52
|
-
});
|
|
111
|
+
let response: OpenAIFilesResponse;
|
|
53
112
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
formData
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
...(
|
|
85
|
-
|
|
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
|
}
|