@ai-sdk/openai 4.0.46 → 4.0.49
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 +31 -0
- package/dist/index.d.ts +29 -12
- package/dist/index.js +384 -276
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +45 -26
- package/dist/internal/index.js +271 -167
- package/dist/internal/index.js.map +1 -1
- package/docs/03-openai.mdx +31 -11
- package/package.json +6 -6
- package/src/chat/convert-openai-chat-usage.ts +1 -1
- package/src/chat/convert-to-openai-chat-messages.ts +5 -1
- package/src/chat/openai-chat-language-model.ts +9 -2
- package/src/completion/openai-completion-language-model.ts +9 -2
- package/src/embedding/openai-embedding-model.ts +2 -0
- package/src/openai-responses-batch.ts +103 -103
- package/src/openai-stream-error.ts +68 -15
- package/src/responses/openai-responses-api.ts +114 -40
- package/src/responses/openai-responses-language-model.ts +28 -18
package/docs/03-openai.mdx
CHANGED
|
@@ -1956,40 +1956,60 @@ The metadata includes the following fields:
|
|
|
1956
1956
|
### Text Batches
|
|
1957
1957
|
|
|
1958
1958
|
<Note type="warning">
|
|
1959
|
-
Text batch
|
|
1959
|
+
Text batch support is experimental and the API may change in patch releases.
|
|
1960
1960
|
</Note>
|
|
1961
1961
|
|
|
1962
|
-
The OpenAI provider supports
|
|
1962
|
+
The OpenAI provider supports asynchronous text generation through the
|
|
1963
|
+
[Batch API](https://developers.openai.com/api/docs/guides/batch). Use the
|
|
1964
|
+
experimental text batch APIs to start a batch, poll its status, and stream its
|
|
1965
|
+
results:
|
|
1963
1966
|
|
|
1964
1967
|
```ts
|
|
1965
1968
|
import { openai } from '@ai-sdk/openai';
|
|
1966
1969
|
import {
|
|
1967
|
-
experimental_startTextBatch as startTextBatch,
|
|
1968
1970
|
experimental_getBatchResults as getBatchResults,
|
|
1969
1971
|
experimental_getBatchStatus as getBatchStatus,
|
|
1972
|
+
experimental_startTextBatch as startTextBatch,
|
|
1970
1973
|
} from 'ai';
|
|
1974
|
+
import { setTimeout } from 'node:timers/promises';
|
|
1971
1975
|
|
|
1972
1976
|
const model = openai('gpt-4.1-nano');
|
|
1973
1977
|
|
|
1974
1978
|
const batch = await startTextBatch({
|
|
1975
1979
|
model,
|
|
1976
1980
|
requests: [
|
|
1977
|
-
{ id: 'france', prompt: 'What is the capital of France?' },
|
|
1978
|
-
{ id: 'germany', prompt: 'What is the capital of Germany?' },
|
|
1981
|
+
{ id: 'capital-france', prompt: 'What is the capital of France?' },
|
|
1982
|
+
{ id: 'capital-germany', prompt: 'What is the capital of Germany?' },
|
|
1979
1983
|
],
|
|
1980
1984
|
});
|
|
1981
1985
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1986
|
+
let status = batch.status;
|
|
1987
|
+
while (status === 'pending') {
|
|
1988
|
+
await setTimeout(60_000);
|
|
1989
|
+
({ status } = await getBatchStatus({ model, batch }));
|
|
1990
|
+
}
|
|
1984
1991
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
console.log(
|
|
1992
|
+
for await (const item of getBatchResults({ model, batch })) {
|
|
1993
|
+
if (item.status === 'succeeded') {
|
|
1994
|
+
console.log(item.id, item.text);
|
|
1995
|
+
} else {
|
|
1996
|
+
console.error(item.id, item.error);
|
|
1988
1997
|
}
|
|
1989
1998
|
}
|
|
1990
1999
|
```
|
|
1991
2000
|
|
|
1992
|
-
|
|
2001
|
+
`startTextBatch` returns a serializable batch reference. Persist this reference
|
|
2002
|
+
to check the batch status or retrieve its results from another process. Results
|
|
2003
|
+
can arrive in a different order from the input requests, so match each result by
|
|
2004
|
+
its `id`.
|
|
2005
|
+
|
|
2006
|
+
#### Webhooks
|
|
2007
|
+
|
|
2008
|
+
<Note>
|
|
2009
|
+
The OpenAI Batch API does not support per-batch webhooks. When you provide a
|
|
2010
|
+
`webhookUrl`, the provider returns an unsupported warning and starts the batch
|
|
2011
|
+
without a webhook.
|
|
2012
|
+
</Note>
|
|
1993
2013
|
|
|
1994
2014
|
### Chat Models
|
|
1995
2015
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,16 +35,16 @@
|
|
|
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.8",
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.32"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
+
"@ai-sdk/test-server": "2.0.1",
|
|
42
43
|
"@types/node": "22.19.19",
|
|
44
|
+
"@vercel/ai-tsconfig": "0.0.0",
|
|
43
45
|
"tsup": "^8.5.1",
|
|
44
46
|
"typescript": "5.8.3",
|
|
45
|
-
"zod": "3.25.76"
|
|
46
|
-
"@ai-sdk/test-server": "2.0.1",
|
|
47
|
-
"@vercel/ai-tsconfig": "0.0.0"
|
|
47
|
+
"zod": "3.25.76"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"zod": "^3.25.76 || ^4.1.8"
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
} from '@ai-sdk/provider-utils';
|
|
14
14
|
|
|
15
15
|
function serializeToolCallArguments(input: unknown): string {
|
|
16
|
-
return JSON.stringify(
|
|
16
|
+
return JSON.stringify(
|
|
17
|
+
typeof input === 'object' && input !== null && !Array.isArray(input)
|
|
18
|
+
? input
|
|
19
|
+
: {},
|
|
20
|
+
);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
type OpenAIPromptCacheBreakpoint = { mode: 'explicit' };
|
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
} from '@ai-sdk/provider-utils';
|
|
27
27
|
import { openaiFailedResponseHandler } from '../openai-error';
|
|
28
28
|
import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createOpenAIProviderStreamError,
|
|
31
|
+
throwIfOpenAIStreamErrorBeforeOutput,
|
|
32
|
+
} from '../openai-stream-error';
|
|
30
33
|
import {
|
|
31
34
|
convertOpenAIChatUsage,
|
|
32
35
|
type OpenAIChatUsage,
|
|
@@ -509,7 +512,11 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
509
512
|
// handle error chunks:
|
|
510
513
|
if ('error' in value) {
|
|
511
514
|
finishReason = { unified: 'error', raw: undefined };
|
|
512
|
-
controller.enqueue({
|
|
515
|
+
controller.enqueue({
|
|
516
|
+
type: 'error',
|
|
517
|
+
error:
|
|
518
|
+
createOpenAIProviderStreamError(value.error) ?? value.error,
|
|
519
|
+
});
|
|
513
520
|
return;
|
|
514
521
|
}
|
|
515
522
|
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
type ParseResult,
|
|
22
22
|
} from '@ai-sdk/provider-utils';
|
|
23
23
|
import { openaiFailedResponseHandler } from '../openai-error';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
createOpenAIProviderStreamError,
|
|
26
|
+
throwIfOpenAIStreamErrorBeforeOutput,
|
|
27
|
+
} from '../openai-stream-error';
|
|
25
28
|
import {
|
|
26
29
|
convertOpenAICompletionUsage,
|
|
27
30
|
type OpenAICompletionUsage,
|
|
@@ -302,7 +305,11 @@ export class OpenAICompletionLanguageModel implements LanguageModelV4 {
|
|
|
302
305
|
// handle error chunks:
|
|
303
306
|
if ('error' in value) {
|
|
304
307
|
finishReason = { unified: 'error', raw: undefined };
|
|
305
|
-
controller.enqueue({
|
|
308
|
+
controller.enqueue({
|
|
309
|
+
type: 'error',
|
|
310
|
+
error:
|
|
311
|
+
createOpenAIProviderStreamError(value.error) ?? value.error,
|
|
312
|
+
});
|
|
306
313
|
return;
|
|
307
314
|
}
|
|
308
315
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
combineHeaders,
|
|
7
7
|
createJsonResponseHandler,
|
|
8
|
+
EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL,
|
|
8
9
|
parseProviderOptions,
|
|
9
10
|
postJsonToApi,
|
|
10
11
|
serializeModelOptions,
|
|
@@ -23,6 +24,7 @@ export class OpenAIEmbeddingModel implements EmbeddingModelV4 {
|
|
|
23
24
|
readonly specificationVersion = 'v4';
|
|
24
25
|
readonly modelId: OpenAIEmbeddingModelId;
|
|
25
26
|
readonly maxEmbeddingsPerCall = 2048;
|
|
27
|
+
readonly [EXPERIMENTAL_EMBEDDING_MODEL_MAX_INPUT_BYTES_PER_CALL] = 300_000;
|
|
26
28
|
readonly supportsParallelCalls = true;
|
|
27
29
|
|
|
28
30
|
private readonly config: OpenAIConfig;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
EmptyResponseBodyError,
|
|
3
2
|
InvalidArgumentError,
|
|
3
|
+
InvalidResponseDataError,
|
|
4
4
|
type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
|
|
5
5
|
type Experimental_BatchV4StartOptions as BatchV4StartOptions,
|
|
6
6
|
type Experimental_BatchV4StartResult as BatchV4StartResult,
|
|
@@ -15,19 +15,18 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
combineHeaders,
|
|
17
17
|
convertAsyncIteratorToReadableStream,
|
|
18
|
+
createJsonLinesResponseHandler,
|
|
18
19
|
createJsonResponseHandler,
|
|
19
20
|
getFromApi,
|
|
20
21
|
lazySchema,
|
|
21
|
-
|
|
22
|
+
normalizeBatchRequestCounts,
|
|
22
23
|
postJsonToApi,
|
|
23
24
|
postToApi,
|
|
24
25
|
safeValidateTypes,
|
|
25
|
-
validateTypes,
|
|
26
26
|
WORKFLOW_DESERIALIZE,
|
|
27
27
|
WORKFLOW_SERIALIZE,
|
|
28
28
|
zodSchema,
|
|
29
29
|
type InferSchema,
|
|
30
|
-
type ResponseHandler,
|
|
31
30
|
} from '@ai-sdk/provider-utils';
|
|
32
31
|
import { z } from 'zod/v4';
|
|
33
32
|
import {
|
|
@@ -44,6 +43,7 @@ import {
|
|
|
44
43
|
} from './responses/openai-responses-api';
|
|
45
44
|
import { OpenAIResponsesLanguageModel } from './responses/openai-responses-language-model';
|
|
46
45
|
import type { OpenAIResponsesModelId } from './responses/openai-responses-language-model-options';
|
|
46
|
+
import type { ResponsesReasoningProviderMetadata } from './responses/openai-responses-provider-metadata';
|
|
47
47
|
|
|
48
48
|
const openaiBatchEndpoint = '/v1/responses';
|
|
49
49
|
const openaiBatchInputFileExpiresAfterSeconds = 48 * 60 * 60;
|
|
@@ -133,7 +133,19 @@ class OpenAIResponsesBatch {
|
|
|
133
133
|
options: BatchV4StartOptions<OpenAIBatchRequest>,
|
|
134
134
|
): Promise<BatchV4StartResult> {
|
|
135
135
|
const fileParts: string[] = [];
|
|
136
|
-
const warnings: BatchV4StartResult['warnings'] =
|
|
136
|
+
const warnings: BatchV4StartResult['warnings'] =
|
|
137
|
+
options.webhookUrl == null
|
|
138
|
+
? []
|
|
139
|
+
: [
|
|
140
|
+
{
|
|
141
|
+
warning: {
|
|
142
|
+
type: 'unsupported',
|
|
143
|
+
feature: 'webhookUrl',
|
|
144
|
+
details:
|
|
145
|
+
'The OpenAI Batch API does not support per-batch webhook URLs.',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
137
149
|
|
|
138
150
|
for (const request of options.requests) {
|
|
139
151
|
const preparedRequest = await this.options.prepareRequest(request);
|
|
@@ -229,7 +241,9 @@ class OpenAIResponsesBatch {
|
|
|
229
241
|
): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
|
|
230
242
|
const batch = await this.retrieveBatch(options);
|
|
231
243
|
|
|
232
|
-
|
|
244
|
+
const batchStatus = convertOpenAIBatchStatus(batch);
|
|
245
|
+
|
|
246
|
+
if (batchStatus.status === 'pending') {
|
|
233
247
|
throw new InvalidArgumentError({
|
|
234
248
|
argument: 'batchId',
|
|
235
249
|
message: `OpenAI batch "${options.batchId}" is not complete.`,
|
|
@@ -239,6 +253,14 @@ class OpenAIResponsesBatch {
|
|
|
239
253
|
const fileIds = [batch.output_file_id, batch.error_file_id].filter(
|
|
240
254
|
(fileId): fileId is string => fileId != null,
|
|
241
255
|
);
|
|
256
|
+
|
|
257
|
+
if (batchStatus.status === 'completed' && fileIds.length === 0) {
|
|
258
|
+
throw new InvalidResponseDataError({
|
|
259
|
+
data: batch,
|
|
260
|
+
message: `OpenAI batch "${options.batchId}" completed without batch output.`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
242
264
|
const iterator = this.iterateBatchResults({ fileIds, options });
|
|
243
265
|
|
|
244
266
|
return convertAsyncIteratorToReadableStream(iterator);
|
|
@@ -270,20 +292,22 @@ class OpenAIResponsesBatch {
|
|
|
270
292
|
options: BatchV4OperationOptions;
|
|
271
293
|
}): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
|
|
272
294
|
for (const fileId of fileIds) {
|
|
273
|
-
const { value:
|
|
295
|
+
const { value: lines } = await getFromApi({
|
|
274
296
|
url: this.getUrl(`/files/${encodeURIComponent(fileId)}/content`),
|
|
275
297
|
headers: combineHeaders(
|
|
276
298
|
this.options.config.headers?.(),
|
|
277
299
|
options.headers,
|
|
278
300
|
),
|
|
279
301
|
failedResponseHandler: openaiFailedResponseHandler,
|
|
280
|
-
successfulResponseHandler:
|
|
302
|
+
successfulResponseHandler: createJsonLinesResponseHandler(
|
|
303
|
+
openaiBatchResultLineSchema,
|
|
304
|
+
),
|
|
281
305
|
abortSignal: options.abortSignal,
|
|
282
306
|
fetch: this.options.config.fetch,
|
|
283
307
|
validateUrl: false,
|
|
284
308
|
});
|
|
285
309
|
|
|
286
|
-
for await (const line of
|
|
310
|
+
for await (const line of lines) {
|
|
287
311
|
yield await this.convertResultLine(line);
|
|
288
312
|
}
|
|
289
313
|
}
|
|
@@ -456,24 +480,15 @@ function convertOpenAIRequestCounts(
|
|
|
456
480
|
const completed = counts?.completed;
|
|
457
481
|
const failed = counts?.failed;
|
|
458
482
|
|
|
459
|
-
|
|
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 {
|
|
483
|
+
return normalizeBatchRequestCounts({
|
|
472
484
|
total,
|
|
473
|
-
pending:
|
|
485
|
+
pending:
|
|
486
|
+
total != null && completed != null && failed != null
|
|
487
|
+
? total - completed - failed
|
|
488
|
+
: undefined,
|
|
474
489
|
completed,
|
|
475
490
|
failed,
|
|
476
|
-
};
|
|
491
|
+
});
|
|
477
492
|
}
|
|
478
493
|
|
|
479
494
|
function convertUnixTimestamp(value: number | null | undefined) {
|
|
@@ -518,11 +533,23 @@ async function convertOpenAIErrorResponse({
|
|
|
518
533
|
async function convertOpenAIResponsesBatchResponse(
|
|
519
534
|
body: unknown,
|
|
520
535
|
): Promise<OpenAIBatchResponseConversion> {
|
|
521
|
-
const
|
|
536
|
+
const validation = await safeValidateTypes({
|
|
522
537
|
value: body,
|
|
523
538
|
schema: openaiResponsesResponseSchema,
|
|
524
539
|
});
|
|
525
540
|
|
|
541
|
+
if (!validation.success) {
|
|
542
|
+
return {
|
|
543
|
+
success: false,
|
|
544
|
+
error: {
|
|
545
|
+
message: 'OpenAI returned an invalid Responses batch result.',
|
|
546
|
+
code: 'invalid_response',
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const response = validation.value;
|
|
552
|
+
|
|
526
553
|
if (response.error != null) {
|
|
527
554
|
return {
|
|
528
555
|
success: false,
|
|
@@ -552,25 +579,59 @@ async function convertOpenAIResponsesBatchResponse(
|
|
|
552
579
|
const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
|
|
553
580
|
|
|
554
581
|
for (const part of response.output) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
582
|
+
switch (part.type) {
|
|
583
|
+
case 'reasoning': {
|
|
584
|
+
const summaries =
|
|
585
|
+
part.summary.length > 0
|
|
586
|
+
? part.summary
|
|
587
|
+
: [{ type: 'summary_text' as const, text: '' }];
|
|
588
|
+
|
|
589
|
+
for (const summary of summaries) {
|
|
590
|
+
content.push({
|
|
591
|
+
type: 'reasoning',
|
|
592
|
+
text: summary.text,
|
|
593
|
+
providerMetadata: {
|
|
594
|
+
openai: {
|
|
595
|
+
itemId: part.id,
|
|
596
|
+
reasoningEncryptedContent: part.encrypted_content ?? null,
|
|
597
|
+
} satisfies ResponsesReasoningProviderMetadata,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
560
600
|
}
|
|
601
|
+
break;
|
|
561
602
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
603
|
+
|
|
604
|
+
case 'message': {
|
|
605
|
+
for (const contentPart of part.content) {
|
|
606
|
+
content.push({ type: 'text', text: contentPart.text });
|
|
607
|
+
if (contentPart.logprobs != null) {
|
|
608
|
+
logprobs.push(contentPart.logprobs);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
case 'function_call':
|
|
615
|
+
case 'custom_tool_call':
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
error: {
|
|
619
|
+
message:
|
|
620
|
+
'OpenAI returned a tool call, but tool calls are not supported in AI SDK text batches.',
|
|
621
|
+
code: 'unsupported_content',
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
default:
|
|
626
|
+
return {
|
|
627
|
+
success: false,
|
|
628
|
+
error: {
|
|
629
|
+
message:
|
|
630
|
+
`OpenAI returned an unsupported "${part.type}" output item ` +
|
|
631
|
+
'in an AI SDK text batch.',
|
|
632
|
+
code: 'unsupported_content',
|
|
633
|
+
},
|
|
634
|
+
};
|
|
574
635
|
}
|
|
575
636
|
}
|
|
576
637
|
|
|
@@ -612,64 +673,3 @@ async function convertOpenAIResponsesBatchResponse(
|
|
|
612
673
|
},
|
|
613
674
|
};
|
|
614
675
|
}
|
|
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
|
-
}
|
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { APICallError } from '@ai-sdk/provider';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
createProviderStreamError,
|
|
4
|
+
type ParseResult,
|
|
5
|
+
type ProviderStreamError,
|
|
6
|
+
} from '@ai-sdk/provider-utils';
|
|
3
7
|
|
|
4
8
|
type StreamError = {
|
|
5
9
|
message: string;
|
|
6
10
|
code?: string | number | null;
|
|
7
11
|
type?: string | null;
|
|
8
|
-
frame: unknown;
|
|
9
12
|
};
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Converts an OpenAI stream error frame into provider-owned metadata that AI
|
|
16
|
+
* SDK Core can normalize without duplicating OpenAI error-code semantics.
|
|
17
|
+
*/
|
|
18
|
+
export function createOpenAIProviderStreamError(
|
|
19
|
+
frame: unknown,
|
|
20
|
+
): ProviderStreamError | undefined {
|
|
21
|
+
const streamError = parseStreamError(frame);
|
|
22
|
+
|
|
23
|
+
if (streamError == null) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const statusCode = getStatusCode(streamError);
|
|
28
|
+
|
|
29
|
+
return createProviderStreamError({
|
|
30
|
+
message: streamError.message,
|
|
31
|
+
type: streamError.type ?? undefined,
|
|
32
|
+
code: streamError.code ?? undefined,
|
|
33
|
+
statusCode,
|
|
34
|
+
isRetryable: isRetryableStreamError(streamError, statusCode),
|
|
35
|
+
data: frame,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
|
|
12
40
|
stream,
|
|
13
41
|
getError,
|
|
@@ -154,17 +182,18 @@ function createOpenAIStreamError({
|
|
|
154
182
|
requestBodyValues: unknown;
|
|
155
183
|
responseHeaders?: Record<string, string>;
|
|
156
184
|
}): APICallError {
|
|
157
|
-
const streamError =
|
|
185
|
+
const streamError = createOpenAIProviderStreamError(frame);
|
|
158
186
|
return new APICallError({
|
|
159
187
|
message:
|
|
160
188
|
streamError?.message ??
|
|
161
189
|
'OpenAI stream failed before any output was generated',
|
|
162
190
|
url,
|
|
163
191
|
requestBodyValues,
|
|
164
|
-
statusCode: streamError
|
|
192
|
+
statusCode: streamError?.statusCode ?? 500,
|
|
165
193
|
responseHeaders,
|
|
166
194
|
responseBody: JSON.stringify(frame),
|
|
167
195
|
data: frame,
|
|
196
|
+
isRetryable: streamError?.isRetryable,
|
|
168
197
|
});
|
|
169
198
|
}
|
|
170
199
|
|
|
@@ -184,7 +213,6 @@ function parseStreamError(frame: unknown): StreamError | undefined {
|
|
|
184
213
|
message: responseError.message,
|
|
185
214
|
code: getStringOrNumber(responseError.code),
|
|
186
215
|
type: 'response.failed',
|
|
187
|
-
frame,
|
|
188
216
|
}
|
|
189
217
|
: undefined;
|
|
190
218
|
}
|
|
@@ -200,21 +228,14 @@ function parseStreamError(frame: unknown): StreamError | undefined {
|
|
|
200
228
|
message: error.message,
|
|
201
229
|
code: getStringOrNumber(error.code),
|
|
202
230
|
type: typeof error.type === 'string' ? error.type : undefined,
|
|
203
|
-
frame,
|
|
204
231
|
}
|
|
205
232
|
: undefined;
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
function getStatusCode(error: StreamError): number {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (typeof error.code === 'string' && /^\d{3}$/.test(error.code)) {
|
|
214
|
-
const numericCode = Number(error.code);
|
|
215
|
-
if (isHttpErrorStatusCode(numericCode)) {
|
|
216
|
-
return numericCode;
|
|
217
|
-
}
|
|
236
|
+
const explicitStatusCode = getHttpStatusCode(error.code);
|
|
237
|
+
if (explicitStatusCode != null) {
|
|
238
|
+
return explicitStatusCode;
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
const discriminator = [error.code, error.type]
|
|
@@ -260,3 +281,35 @@ function getStringOrNumber(value: unknown): string | number | undefined {
|
|
|
260
281
|
function isHttpErrorStatusCode(value: number): boolean {
|
|
261
282
|
return Number.isInteger(value) && value >= 400 && value <= 599;
|
|
262
283
|
}
|
|
284
|
+
|
|
285
|
+
function getHttpStatusCode(value: string | number | null | undefined) {
|
|
286
|
+
const statusCode =
|
|
287
|
+
typeof value === 'string' && /^\d{3}$/.test(value) ? Number(value) : value;
|
|
288
|
+
|
|
289
|
+
return typeof statusCode === 'number' && isHttpErrorStatusCode(statusCode)
|
|
290
|
+
? statusCode
|
|
291
|
+
: undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isRetryableStatusCode(statusCode: number): boolean {
|
|
295
|
+
return (
|
|
296
|
+
statusCode === 408 ||
|
|
297
|
+
statusCode === 409 ||
|
|
298
|
+
statusCode === 429 ||
|
|
299
|
+
statusCode >= 500
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isRetryableStreamError(
|
|
304
|
+
error: StreamError,
|
|
305
|
+
statusCode: number,
|
|
306
|
+
): boolean {
|
|
307
|
+
if (
|
|
308
|
+
error.code === 'insufficient_quota' ||
|
|
309
|
+
error.type === 'insufficient_quota'
|
|
310
|
+
) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return isRetryableStatusCode(statusCode);
|
|
315
|
+
}
|