@ai-sdk/openai 3.0.80 → 3.0.82
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 +20 -0
- package/dist/index.d.mts +16 -9
- package/dist/index.d.ts +16 -9
- package/dist/index.js +276 -91
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -70
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.d.mts +16 -9
- package/dist/internal/index.d.ts +16 -9
- package/dist/internal/index.js +275 -90
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +253 -69
- package/dist/internal/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/chat/convert-to-openai-chat-messages.ts +1 -1
- package/src/chat/openai-chat-language-model.ts +56 -44
- package/src/completion/openai-completion-language-model.ts +26 -6
- package/src/openai-stream-error.ts +181 -0
- package/src/responses/convert-to-openai-responses-input.ts +2 -2
- package/src/responses/openai-responses-api.ts +27 -10
- package/src/responses/openai-responses-language-model.ts +46 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.82",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@ai-sdk/provider": "3.0.13",
|
|
40
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
40
|
+
"@ai-sdk/provider-utils": "4.0.37"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "20.17.24",
|
|
@@ -200,7 +200,7 @@ export function convertToOpenAIChatMessages({
|
|
|
200
200
|
contentValue = output.value;
|
|
201
201
|
break;
|
|
202
202
|
case 'execution-denied':
|
|
203
|
-
contentValue = output.reason ?? 'Tool execution denied.';
|
|
203
|
+
contentValue = output.reason ?? 'Tool call execution denied.';
|
|
204
204
|
break;
|
|
205
205
|
case 'content':
|
|
206
206
|
case 'json':
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
createEventSourceResponseHandler,
|
|
16
16
|
createJsonResponseHandler,
|
|
17
17
|
generateId,
|
|
18
|
-
isParsableJson,
|
|
19
18
|
parseProviderOptions,
|
|
20
19
|
postJsonToApi,
|
|
21
20
|
type FetchFunction,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
} from '@ai-sdk/provider-utils';
|
|
24
23
|
import { openaiFailedResponseHandler } from '../openai-error';
|
|
25
24
|
import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
|
|
25
|
+
import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
|
|
26
26
|
import {
|
|
27
27
|
convertOpenAIChatUsage,
|
|
28
28
|
type OpenAIChatUsage,
|
|
@@ -414,11 +414,13 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
414
414
|
},
|
|
415
415
|
};
|
|
416
416
|
|
|
417
|
+
const url = this.config.url({
|
|
418
|
+
path: '/chat/completions',
|
|
419
|
+
modelId: this.modelId,
|
|
420
|
+
});
|
|
421
|
+
|
|
417
422
|
const { responseHeaders, value: response } = await postJsonToApi({
|
|
418
|
-
url
|
|
419
|
-
path: '/chat/completions',
|
|
420
|
-
modelId: this.modelId,
|
|
421
|
-
}),
|
|
423
|
+
url,
|
|
422
424
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
423
425
|
body,
|
|
424
426
|
failedResponseHandler: openaiFailedResponseHandler,
|
|
@@ -429,6 +431,15 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
429
431
|
fetch: this.config.fetch,
|
|
430
432
|
});
|
|
431
433
|
|
|
434
|
+
const checkedResponse = await throwIfOpenAIStreamErrorBeforeOutput({
|
|
435
|
+
stream: response,
|
|
436
|
+
getError: chunk => ('error' in chunk ? chunk.error : undefined),
|
|
437
|
+
isOutputChunk: isOpenAIChatOutputChunk,
|
|
438
|
+
url,
|
|
439
|
+
requestBodyValues: body,
|
|
440
|
+
responseHeaders,
|
|
441
|
+
});
|
|
442
|
+
|
|
432
443
|
const toolCalls: Array<{
|
|
433
444
|
id: string;
|
|
434
445
|
type: 'function';
|
|
@@ -449,8 +460,8 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
449
460
|
|
|
450
461
|
const providerMetadata: SharedV3ProviderMetadata = { openai: {} };
|
|
451
462
|
|
|
452
|
-
|
|
453
|
-
stream:
|
|
463
|
+
const result = {
|
|
464
|
+
stream: checkedResponse.pipeThrough(
|
|
454
465
|
new TransformStream<
|
|
455
466
|
ParseResult<OpenAIChatChunk>,
|
|
456
467
|
LanguageModelV3StreamPart
|
|
@@ -605,23 +616,6 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
605
616
|
delta: toolCall.function.arguments,
|
|
606
617
|
});
|
|
607
618
|
}
|
|
608
|
-
|
|
609
|
-
// check if tool call is complete
|
|
610
|
-
// (some providers send the full tool call in one chunk):
|
|
611
|
-
if (isParsableJson(toolCall.function.arguments)) {
|
|
612
|
-
controller.enqueue({
|
|
613
|
-
type: 'tool-input-end',
|
|
614
|
-
id: toolCall.id,
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
controller.enqueue({
|
|
618
|
-
type: 'tool-call',
|
|
619
|
-
toolCallId: toolCall.id ?? generateId(),
|
|
620
|
-
toolName: toolCall.function.name,
|
|
621
|
-
input: toolCall.function.arguments,
|
|
622
|
-
});
|
|
623
|
-
toolCall.hasFinished = true;
|
|
624
|
-
}
|
|
625
619
|
}
|
|
626
620
|
|
|
627
621
|
continue;
|
|
@@ -645,26 +639,6 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
645
639
|
id: toolCall.id,
|
|
646
640
|
delta: toolCallDelta.function.arguments ?? '',
|
|
647
641
|
});
|
|
648
|
-
|
|
649
|
-
// check if tool call is complete
|
|
650
|
-
if (
|
|
651
|
-
toolCall.function?.name != null &&
|
|
652
|
-
toolCall.function?.arguments != null &&
|
|
653
|
-
isParsableJson(toolCall.function.arguments)
|
|
654
|
-
) {
|
|
655
|
-
controller.enqueue({
|
|
656
|
-
type: 'tool-input-end',
|
|
657
|
-
id: toolCall.id,
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
controller.enqueue({
|
|
661
|
-
type: 'tool-call',
|
|
662
|
-
toolCallId: toolCall.id ?? generateId(),
|
|
663
|
-
toolName: toolCall.function.name,
|
|
664
|
-
input: toolCall.function.arguments,
|
|
665
|
-
});
|
|
666
|
-
toolCall.hasFinished = true;
|
|
667
|
-
}
|
|
668
642
|
}
|
|
669
643
|
}
|
|
670
644
|
|
|
@@ -687,6 +661,26 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
687
661
|
controller.enqueue({ type: 'text-end', id: '0' });
|
|
688
662
|
}
|
|
689
663
|
|
|
664
|
+
// Finalize any unfinished tool calls. Tool calls are only
|
|
665
|
+
// finalized here (on stream end) to prevent premature
|
|
666
|
+
// execution when partial JSON is coincidentally parsable.
|
|
667
|
+
for (const toolCall of toolCalls) {
|
|
668
|
+
if (!toolCall.hasFinished) {
|
|
669
|
+
controller.enqueue({
|
|
670
|
+
type: 'tool-input-end',
|
|
671
|
+
id: toolCall.id,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
controller.enqueue({
|
|
675
|
+
type: 'tool-call',
|
|
676
|
+
toolCallId: toolCall.id ?? generateId(),
|
|
677
|
+
toolName: toolCall.function.name,
|
|
678
|
+
input: toolCall.function.arguments,
|
|
679
|
+
});
|
|
680
|
+
toolCall.hasFinished = true;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
690
684
|
controller.enqueue({
|
|
691
685
|
type: 'finish',
|
|
692
686
|
finishReason,
|
|
@@ -699,5 +693,23 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
|
|
|
699
693
|
request: { body },
|
|
700
694
|
response: { headers: responseHeaders },
|
|
701
695
|
};
|
|
696
|
+
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function isOpenAIChatOutputChunk(chunk: OpenAIChatChunk): boolean {
|
|
702
|
+
if ('error' in chunk) {
|
|
703
|
+
return false;
|
|
702
704
|
}
|
|
705
|
+
|
|
706
|
+
return chunk.choices.some(choice => {
|
|
707
|
+
const delta = choice.delta;
|
|
708
|
+
|
|
709
|
+
return (
|
|
710
|
+
(delta?.content != null && delta.content.length > 0) ||
|
|
711
|
+
(delta?.tool_calls != null && delta.tool_calls.length > 0) ||
|
|
712
|
+
(delta?.annotations != null && delta.annotations.length > 0)
|
|
713
|
+
);
|
|
714
|
+
});
|
|
703
715
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ParseResult,
|
|
19
19
|
} from '@ai-sdk/provider-utils';
|
|
20
20
|
import { openaiFailedResponseHandler } from '../openai-error';
|
|
21
|
+
import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
|
|
21
22
|
import {
|
|
22
23
|
convertOpenAICompletionUsage,
|
|
23
24
|
type OpenAICompletionUsage,
|
|
@@ -224,11 +225,13 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
|
|
|
224
225
|
},
|
|
225
226
|
};
|
|
226
227
|
|
|
228
|
+
const url = this.config.url({
|
|
229
|
+
path: '/completions',
|
|
230
|
+
modelId: this.modelId,
|
|
231
|
+
});
|
|
232
|
+
|
|
227
233
|
const { responseHeaders, value: response } = await postJsonToApi({
|
|
228
|
-
url
|
|
229
|
-
path: '/completions',
|
|
230
|
-
modelId: this.modelId,
|
|
231
|
-
}),
|
|
234
|
+
url,
|
|
232
235
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
233
236
|
body,
|
|
234
237
|
failedResponseHandler: openaiFailedResponseHandler,
|
|
@@ -239,6 +242,15 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
|
|
|
239
242
|
fetch: this.config.fetch,
|
|
240
243
|
});
|
|
241
244
|
|
|
245
|
+
const checkedResponse = await throwIfOpenAIStreamErrorBeforeOutput({
|
|
246
|
+
stream: response,
|
|
247
|
+
getError: chunk => ('error' in chunk ? chunk.error : undefined),
|
|
248
|
+
isOutputChunk: isOpenAICompletionOutputChunk,
|
|
249
|
+
url,
|
|
250
|
+
requestBodyValues: body,
|
|
251
|
+
responseHeaders,
|
|
252
|
+
});
|
|
253
|
+
|
|
242
254
|
let finishReason: LanguageModelV3FinishReason = {
|
|
243
255
|
unified: 'other',
|
|
244
256
|
raw: undefined,
|
|
@@ -247,8 +259,8 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
|
|
|
247
259
|
let usage: OpenAICompletionUsage | undefined = undefined;
|
|
248
260
|
let isFirstChunk = true;
|
|
249
261
|
|
|
250
|
-
|
|
251
|
-
stream:
|
|
262
|
+
const result = {
|
|
263
|
+
stream: checkedResponse.pipeThrough(
|
|
252
264
|
new TransformStream<
|
|
253
265
|
ParseResult<OpenAICompletionChunk>,
|
|
254
266
|
LanguageModelV3StreamPart
|
|
@@ -332,5 +344,13 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 {
|
|
|
332
344
|
request: { body },
|
|
333
345
|
response: { headers: responseHeaders },
|
|
334
346
|
};
|
|
347
|
+
|
|
348
|
+
return result;
|
|
335
349
|
}
|
|
336
350
|
}
|
|
351
|
+
|
|
352
|
+
function isOpenAICompletionOutputChunk(chunk: OpenAICompletionChunk): boolean {
|
|
353
|
+
return (
|
|
354
|
+
!('error' in chunk) && chunk.choices.some(choice => choice.text.length > 0)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { APICallError } from '@ai-sdk/provider';
|
|
2
|
+
import type { ParseResult } from '@ai-sdk/provider-utils';
|
|
3
|
+
|
|
4
|
+
type StreamError = {
|
|
5
|
+
message: string;
|
|
6
|
+
code?: string | number | null;
|
|
7
|
+
type?: string | null;
|
|
8
|
+
frame: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
|
|
12
|
+
stream,
|
|
13
|
+
getError,
|
|
14
|
+
isOutputChunk,
|
|
15
|
+
url,
|
|
16
|
+
requestBodyValues,
|
|
17
|
+
responseHeaders,
|
|
18
|
+
}: {
|
|
19
|
+
stream: ReadableStream<ParseResult<T>>;
|
|
20
|
+
getError: (chunk: T) => unknown | undefined;
|
|
21
|
+
isOutputChunk: (chunk: T) => boolean;
|
|
22
|
+
url: string;
|
|
23
|
+
requestBodyValues: unknown;
|
|
24
|
+
responseHeaders?: Record<string, string>;
|
|
25
|
+
}): Promise<ReadableStream<ParseResult<T>>> {
|
|
26
|
+
const [streamForEarlyError, streamForConsumer] = stream.tee();
|
|
27
|
+
const reader = streamForEarlyError.getReader();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
while (true) {
|
|
31
|
+
const result = await reader.read();
|
|
32
|
+
|
|
33
|
+
if (result.done) {
|
|
34
|
+
return streamForConsumer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const chunk = result.value;
|
|
38
|
+
|
|
39
|
+
if (!chunk.success) {
|
|
40
|
+
return streamForConsumer;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const errorFrame = getError(chunk.value);
|
|
44
|
+
|
|
45
|
+
if (errorFrame != null) {
|
|
46
|
+
streamForConsumer.cancel().catch(() => {});
|
|
47
|
+
throw createOpenAIStreamError({
|
|
48
|
+
frame: errorFrame,
|
|
49
|
+
url,
|
|
50
|
+
requestBodyValues,
|
|
51
|
+
responseHeaders,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isOutputChunk(chunk.value)) {
|
|
56
|
+
return streamForConsumer;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
reader.cancel().catch(() => {});
|
|
61
|
+
reader.releaseLock();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createOpenAIStreamError({
|
|
66
|
+
frame,
|
|
67
|
+
url,
|
|
68
|
+
requestBodyValues,
|
|
69
|
+
responseHeaders,
|
|
70
|
+
}: {
|
|
71
|
+
frame: unknown;
|
|
72
|
+
url: string;
|
|
73
|
+
requestBodyValues: unknown;
|
|
74
|
+
responseHeaders?: Record<string, string>;
|
|
75
|
+
}): APICallError {
|
|
76
|
+
const streamError = parseStreamError(frame);
|
|
77
|
+
return new APICallError({
|
|
78
|
+
message:
|
|
79
|
+
streamError?.message ??
|
|
80
|
+
'OpenAI stream failed before any output was generated',
|
|
81
|
+
url,
|
|
82
|
+
requestBodyValues,
|
|
83
|
+
statusCode: streamError == null ? 500 : getStatusCode(streamError),
|
|
84
|
+
responseHeaders,
|
|
85
|
+
responseBody: JSON.stringify(frame),
|
|
86
|
+
data: frame,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseStreamError(frame: unknown): StreamError | undefined {
|
|
91
|
+
const value = asRecord(frame);
|
|
92
|
+
|
|
93
|
+
if (value == null) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (value.type === 'response.failed') {
|
|
98
|
+
const response = asRecord(value.response);
|
|
99
|
+
const responseError = asRecord(response?.error);
|
|
100
|
+
|
|
101
|
+
return typeof responseError?.message === 'string'
|
|
102
|
+
? {
|
|
103
|
+
message: responseError.message,
|
|
104
|
+
code: getStringOrNumber(responseError.code),
|
|
105
|
+
type: 'response.failed',
|
|
106
|
+
frame,
|
|
107
|
+
}
|
|
108
|
+
: undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const error = asRecord(value.error) ?? value;
|
|
112
|
+
|
|
113
|
+
return typeof error.message === 'string' &&
|
|
114
|
+
(asRecord(value.error) != null ||
|
|
115
|
+
typeof error.type === 'string' ||
|
|
116
|
+
'code' in error ||
|
|
117
|
+
'param' in error)
|
|
118
|
+
? {
|
|
119
|
+
message: error.message,
|
|
120
|
+
code: getStringOrNumber(error.code),
|
|
121
|
+
type: typeof error.type === 'string' ? error.type : undefined,
|
|
122
|
+
frame,
|
|
123
|
+
}
|
|
124
|
+
: undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getStatusCode(error: StreamError): number {
|
|
128
|
+
if (typeof error.code === 'number' && isHttpErrorStatusCode(error.code)) {
|
|
129
|
+
return error.code;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof error.code === 'string' && /^\d{3}$/.test(error.code)) {
|
|
133
|
+
const numericCode = Number(error.code);
|
|
134
|
+
if (isHttpErrorStatusCode(numericCode)) {
|
|
135
|
+
return numericCode;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const discriminator = [error.code, error.type]
|
|
140
|
+
.filter(value => typeof value === 'string' || typeof value === 'number')
|
|
141
|
+
.join(' ')
|
|
142
|
+
.toLowerCase();
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
['insufficient_quota', 'rate_limit'].some(term =>
|
|
146
|
+
discriminator.includes(term),
|
|
147
|
+
)
|
|
148
|
+
) {
|
|
149
|
+
return 429;
|
|
150
|
+
}
|
|
151
|
+
if (discriminator.includes('authentication')) return 401;
|
|
152
|
+
if (discriminator.includes('permission')) return 403;
|
|
153
|
+
if (discriminator.includes('not_found')) return 404;
|
|
154
|
+
if (
|
|
155
|
+
['invalid', 'bad_request', 'context_length'].some(term =>
|
|
156
|
+
discriminator.includes(term),
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
return 400;
|
|
160
|
+
}
|
|
161
|
+
if (discriminator.includes('overload')) return 503;
|
|
162
|
+
if (discriminator.includes('timeout')) return 504;
|
|
163
|
+
|
|
164
|
+
return 500;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
168
|
+
return typeof value === 'object' && value != null
|
|
169
|
+
? (value as Record<string, unknown>)
|
|
170
|
+
: undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getStringOrNumber(value: unknown): string | number | undefined {
|
|
174
|
+
return typeof value === 'string' || typeof value === 'number'
|
|
175
|
+
? value
|
|
176
|
+
: undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isHttpErrorStatusCode(value: number): boolean {
|
|
180
|
+
return Number.isInteger(value) && value >= 400 && value <= 599;
|
|
181
|
+
}
|
|
@@ -734,7 +734,7 @@ export async function convertToOpenAIResponsesInput({
|
|
|
734
734
|
outputValue = output.value;
|
|
735
735
|
break;
|
|
736
736
|
case 'execution-denied':
|
|
737
|
-
outputValue = output.reason ?? 'Tool execution denied.';
|
|
737
|
+
outputValue = output.reason ?? 'Tool call execution denied.';
|
|
738
738
|
break;
|
|
739
739
|
case 'json':
|
|
740
740
|
case 'error-json':
|
|
@@ -801,7 +801,7 @@ export async function convertToOpenAIResponsesInput({
|
|
|
801
801
|
contentValue = output.value;
|
|
802
802
|
break;
|
|
803
803
|
case 'execution-denied':
|
|
804
|
-
contentValue = output.reason ?? 'Tool execution denied.';
|
|
804
|
+
contentValue = output.reason ?? 'Tool call execution denied.';
|
|
805
805
|
break;
|
|
806
806
|
case 'json':
|
|
807
807
|
case 'error-json':
|
|
@@ -472,6 +472,30 @@ export type OpenAIResponsesReasoning = {
|
|
|
472
472
|
}>;
|
|
473
473
|
};
|
|
474
474
|
|
|
475
|
+
// Captured from the Responses API when OpenAI returned an early
|
|
476
|
+
// insufficient_quota stream error after HTTP 200. This shape differs from the
|
|
477
|
+
// currently documented ResponseErrorEvent below.
|
|
478
|
+
const openaiResponsesNestedErrorChunkSchema = z.object({
|
|
479
|
+
type: z.literal('error'),
|
|
480
|
+
sequence_number: z.number(),
|
|
481
|
+
error: z.object({
|
|
482
|
+
type: z.string(),
|
|
483
|
+
code: z.string(),
|
|
484
|
+
message: z.string(),
|
|
485
|
+
param: z.string().nullish(),
|
|
486
|
+
}),
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Current OpenAI OpenAPI docs define ResponseErrorEvent with top-level
|
|
490
|
+
// code/message/param fields.
|
|
491
|
+
const openaiResponsesErrorChunkSchema = z.object({
|
|
492
|
+
type: z.literal('error'),
|
|
493
|
+
sequence_number: z.number(),
|
|
494
|
+
code: z.string().nullish(),
|
|
495
|
+
message: z.string(),
|
|
496
|
+
param: z.string().nullish(),
|
|
497
|
+
});
|
|
498
|
+
|
|
475
499
|
export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
476
500
|
zodSchema(
|
|
477
501
|
z.union([
|
|
@@ -520,6 +544,7 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
|
520
544
|
}),
|
|
521
545
|
z.object({
|
|
522
546
|
type: z.literal('response.failed'),
|
|
547
|
+
sequence_number: z.number(),
|
|
523
548
|
response: z.object({
|
|
524
549
|
error: z
|
|
525
550
|
.object({
|
|
@@ -1034,16 +1059,8 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
|
1034
1059
|
output_index: z.number(),
|
|
1035
1060
|
diff: z.string(),
|
|
1036
1061
|
}),
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
sequence_number: z.number(),
|
|
1040
|
-
error: z.object({
|
|
1041
|
-
type: z.string(),
|
|
1042
|
-
code: z.string(),
|
|
1043
|
-
message: z.string(),
|
|
1044
|
-
param: z.string().nullish(),
|
|
1045
|
-
}),
|
|
1046
|
-
}),
|
|
1062
|
+
openaiResponsesNestedErrorChunkSchema,
|
|
1063
|
+
openaiResponsesErrorChunkSchema,
|
|
1047
1064
|
z
|
|
1048
1065
|
.object({ type: z.string() })
|
|
1049
1066
|
.loose()
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import type { OpenAIConfig } from '../openai-config';
|
|
29
29
|
import { openaiFailedResponseHandler } from '../openai-error';
|
|
30
30
|
import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
|
|
31
|
+
import { throwIfOpenAIStreamErrorBeforeOutput } from '../openai-stream-error';
|
|
31
32
|
import type { applyPatchInputSchema } from '../tool/apply-patch';
|
|
32
33
|
import type {
|
|
33
34
|
codeInterpreterInputSchema,
|
|
@@ -1057,6 +1058,19 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
1057
1058
|
fetch: this.config.fetch,
|
|
1058
1059
|
});
|
|
1059
1060
|
|
|
1061
|
+
const checkedResponse = await throwIfOpenAIStreamErrorBeforeOutput({
|
|
1062
|
+
stream: response,
|
|
1063
|
+
getError: chunk =>
|
|
1064
|
+
isErrorChunk(chunk) ||
|
|
1065
|
+
(isResponseFailedChunk(chunk) && chunk.response.error != null)
|
|
1066
|
+
? chunk
|
|
1067
|
+
: undefined,
|
|
1068
|
+
isOutputChunk: isResponseOutputChunk,
|
|
1069
|
+
url,
|
|
1070
|
+
requestBodyValues: body,
|
|
1071
|
+
responseHeaders,
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1060
1074
|
const self = this;
|
|
1061
1075
|
|
|
1062
1076
|
const approvalRequestIdToDummyToolCallIdFromPrompt =
|
|
@@ -1117,9 +1131,10 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
1117
1131
|
|
|
1118
1132
|
let serviceTier: string | undefined;
|
|
1119
1133
|
const hostedToolSearchCallIds: string[] = [];
|
|
1134
|
+
let encounteredStreamError = false;
|
|
1120
1135
|
|
|
1121
|
-
|
|
1122
|
-
stream:
|
|
1136
|
+
const result = {
|
|
1137
|
+
stream: checkedResponse.pipeThrough(
|
|
1123
1138
|
new TransformStream<
|
|
1124
1139
|
ParseResult<OpenAIResponsesChunk>,
|
|
1125
1140
|
LanguageModelV3StreamPart
|
|
@@ -2046,6 +2061,22 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
2046
2061
|
raw: incompleteReason ?? 'error',
|
|
2047
2062
|
};
|
|
2048
2063
|
usage = value.response.usage ?? undefined;
|
|
2064
|
+
|
|
2065
|
+
if (!encounteredStreamError && value.response.error != null) {
|
|
2066
|
+
encounteredStreamError = true;
|
|
2067
|
+
controller.enqueue({
|
|
2068
|
+
type: 'error',
|
|
2069
|
+
error: {
|
|
2070
|
+
type: 'response.failed',
|
|
2071
|
+
sequence_number: value.sequence_number,
|
|
2072
|
+
response: {
|
|
2073
|
+
error: value.response.error,
|
|
2074
|
+
incomplete_details: value.response.incomplete_details,
|
|
2075
|
+
service_tier: value.response.service_tier,
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2049
2080
|
} else if (isResponseAnnotationAddedChunk(value)) {
|
|
2050
2081
|
ongoingAnnotations.push(value.annotation);
|
|
2051
2082
|
if (value.annotation.type === 'url_citation') {
|
|
@@ -2115,6 +2146,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
2115
2146
|
});
|
|
2116
2147
|
}
|
|
2117
2148
|
} else if (isErrorChunk(value)) {
|
|
2149
|
+
encounteredStreamError = true;
|
|
2150
|
+
finishReason = { unified: 'error', raw: 'error' };
|
|
2118
2151
|
controller.enqueue({ type: 'error', error: value });
|
|
2119
2152
|
}
|
|
2120
2153
|
},
|
|
@@ -2140,6 +2173,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
2140
2173
|
request: { body },
|
|
2141
2174
|
response: { headers: responseHeaders },
|
|
2142
2175
|
};
|
|
2176
|
+
|
|
2177
|
+
return result;
|
|
2143
2178
|
}
|
|
2144
2179
|
}
|
|
2145
2180
|
|
|
@@ -2293,6 +2328,15 @@ function isErrorChunk(
|
|
|
2293
2328
|
return chunk.type === 'error';
|
|
2294
2329
|
}
|
|
2295
2330
|
|
|
2331
|
+
function isResponseOutputChunk(chunk: OpenAIResponsesChunk): boolean {
|
|
2332
|
+
return !(
|
|
2333
|
+
chunk.type === 'response.created' ||
|
|
2334
|
+
chunk.type === 'response.failed' ||
|
|
2335
|
+
chunk.type === 'error' ||
|
|
2336
|
+
chunk.type === 'unknown_chunk'
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2296
2340
|
function mapWebSearchOutput(
|
|
2297
2341
|
action: OpenAIResponsesWebSearchAction | null | undefined,
|
|
2298
2342
|
): InferSchema<typeof webSearchOutputSchema> {
|