@ai-sdk/openai 4.0.47 → 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 +21 -0
- package/dist/index.d.ts +29 -12
- package/dist/index.js +374 -274
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +45 -26
- package/dist/internal/index.js +270 -166
- package/dist/internal/index.js.map +1 -1
- package/docs/03-openai.mdx +31 -11
- package/package.json +2 -2
- 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 +90 -102
- 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,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@ai-sdk/provider": "4.0.8",
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.32"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@ai-sdk/test-server": "2.0.1",
|
|
@@ -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;
|
|
@@ -241,7 +241,9 @@ class OpenAIResponsesBatch {
|
|
|
241
241
|
): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
|
|
242
242
|
const batch = await this.retrieveBatch(options);
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
const batchStatus = convertOpenAIBatchStatus(batch);
|
|
245
|
+
|
|
246
|
+
if (batchStatus.status === 'pending') {
|
|
245
247
|
throw new InvalidArgumentError({
|
|
246
248
|
argument: 'batchId',
|
|
247
249
|
message: `OpenAI batch "${options.batchId}" is not complete.`,
|
|
@@ -251,6 +253,14 @@ class OpenAIResponsesBatch {
|
|
|
251
253
|
const fileIds = [batch.output_file_id, batch.error_file_id].filter(
|
|
252
254
|
(fileId): fileId is string => fileId != null,
|
|
253
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
|
+
|
|
254
264
|
const iterator = this.iterateBatchResults({ fileIds, options });
|
|
255
265
|
|
|
256
266
|
return convertAsyncIteratorToReadableStream(iterator);
|
|
@@ -282,20 +292,22 @@ class OpenAIResponsesBatch {
|
|
|
282
292
|
options: BatchV4OperationOptions;
|
|
283
293
|
}): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
|
|
284
294
|
for (const fileId of fileIds) {
|
|
285
|
-
const { value:
|
|
295
|
+
const { value: lines } = await getFromApi({
|
|
286
296
|
url: this.getUrl(`/files/${encodeURIComponent(fileId)}/content`),
|
|
287
297
|
headers: combineHeaders(
|
|
288
298
|
this.options.config.headers?.(),
|
|
289
299
|
options.headers,
|
|
290
300
|
),
|
|
291
301
|
failedResponseHandler: openaiFailedResponseHandler,
|
|
292
|
-
successfulResponseHandler:
|
|
302
|
+
successfulResponseHandler: createJsonLinesResponseHandler(
|
|
303
|
+
openaiBatchResultLineSchema,
|
|
304
|
+
),
|
|
293
305
|
abortSignal: options.abortSignal,
|
|
294
306
|
fetch: this.options.config.fetch,
|
|
295
307
|
validateUrl: false,
|
|
296
308
|
});
|
|
297
309
|
|
|
298
|
-
for await (const line of
|
|
310
|
+
for await (const line of lines) {
|
|
299
311
|
yield await this.convertResultLine(line);
|
|
300
312
|
}
|
|
301
313
|
}
|
|
@@ -468,24 +480,15 @@ function convertOpenAIRequestCounts(
|
|
|
468
480
|
const completed = counts?.completed;
|
|
469
481
|
const failed = counts?.failed;
|
|
470
482
|
|
|
471
|
-
|
|
472
|
-
total == null ||
|
|
473
|
-
completed == null ||
|
|
474
|
-
failed == null ||
|
|
475
|
-
total < 0 ||
|
|
476
|
-
completed < 0 ||
|
|
477
|
-
failed < 0 ||
|
|
478
|
-
completed + failed > total
|
|
479
|
-
) {
|
|
480
|
-
return undefined;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return {
|
|
483
|
+
return normalizeBatchRequestCounts({
|
|
484
484
|
total,
|
|
485
|
-
pending:
|
|
485
|
+
pending:
|
|
486
|
+
total != null && completed != null && failed != null
|
|
487
|
+
? total - completed - failed
|
|
488
|
+
: undefined,
|
|
486
489
|
completed,
|
|
487
490
|
failed,
|
|
488
|
-
};
|
|
491
|
+
});
|
|
489
492
|
}
|
|
490
493
|
|
|
491
494
|
function convertUnixTimestamp(value: number | null | undefined) {
|
|
@@ -530,11 +533,23 @@ async function convertOpenAIErrorResponse({
|
|
|
530
533
|
async function convertOpenAIResponsesBatchResponse(
|
|
531
534
|
body: unknown,
|
|
532
535
|
): Promise<OpenAIBatchResponseConversion> {
|
|
533
|
-
const
|
|
536
|
+
const validation = await safeValidateTypes({
|
|
534
537
|
value: body,
|
|
535
538
|
schema: openaiResponsesResponseSchema,
|
|
536
539
|
});
|
|
537
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
|
+
|
|
538
553
|
if (response.error != null) {
|
|
539
554
|
return {
|
|
540
555
|
success: false,
|
|
@@ -564,25 +579,59 @@ async function convertOpenAIResponsesBatchResponse(
|
|
|
564
579
|
const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
|
|
565
580
|
|
|
566
581
|
for (const part of response.output) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
+
});
|
|
572
600
|
}
|
|
601
|
+
break;
|
|
573
602
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
};
|
|
586
635
|
}
|
|
587
636
|
}
|
|
588
637
|
|
|
@@ -624,64 +673,3 @@ async function convertOpenAIResponsesBatchResponse(
|
|
|
624
673
|
},
|
|
625
674
|
};
|
|
626
675
|
}
|
|
627
|
-
|
|
628
|
-
const rawStreamResponseHandler: ResponseHandler<
|
|
629
|
-
ReadableStream<Uint8Array>
|
|
630
|
-
> = async ({ response }) => {
|
|
631
|
-
if (response.body == null) {
|
|
632
|
-
throw new EmptyResponseBodyError();
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return { value: response.body };
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
async function* parseJsonLines(
|
|
639
|
-
stream: ReadableStream<Uint8Array>,
|
|
640
|
-
): AsyncGenerator<OpenAIBatchResultLine> {
|
|
641
|
-
const reader = stream.getReader();
|
|
642
|
-
const decoder = new TextDecoder();
|
|
643
|
-
let buffer = '';
|
|
644
|
-
let finished = false;
|
|
645
|
-
|
|
646
|
-
try {
|
|
647
|
-
while (true) {
|
|
648
|
-
const { done, value } = await reader.read();
|
|
649
|
-
|
|
650
|
-
if (done) {
|
|
651
|
-
finished = true;
|
|
652
|
-
buffer += decoder.decode();
|
|
653
|
-
break;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
buffer += decoder.decode(value, { stream: true });
|
|
657
|
-
|
|
658
|
-
let lineEnd = buffer.indexOf('\n');
|
|
659
|
-
while (lineEnd !== -1) {
|
|
660
|
-
const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
|
|
661
|
-
buffer = buffer.slice(lineEnd + 1);
|
|
662
|
-
|
|
663
|
-
if (line.trim().length > 0) {
|
|
664
|
-
yield await parseJSON({
|
|
665
|
-
text: line,
|
|
666
|
-
schema: openaiBatchResultLineSchema,
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
lineEnd = buffer.indexOf('\n');
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const finalLine = buffer.replace(/\r$/, '');
|
|
675
|
-
if (finalLine.trim().length > 0) {
|
|
676
|
-
yield await parseJSON({
|
|
677
|
-
text: finalLine,
|
|
678
|
-
schema: openaiBatchResultLineSchema,
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
} finally {
|
|
682
|
-
if (!finished) {
|
|
683
|
-
await reader.cancel().catch(() => {});
|
|
684
|
-
}
|
|
685
|
-
reader.releaseLock();
|
|
686
|
-
}
|
|
687
|
-
}
|
|
@@ -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
|
+
}
|