@ai-sdk/xai 4.0.51 → 4.0.52

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/xai",
3
- "version": "4.0.51",
3
+ "version": "4.0.52",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -29,8 +29,8 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@ai-sdk/provider": "4.0.9",
33
- "@ai-sdk/provider-utils": "5.0.34"
32
+ "@ai-sdk/provider": "4.0.10",
33
+ "@ai-sdk/provider-utils": "5.0.35"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@ai-sdk/test-server": "2.0.1",
@@ -8,9 +8,20 @@ export const xaiFilesResponseSchema = lazySchema(() =>
8
8
  object: z.string().nullish(),
9
9
  bytes: z.number().nullish(),
10
10
  created_at: z.number().nullish(),
11
+ expires_at: z.number().nullish(),
11
12
  filename: z.string().nullish(),
12
13
  purpose: z.string().nullish(),
13
14
  status: z.string().nullish(),
14
15
  }),
15
16
  ),
16
17
  );
18
+
19
+ export const xaiFileDeleteResponseSchema = lazySchema(() =>
20
+ zodSchema(
21
+ z.object({
22
+ id: z.string(),
23
+ object: z.string().nullish(),
24
+ deleted: z.boolean(),
25
+ }),
26
+ ),
27
+ );
@@ -10,6 +10,12 @@ export const xaiFilesOptionsSchema = lazySchema(() =>
10
10
  z.looseObject({
11
11
  teamId: z.string().optional(),
12
12
  filePath: z.string().optional(),
13
+ /**
14
+ * TTL in seconds measured from upload time; xAI accepts integers
15
+ * between 3600 (1 hour) and 2592000 (30 days) inclusive.
16
+ * Omit to keep the file until it is deleted.
17
+ */
18
+ expiresAfter: z.number().int().min(3600).max(2_592_000).optional(),
13
19
  }),
14
20
  ),
15
21
  );
@@ -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 { xaiFailedResponseHandler } from '../xai-error';
15
- import { xaiFilesResponseSchema } from './xai-files-api';
29
+ import {
30
+ xaiFileDeleteResponseSchema,
31
+ xaiFilesResponseSchema,
32
+ } from './xai-files-api';
16
33
  import {
17
34
  xaiFilesOptionsSchema,
18
35
  type XaiFilesOptions,
19
36
  } from './xai-files-options';
