@ai-sdk/openai 4.0.32 → 4.0.33
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 +6 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2812 -2315
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +646 -22
- package/dist/internal/index.js.map +1 -1
- package/docs/03-openai.mdx +38 -0
- package/package.json +1 -1
- package/src/openai-provider.ts +6 -5
- package/src/openai-responses-batch.ts +675 -0
- package/src/responses/openai-responses-language-model.ts +1 -1
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EmptyResponseBodyError,
|
|
3
|
+
InvalidArgumentError,
|
|
4
|
+
type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
|
|
5
|
+
type Experimental_BatchV4StartOptions as BatchV4StartOptions,
|
|
6
|
+
type Experimental_BatchV4StartResult as BatchV4StartResult,
|
|
7
|
+
type Experimental_BatchV4Error as BatchV4Error,
|
|
8
|
+
type Experimental_BatchV4ItemResult as BatchV4ItemResult,
|
|
9
|
+
type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
|
|
10
|
+
type Experimental_BatchV4Status as BatchV4Status,
|
|
11
|
+
type LanguageModelV4GenerateResult,
|
|
12
|
+
type SharedV4ProviderMetadata,
|
|
13
|
+
type SharedV4Warning,
|
|
14
|
+
} from '@ai-sdk/provider';
|
|
15
|
+
import {
|
|
16
|
+
combineHeaders,
|
|
17
|
+
convertAsyncIteratorToReadableStream,
|
|
18
|
+
createJsonResponseHandler,
|
|
19
|
+
getFromApi,
|
|
20
|
+
lazySchema,
|
|
21
|
+
parseJSON,
|
|
22
|
+
postJsonToApi,
|
|
23
|
+
postToApi,
|
|
24
|
+
safeValidateTypes,
|
|
25
|
+
validateTypes,
|
|
26
|
+
WORKFLOW_DESERIALIZE,
|
|
27
|
+
WORKFLOW_SERIALIZE,
|
|
28
|
+
zodSchema,
|
|
29
|
+
type InferSchema,
|
|
30
|
+
type ResponseHandler,
|
|
31
|
+
} from '@ai-sdk/provider-utils';
|
|
32
|
+
import { z } from 'zod/v4';
|
|
33
|
+
import {
|
|
34
|
+
openaiErrorDataSchema,
|
|
35
|
+
openaiFailedResponseHandler,
|
|
36
|
+
} from './openai-error';
|
|
37
|
+
import type { OpenAIConfig } from './openai-config';
|
|
38
|
+
import { openaiFilesResponseSchema } from './files/openai-files-api';
|
|
39
|
+
import { convertOpenAIResponsesUsage } from './responses/convert-openai-responses-usage';
|
|
40
|
+
import { mapOpenAIResponseFinishReason } from './responses/map-openai-responses-finish-reason';
|
|
41
|
+
import {
|
|
42
|
+
openaiResponsesResponseSchema,
|
|
43
|
+
type OpenAIResponsesLogprobs,
|
|
44
|
+
} from './responses/openai-responses-api';
|
|
45
|
+
import { OpenAIResponsesLanguageModel } from './responses/openai-responses-language-model';
|
|
46
|
+
import type { OpenAIResponsesModelId } from './responses/openai-responses-language-model-options';
|
|
47
|
+
|
|
48
|
+
const openaiBatchEndpoint = '/v1/responses';
|
|
49
|
+
const openaiBatchInputFileExpiresAfterSeconds = 48 * 60 * 60;
|
|
50
|
+
|
|
51
|
+
type OpenAIBatchRequest = Parameters<
|
|
52
|
+
BatchLanguageModelV4['experimental_doStartBatch']
|
|
53
|
+
>[0]['requests'][number];
|
|
54
|
+
|
|
55
|
+
type OpenAIBatchPreparedRequest = {
|
|
56
|
+
body: unknown;
|
|
57
|
+
warnings: SharedV4Warning[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type OpenAIBatchResponseConversion =
|
|
61
|
+
| { success: true; result: LanguageModelV4GenerateResult }
|
|
62
|
+
| { success: false; error: BatchV4Error };
|
|
63
|
+
|
|
64
|
+
const openaiBatchResponseSchema = lazySchema(() =>
|
|
65
|
+
zodSchema(
|
|
66
|
+
z.object({
|
|
67
|
+
id: z.string(),
|
|
68
|
+
status: z.string(),
|
|
69
|
+
output_file_id: z.string().nullish(),
|
|
70
|
+
error_file_id: z.string().nullish(),
|
|
71
|
+
created_at: z.number().nullish(),
|
|
72
|
+
expires_at: z.number().nullish(),
|
|
73
|
+
request_counts: z
|
|
74
|
+
.object({
|
|
75
|
+
total: z.number().nullish(),
|
|
76
|
+
completed: z.number().nullish(),
|
|
77
|
+
failed: z.number().nullish(),
|
|
78
|
+
})
|
|
79
|
+
.nullish(),
|
|
80
|
+
errors: z
|
|
81
|
+
.object({
|
|
82
|
+
data: z
|
|
83
|
+
.array(
|
|
84
|
+
z.object({
|
|
85
|
+
code: z.string().nullish(),
|
|
86
|
+
message: z.string().nullish(),
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
.nullish(),
|
|
90
|
+
})
|
|
91
|
+
.nullish(),
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
type OpenAIBatchResponse = InferSchema<typeof openaiBatchResponseSchema>;
|
|
97
|
+
|
|
98
|
+
const openaiBatchResultLineSchema = lazySchema(() =>
|
|
99
|
+
zodSchema(
|
|
100
|
+
z.object({
|
|
101
|
+
custom_id: z.string(),
|
|
102
|
+
response: z
|
|
103
|
+
.object({
|
|
104
|
+
status_code: z.number(),
|
|
105
|
+
request_id: z.string().nullish(),
|
|
106
|
+
body: z.unknown(),
|
|
107
|
+
})
|
|
108
|
+
.nullish(),
|
|
109
|
+
error: z
|
|
110
|
+
.object({
|
|
111
|
+
code: z.string(),
|
|
112
|
+
message: z.string(),
|
|
113
|
+
})
|
|
114
|
+
.nullish(),
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
type OpenAIBatchResultLine = InferSchema<typeof openaiBatchResultLineSchema>;
|
|
120
|
+
|
|
121
|
+
class OpenAIResponsesBatch {
|
|
122
|
+
constructor(
|
|
123
|
+
private readonly options: {
|
|
124
|
+
modelId: string;
|
|
125
|
+
config: OpenAIConfig;
|
|
126
|
+
prepareRequest: (
|
|
127
|
+
request: OpenAIBatchRequest,
|
|
128
|
+
) => PromiseLike<OpenAIBatchPreparedRequest>;
|
|
129
|
+
},
|
|
130
|
+
) {}
|
|
131
|
+
|
|
132
|
+
async startBatch(
|
|
133
|
+
options: BatchV4StartOptions<OpenAIBatchRequest>,
|
|
134
|
+
): Promise<BatchV4StartResult> {
|
|
135
|
+
const fileParts: string[] = [];
|
|
136
|
+
const warnings: BatchV4StartResult['warnings'] = [];
|
|
137
|
+
|
|
138
|
+
for (const request of options.requests) {
|
|
139
|
+
const preparedRequest = await this.options.prepareRequest(request);
|
|
140
|
+
|
|
141
|
+
fileParts.push(
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
custom_id: request.id,
|
|
144
|
+
method: 'POST',
|
|
145
|
+
url: openaiBatchEndpoint,
|
|
146
|
+
body: preparedRequest.body,
|
|
147
|
+
}),
|
|
148
|
+
'\n',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
for (const warning of preparedRequest.warnings) {
|
|
152
|
+
warnings.push({ requestId: request.id, warning });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const filename = 'batch.jsonl';
|
|
157
|
+
const file = new Blob(fileParts, {
|
|
158
|
+
type: 'application/jsonl',
|
|
159
|
+
});
|
|
160
|
+
// Blob snapshots the strings, so release the potentially large input array.
|
|
161
|
+
fileParts.length = 0;
|
|
162
|
+
const formData = new FormData();
|
|
163
|
+
formData.append('file', file, filename);
|
|
164
|
+
formData.append('purpose', 'batch');
|
|
165
|
+
formData.append('expires_after[anchor]', 'created_at');
|
|
166
|
+
formData.append(
|
|
167
|
+
'expires_after[seconds]',
|
|
168
|
+
String(openaiBatchInputFileExpiresAfterSeconds),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const { value: uploadedFile } = await postToApi({
|
|
172
|
+
url: this.getUrl('/files'),
|
|
173
|
+
headers: combineHeaders(this.options.config.headers?.(), options.headers),
|
|
174
|
+
body: {
|
|
175
|
+
content: formData,
|
|
176
|
+
values: {
|
|
177
|
+
purpose: 'batch',
|
|
178
|
+
'expires_after[anchor]': 'created_at',
|
|
179
|
+
'expires_after[seconds]': String(
|
|
180
|
+
openaiBatchInputFileExpiresAfterSeconds,
|
|
181
|
+
),
|
|
182
|
+
file: {
|
|
183
|
+
name: filename,
|
|
184
|
+
type: file.type,
|
|
185
|
+
size: file.size,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
failedResponseHandler: openaiFailedResponseHandler,
|
|
190
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
191
|
+
openaiFilesResponseSchema,
|
|
192
|
+
),
|
|
193
|
+
abortSignal: options.abortSignal,
|
|
194
|
+
fetch: this.options.config.fetch,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { value: batch } = await postJsonToApi({
|
|
198
|
+
url: this.getUrl('/batches'),
|
|
199
|
+
headers: combineHeaders(this.options.config.headers?.(), options.headers),
|
|
200
|
+
body: {
|
|
201
|
+
input_file_id: uploadedFile.id,
|
|
202
|
+
endpoint: openaiBatchEndpoint,
|
|
203
|
+
completion_window: '24h',
|
|
204
|
+
},
|
|
205
|
+
failedResponseHandler: openaiFailedResponseHandler,
|
|
206
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
207
|
+
openaiBatchResponseSchema,
|
|
208
|
+
),
|
|
209
|
+
abortSignal: options.abortSignal,
|
|
210
|
+
fetch: this.options.config.fetch,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
batchId: batch.id,
|
|
215
|
+
...convertOpenAIBatchStatus(batch),
|
|
216
|
+
warnings,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async getBatchStatus(
|
|
221
|
+
options: BatchV4OperationOptions,
|
|
222
|
+
): Promise<BatchV4Status> {
|
|
223
|
+
const batch = await this.retrieveBatch(options);
|
|
224
|
+
return convertOpenAIBatchStatus(batch);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getBatchResults(
|
|
228
|
+
options: BatchV4OperationOptions,
|
|
229
|
+
): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
|
|
230
|
+
const batch = await this.retrieveBatch(options);
|
|
231
|
+
|
|
232
|
+
if (convertOpenAIBatchStatus(batch).status === 'pending') {
|
|
233
|
+
throw new InvalidArgumentError({
|
|
234
|
+
argument: 'batchId',
|
|
235
|
+
message: `OpenAI batch "${options.batchId}" is not complete.`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const fileIds = [batch.output_file_id, batch.error_file_id].filter(
|
|
240
|
+
(fileId): fileId is string => fileId != null,
|
|
241
|
+
);
|
|
242
|
+
const iterator = this.iterateBatchResults({ fileIds, options });
|
|
243
|
+
|
|
244
|
+
return convertAsyncIteratorToReadableStream(iterator);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async retrieveBatch(
|
|
248
|
+
options: BatchV4OperationOptions,
|
|
249
|
+
): Promise<OpenAIBatchResponse> {
|
|
250
|
+
const { value: batch } = await getFromApi({
|
|
251
|
+
url: this.getUrl(`/batches/${encodeURIComponent(options.batchId)}`),
|
|
252
|
+
headers: combineHeaders(this.options.config.headers?.(), options.headers),
|
|
253
|
+
failedResponseHandler: openaiFailedResponseHandler,
|
|
254
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
255
|
+
openaiBatchResponseSchema,
|
|
256
|
+
),
|
|
257
|
+
abortSignal: options.abortSignal,
|
|
258
|
+
fetch: this.options.config.fetch,
|
|
259
|
+
validateUrl: false,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return batch;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async *iterateBatchResults({
|
|
266
|
+
fileIds,
|
|
267
|
+
options,
|
|
268
|
+
}: {
|
|
269
|
+
fileIds: string[];
|
|
270
|
+
options: BatchV4OperationOptions;
|
|
271
|
+
}): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
|
|
272
|
+
for (const fileId of fileIds) {
|
|
273
|
+
const { value: stream } = await getFromApi({
|
|
274
|
+
url: this.getUrl(`/files/${encodeURIComponent(fileId)}/content`),
|
|
275
|
+
headers: combineHeaders(
|
|
276
|
+
this.options.config.headers?.(),
|
|
277
|
+
options.headers,
|
|
278
|
+
),
|
|
279
|
+
failedResponseHandler: openaiFailedResponseHandler,
|
|
280
|
+
successfulResponseHandler: rawStreamResponseHandler,
|
|
281
|
+
abortSignal: options.abortSignal,
|
|
282
|
+
fetch: this.options.config.fetch,
|
|
283
|
+
validateUrl: false,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
for await (const line of parseJsonLines(stream)) {
|
|
287
|
+
yield await this.convertResultLine(line);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async convertResultLine(
|
|
293
|
+
line: OpenAIBatchResultLine,
|
|
294
|
+
): Promise<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
|
|
295
|
+
if (line.error != null) {
|
|
296
|
+
const error = {
|
|
297
|
+
message: line.error.message,
|
|
298
|
+
code: line.error.code,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (line.error.code === 'batch_cancelled') {
|
|
302
|
+
return { id: line.custom_id, status: 'cancelled', error };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (line.error.code === 'batch_expired') {
|
|
306
|
+
return { id: line.custom_id, status: 'expired', error };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { id: line.custom_id, status: 'failed', error };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (line.response == null) {
|
|
313
|
+
return {
|
|
314
|
+
id: line.custom_id,
|
|
315
|
+
status: 'failed',
|
|
316
|
+
error: {
|
|
317
|
+
message:
|
|
318
|
+
'OpenAI returned a batch result without a response or error.',
|
|
319
|
+
code: 'invalid_batch_result',
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (line.response.status_code < 200 || line.response.status_code >= 300) {
|
|
325
|
+
return {
|
|
326
|
+
id: line.custom_id,
|
|
327
|
+
status: 'failed',
|
|
328
|
+
error: await convertOpenAIErrorResponse({
|
|
329
|
+
body: line.response.body,
|
|
330
|
+
statusCode: line.response.status_code,
|
|
331
|
+
}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const conversion = await convertOpenAIResponsesBatchResponse(
|
|
336
|
+
line.response.body,
|
|
337
|
+
);
|
|
338
|
+
if (!conversion.success) {
|
|
339
|
+
return {
|
|
340
|
+
id: line.custom_id,
|
|
341
|
+
status: 'failed',
|
|
342
|
+
error: conversion.error,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
id: line.custom_id,
|
|
348
|
+
status: 'succeeded',
|
|
349
|
+
result: conversion.result,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private getUrl(path: string) {
|
|
354
|
+
return this.options.config.url({
|
|
355
|
+
modelId: this.options.modelId,
|
|
356
|
+
path,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export class OpenAIResponsesBatchLanguageModel
|
|
362
|
+
extends OpenAIResponsesLanguageModel
|
|
363
|
+
implements BatchLanguageModelV4
|
|
364
|
+
{
|
|
365
|
+
private readonly batch: OpenAIResponsesBatch;
|
|
366
|
+
|
|
367
|
+
static [WORKFLOW_SERIALIZE](model: OpenAIResponsesLanguageModel) {
|
|
368
|
+
return OpenAIResponsesLanguageModel[WORKFLOW_SERIALIZE](model);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static [WORKFLOW_DESERIALIZE](options: {
|
|
372
|
+
modelId: OpenAIResponsesModelId;
|
|
373
|
+
config: OpenAIConfig;
|
|
374
|
+
}) {
|
|
375
|
+
return new OpenAIResponsesBatchLanguageModel(
|
|
376
|
+
options.modelId,
|
|
377
|
+
options.config,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
constructor(modelId: OpenAIResponsesModelId, config: OpenAIConfig) {
|
|
382
|
+
super(modelId, config);
|
|
383
|
+
this.batch = new OpenAIResponsesBatch({
|
|
384
|
+
modelId,
|
|
385
|
+
config,
|
|
386
|
+
prepareRequest: async request => {
|
|
387
|
+
const { args: body, warnings } = await this.getArgs(request.options);
|
|
388
|
+
|
|
389
|
+
return { body, warnings };
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
experimental_doStartBatch(
|
|
395
|
+
options: Parameters<BatchLanguageModelV4['experimental_doStartBatch']>[0],
|
|
396
|
+
) {
|
|
397
|
+
return this.batch.startBatch(options);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
experimental_doGetBatchStatus(options: BatchV4OperationOptions) {
|
|
401
|
+
return this.batch.getBatchStatus(options);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
experimental_doGetBatchResults(options: BatchV4OperationOptions) {
|
|
405
|
+
return this.batch.getBatchResults(options);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function convertOpenAIBatchStatus(batch: OpenAIBatchResponse): BatchV4Status {
|
|
410
|
+
const status = mapOpenAIBatchStatus(batch.status);
|
|
411
|
+
const firstError = batch.errors?.data?.[0];
|
|
412
|
+
const requestCounts = convertOpenAIRequestCounts(batch.request_counts);
|
|
413
|
+
const createdAt = convertUnixTimestamp(batch.created_at);
|
|
414
|
+
const expiresAt = convertUnixTimestamp(batch.expires_at);
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
status,
|
|
418
|
+
rawStatus: batch.status,
|
|
419
|
+
...(requestCounts != null ? { requestCounts } : {}),
|
|
420
|
+
...(firstError != null
|
|
421
|
+
? {
|
|
422
|
+
error: {
|
|
423
|
+
message: firstError.message ?? 'OpenAI batch failed.',
|
|
424
|
+
...(firstError.code != null ? { code: firstError.code } : {}),
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
: {}),
|
|
428
|
+
...(createdAt != null ? { createdAt } : {}),
|
|
429
|
+
...(expiresAt != null ? { expiresAt } : {}),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function mapOpenAIBatchStatus(rawStatus: string): BatchV4Status['status'] {
|
|
434
|
+
switch (rawStatus) {
|
|
435
|
+
case 'completed':
|
|
436
|
+
return 'completed';
|
|
437
|
+
case 'failed':
|
|
438
|
+
case 'expired':
|
|
439
|
+
case 'cancelled':
|
|
440
|
+
return 'failed';
|
|
441
|
+
case 'validating':
|
|
442
|
+
case 'in_progress':
|
|
443
|
+
case 'finalizing':
|
|
444
|
+
case 'cancelling':
|
|
445
|
+
default:
|
|
446
|
+
// Treat unknown provider states conservatively as non-terminal so callers
|
|
447
|
+
// do not attempt to retrieve incomplete result artifacts.
|
|
448
|
+
return 'pending';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function convertOpenAIRequestCounts(
|
|
453
|
+
counts: OpenAIBatchResponse['request_counts'],
|
|
454
|
+
): BatchV4Status['requestCounts'] | undefined {
|
|
455
|
+
const total = counts?.total;
|
|
456
|
+
const completed = counts?.completed;
|
|
457
|
+
const failed = counts?.failed;
|
|
458
|
+
|
|
459
|
+
if (
|
|
460
|
+
total == null ||
|
|
461
|
+
completed == null ||
|
|
462
|
+
failed == null ||
|
|
463
|
+
total < 0 ||
|
|
464
|
+
completed < 0 ||
|
|
465
|
+
failed < 0 ||
|
|
466
|
+
completed + failed > total
|
|
467
|
+
) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
total,
|
|
473
|
+
pending: total - completed - failed,
|
|
474
|
+
completed,
|
|
475
|
+
failed,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function convertUnixTimestamp(value: number | null | undefined) {
|
|
480
|
+
if (value == null || !Number.isFinite(value)) {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const date = new Date(value * 1000);
|
|
485
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function convertOpenAIErrorResponse({
|
|
489
|
+
body,
|
|
490
|
+
statusCode,
|
|
491
|
+
}: {
|
|
492
|
+
body: unknown;
|
|
493
|
+
statusCode: number;
|
|
494
|
+
}): Promise<BatchV4Error> {
|
|
495
|
+
const result = await safeValidateTypes({
|
|
496
|
+
value: body,
|
|
497
|
+
schema: openaiErrorDataSchema,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!result.success) {
|
|
501
|
+
return {
|
|
502
|
+
message: `OpenAI batch request failed with status code ${statusCode}.`,
|
|
503
|
+
statusCode,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
message: result.value.error.message,
|
|
509
|
+
type: result.value.error.type ?? undefined,
|
|
510
|
+
code:
|
|
511
|
+
result.value.error.code != null
|
|
512
|
+
? String(result.value.error.code)
|
|
513
|
+
: undefined,
|
|
514
|
+
statusCode,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function convertOpenAIResponsesBatchResponse(
|
|
519
|
+
body: unknown,
|
|
520
|
+
): Promise<OpenAIBatchResponseConversion> {
|
|
521
|
+
const response = await validateTypes({
|
|
522
|
+
value: body,
|
|
523
|
+
schema: openaiResponsesResponseSchema,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
if (response.error != null) {
|
|
527
|
+
return {
|
|
528
|
+
success: false,
|
|
529
|
+
error: {
|
|
530
|
+
message: response.error.message,
|
|
531
|
+
type: response.error.type,
|
|
532
|
+
code: response.error.code,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (response.output == null) {
|
|
538
|
+
const detail = response.incomplete_details?.reason;
|
|
539
|
+
return {
|
|
540
|
+
success: false,
|
|
541
|
+
error: {
|
|
542
|
+
message:
|
|
543
|
+
detail != null
|
|
544
|
+
? `OpenAI Responses returned no output (${detail}).`
|
|
545
|
+
: 'OpenAI Responses returned no output.',
|
|
546
|
+
code: 'invalid_response',
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const content: LanguageModelV4GenerateResult['content'] = [];
|
|
552
|
+
const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
|
|
553
|
+
|
|
554
|
+
for (const part of response.output) {
|
|
555
|
+
if (part.type === 'message') {
|
|
556
|
+
for (const contentPart of part.content) {
|
|
557
|
+
content.push({ type: 'text', text: contentPart.text });
|
|
558
|
+
if (contentPart.logprobs != null) {
|
|
559
|
+
logprobs.push(contentPart.logprobs);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} else if (
|
|
563
|
+
part.type === 'function_call' ||
|
|
564
|
+
part.type === 'custom_tool_call'
|
|
565
|
+
) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
error: {
|
|
569
|
+
message:
|
|
570
|
+
'OpenAI returned a tool call, but tool calls are not supported in AI SDK text batches.',
|
|
571
|
+
code: 'unsupported_tool_call',
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const providerMetadata: SharedV4ProviderMetadata = {
|
|
578
|
+
openai: {
|
|
579
|
+
responseId: response.id,
|
|
580
|
+
...(logprobs.length > 0 ? { logprobs } : {}),
|
|
581
|
+
...(typeof response.service_tier === 'string'
|
|
582
|
+
? { serviceTier: response.service_tier }
|
|
583
|
+
: {}),
|
|
584
|
+
...(response.reasoning?.context != null
|
|
585
|
+
? { reasoningContext: response.reasoning.context }
|
|
586
|
+
: {}),
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
success: true,
|
|
592
|
+
result: {
|
|
593
|
+
content,
|
|
594
|
+
finishReason: {
|
|
595
|
+
unified: mapOpenAIResponseFinishReason({
|
|
596
|
+
finishReason: response.incomplete_details?.reason,
|
|
597
|
+
hasFunctionCall: false,
|
|
598
|
+
}),
|
|
599
|
+
raw: response.incomplete_details?.reason ?? undefined,
|
|
600
|
+
},
|
|
601
|
+
usage: convertOpenAIResponsesUsage(response.usage),
|
|
602
|
+
response: {
|
|
603
|
+
id: response.id,
|
|
604
|
+
timestamp:
|
|
605
|
+
response.created_at != null
|
|
606
|
+
? new Date(response.created_at * 1000)
|
|
607
|
+
: undefined,
|
|
608
|
+
modelId: response.model,
|
|
609
|
+
},
|
|
610
|
+
providerMetadata,
|
|
611
|
+
warnings: [],
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const rawStreamResponseHandler: ResponseHandler<
|
|
617
|
+
ReadableStream<Uint8Array>
|
|
618
|
+
> = async ({ response }) => {
|
|
619
|
+
if (response.body == null) {
|
|
620
|
+
throw new EmptyResponseBodyError();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return { value: response.body };
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
async function* parseJsonLines(
|
|
627
|
+
stream: ReadableStream<Uint8Array>,
|
|
628
|
+
): AsyncGenerator<OpenAIBatchResultLine> {
|
|
629
|
+
const reader = stream.getReader();
|
|
630
|
+
const decoder = new TextDecoder();
|
|
631
|
+
let buffer = '';
|
|
632
|
+
let finished = false;
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
while (true) {
|
|
636
|
+
const { done, value } = await reader.read();
|
|
637
|
+
|
|
638
|
+
if (done) {
|
|
639
|
+
finished = true;
|
|
640
|
+
buffer += decoder.decode();
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
buffer += decoder.decode(value, { stream: true });
|
|
645
|
+
|
|
646
|
+
let lineEnd = buffer.indexOf('\n');
|
|
647
|
+
while (lineEnd !== -1) {
|
|
648
|
+
const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
|
|
649
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
650
|
+
|
|
651
|
+
if (line.trim().length > 0) {
|
|
652
|
+
yield await parseJSON({
|
|
653
|
+
text: line,
|
|
654
|
+
schema: openaiBatchResultLineSchema,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
lineEnd = buffer.indexOf('\n');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const finalLine = buffer.replace(/\r$/, '');
|
|
663
|
+
if (finalLine.trim().length > 0) {
|
|
664
|
+
yield await parseJSON({
|
|
665
|
+
text: finalLine,
|
|
666
|
+
schema: openaiBatchResultLineSchema,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
} finally {
|
|
670
|
+
if (!finished) {
|
|
671
|
+
await reader.cancel().catch(() => {});
|
|
672
|
+
}
|
|
673
|
+
reader.releaseLock();
|
|
674
|
+
}
|
|
675
|
+
}
|