37
+
20
38
  interface XaiFilesConfig {
21
39
  provider: string;
22
40
  baseURL: string | undefined;
@@ -24,6 +42,19 @@ interface XaiFilesConfig {
24
42
  fetch?: FetchFunction;
25
43
  }
26
44
 
45
+ type XaiFilesResponse = InferSchema<typeof xaiFilesResponseSchema>;
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 XaiFiles implements FilesV4 {
28
59
  readonly specificationVersion = 'v4';
29
60
 
@@ -33,62 +64,250 @@ export class XaiFiles implements FilesV4 {
33
64
 
34
65
  constructor(private readonly config: XaiFilesConfig) {}
35
66
 
67
+ private getFileId(file: SharedV4ProviderReference): string {
68
+ const fileId = file.xai;
69
+ if (fileId == null || fileId.trim() === '') {
70
+ throw new InvalidArgumentError({
71
+ argument: 'file',
72
+ message: "file reference is missing an 'xai' 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 xaiOptions = (await parseProviderOptions({
43
- provider: 'xai',
44
- providerOptions,
45
- schema: xaiFilesOptionsSchema,
46
- })) as XaiFilesOptions | undefined;
92
+ let xaiOptions: XaiFilesOptions | undefined;
93
+ try {
94
+ xaiOptions = (await parseProviderOptions({
95
+ provider: 'xai',
96
+ providerOptions,
97
+ schema: xaiFilesOptionsSchema,
98
+ })) as XaiFilesOptions | 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 requestHeaders = this.getHeaders(headers);
108
+ const url = `${this.config.baseURL}/files`;
49
109
 
50
- const blob = new Blob([fileBytes], {
51
- type: mediaType,
52
- });
110
+ let response: XaiFilesResponse;
111
+
112
+ if (data.type === 'stream') {
113
+ // xAI rejects uploads where expires_after arrives after the file part,
114
+ // so all fields precede the file.
115
+ const parts: Array<MultipartStreamPart> = [];
53
116
 
54
- const formData = new FormData();
55
- if (filename != null) {
56
- formData.append('file', blob, filename);
117
+ if (xaiOptions?.expiresAfter != null) {
118
+ parts.push({
119
+ type: 'field',
120
+ name: 'expires_after',
121
+ value: String(xaiOptions.expiresAfter),
122
+ });
123
+ }
124
+
125
+ if (xaiOptions?.teamId != null) {
126
+ parts.push({
127
+ type: 'field',
128
+ name: 'team_id',
129
+ value: xaiOptions.teamId,
130
+ });
131
+ }
132
+
133
+ parts.push({
134
+ type: 'file',
135
+ name: 'file',
136
+ filename,
137
+ mediaType,
138
+ content: data.stream,
139
+ });
140
+
141
+ ({ value: response } = await postMultipartStreamToApi({
142
+ url,
143
+ headers: requestHeaders,
144
+ parts,
145
+ failedResponseHandler: xaiFailedResponseHandler,
146
+ successfulResponseHandler: createJsonResponseHandler(
147
+ xaiFilesResponseSchema,
148
+ ),
149
+ abortSignal,
150
+ fetch: this.config.fetch,
151
+ }));
57
152
  } else {
58
- formData.append('file', blob);
59
- }
153
+ const fileBytes = convertInlineFileDataToUint8Array(data);
60
154
 
61
- if (xaiOptions?.teamId != null) {
62
- formData.append('team_id', xaiOptions.teamId);
155
+ const blob = new Blob([fileBytes], {
156
+ type: mediaType,
157
+ });
158
+
159
+ // FormData serializes in append order; expires_after must precede file.
160
+ const formData = new FormData();
161
+
162
+ if (xaiOptions?.expiresAfter != null) {
163
+ formData.append('expires_after', String(xaiOptions.expiresAfter));
164
+ }
165
+
166
+ if (xaiOptions?.teamId != null) {
167
+ formData.append('team_id', xaiOptions.teamId);
168
+ }
169
+
170
+ if (filename != null) {
171
+ formData.append('file', blob, filename);
172
+ } else {
173
+ formData.append('file', blob);
174
+ }
175
+
176
+ ({ value: response } = await postFormDataToApi({
177
+ url,
178
+ headers: requestHeaders,
179
+ formData,
180
+ failedResponseHandler: xaiFailedResponseHandler,
181
+ successfulResponseHandler: createJsonResponseHandler(
182
+ xaiFilesResponseSchema,
183
+ ),
184
+ abortSignal,
185
+ fetch: this.config.fetch,
186
+ }));
63
187
  }
64
188
 
65
- const { value: response } = await postFormDataToApi({
66
- url: `${this.config.baseURL}/files`,
67
- headers: combineHeaders(this.config.headers()),
68
- formData,
189
+ return {
190
+ warnings: [],
191
+ providerReference: { xai: response.id },
192
+ ...((response.filename ?? filename)
193
+ ? { filename: response.filename ?? filename }
194
+ : {}),
195
+ ...(mediaType != null ? { mediaType } : {}),
196
+ ...(response.bytes != null ? { byteSize: response.bytes } : {}),
197
+ ...(response.created_at != null
198
+ ? { createdAt: new Date(response.created_at * 1000) }
199
+ : {}),
200
+ ...(response.expires_at != null
201
+ ? { expiresAt: new Date(response.expires_at * 1000) }
202
+ : {}),
203
+ providerMetadata: {
204
+ xai: this.toFileMetadata(response),
205
+ },
206
+ };
207
+ }
208
+
209
+ async getFileMetadata({
210
+ file,
211
+ abortSignal,
212
+ headers,
213
+ }: FilesV4GetFileMetadataCallOptions): Promise<FilesV4GetFileMetadataResult> {
214
+ const fileId = this.getFileId(file);
215
+
216
+ const { value: response } = await getFromApi({
217
+ url: `${this.config.baseURL}/files/${encodePathSegment(fileId)}`,
218
+ headers: this.getHeaders(headers),
69
219
  failedResponseHandler: xaiFailedResponseHandler,
70
220
  successfulResponseHandler: createJsonResponseHandler(
71
221
  xaiFilesResponseSchema,
72
222
  ),
223
+ abortSignal,
73
224
  fetch: this.config.fetch,
225
+ validateUrl: false,
74
226
  });
75
227
 
76
228
  return {
77
229
  warnings: [],
78
230
  providerReference: { xai: response.id },
79
- ...((response.filename ?? filename)
80
- ? { filename: response.filename ?? filename }
231
+ ...(response.filename != null ? { filename: response.filename } : {}),
232
+ ...(response.bytes != null ? { byteSize: response.bytes } : {}),
233
+ ...(response.created_at != null
234
+ ? { createdAt: new Date(response.created_at * 1000) }
235
+ : {}),
236
+ ...(response.expires_at != null
237
+ ? { expiresAt: new Date(response.expires_at * 1000) }
81
238
  : {}),
82
- ...(mediaType != null ? { mediaType } : {}),
83
239
  providerMetadata: {
84
- xai: {
85
- ...(response.filename != null ? { filename: response.filename } : {}),
86
- ...(response.bytes != null ? { bytes: response.bytes } : {}),
87
- ...(response.created_at != null
88
- ? { createdAt: response.created_at }
89
- : {}),
90
- },
240
+ xai: this.toFileMetadata(response),
91
241
  },
92
242
  };
93
243
  }
244
+
245
+ async downloadFile({
246
+ file,
247
+ abortSignal,
248
+ headers,
249
+ }: FilesV4DownloadFileCallOptions): Promise<FilesV4DownloadFileResult> {
250
+ const fileId = this.getFileId(file);
251
+
252
+ const { value: content, responseHeaders } = await getFromApi({
253
+ url: `${this.config.baseURL}/files/${encodePathSegment(fileId)}/content`,
254
+ headers: this.getHeaders(headers),
255
+ failedResponseHandler: xaiFailedResponseHandler,
256
+ successfulResponseHandler: createBinaryStreamResponseHandler(),
257
+ abortSignal,
258
+ fetch: this.config.fetch,
259
+ validateUrl: false,
260
+ });
261
+
262
+ // media type from the content endpoint's Content-Type, without parameters
263
+ const mediaType = responseHeaders?.['content-type']?.split(';')[0].trim();
264
+
265
+ return {
266
+ warnings: [],
267
+ content,
268
+ ...(mediaType ? { mediaType } : {}),
269
+ };
270
+ }
271
+
272
+ async deleteFile({
273
+ file,
274
+ abortSignal,
275
+ headers,
276
+ }: FilesV4DeleteFileCallOptions): Promise<FilesV4DeleteFileResult> {
277
+ const fileId = this.getFileId(file);
278
+
279
+ const { value: response } = await deleteFromApi({
280
+ url: `${this.config.baseURL}/files/${encodePathSegment(fileId)}`,
281
+ headers: this.getHeaders(headers),
282
+ failedResponseHandler: xaiFailedResponseHandler,
283
+ successfulResponseHandler: createJsonResponseHandler(
284
+ xaiFileDeleteResponseSchema,
285
+ ),
286
+ abortSignal,
287
+ fetch: this.config.fetch,
288
+ });
289
+
290
+ return {
291
+ warnings: [],
292
+ providerReference: { xai: response.id },
293
+ deleted: response.deleted,
294
+ };
295
+ }
296
+
297
+ private toFileMetadata(
298
+ response: XaiFilesResponse,
299
+ ): Record<string, string | number> {
300
+ return {
301
+ ...(response.filename != null ? { filename: response.filename } : {}),
302
+ ...(response.purpose != null ? { purpose: response.purpose } : {}),
303
+ ...(response.bytes != null ? { bytes: response.bytes } : {}),
304
+ ...(response.created_at != null
305
+ ? { createdAt: response.created_at }
306
+ : {}),
307
+ ...(response.status != null ? { status: response.status } : {}),
308
+ ...(response.expires_at != null
309
+ ? { expiresAt: response.expires_at }
310
+ : {}),
311
+ };
312
+ }
94
313
  }