@ai-sdk/openai 4.0.24 → 4.0.26
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 +10 -2
- package/dist/index.js +93 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +10 -2
- package/dist/internal/index.js +87 -6
- package/dist/internal/index.js.map +1 -1
- package/package.json +5 -5
- package/src/files/openai-files.ts +5 -1
- package/src/openai-stream-error.ts +84 -3
- package/src/responses/openai-responses-api.ts +9 -0
- package/src/responses/openai-responses-language-model.ts +8 -0
- package/src/tool/programmatic-tool-calling.ts +23 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/openai",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.26",
|
|
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": "
|
|
39
|
-
"@ai-sdk/provider
|
|
38
|
+
"@ai-sdk/provider-utils": "5.0.17",
|
|
39
|
+
"@ai-sdk/provider": "4.0.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "22.19.19",
|
|
43
43
|
"tsup": "^8.5.1",
|
|
44
44
|
"typescript": "5.8.3",
|
|
45
45
|
"zod": "3.25.76",
|
|
46
|
-
"@ai-
|
|
47
|
-
"@
|
|
46
|
+
"@vercel/ai-tsconfig": "0.0.0",
|
|
47
|
+
"@ai-sdk/test-server": "2.0.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"zod": "^3.25.76 || ^4.1.8"
|
|
@@ -60,7 +60,11 @@ export class OpenAIFiles implements FilesV4 {
|
|
|
60
60
|
formData.append('purpose', openaiOptions?.purpose ?? 'assistants');
|
|
61
61
|
|
|
62
62
|
if (openaiOptions?.expiresAfter != null) {
|
|
63
|
-
formData.append('expires_after',
|
|
63
|
+
formData.append('expires_after[anchor]', 'created_at');
|
|
64
|
+
formData.append(
|
|
65
|
+
'expires_after[seconds]',
|
|
66
|
+
String(openaiOptions.expiresAfter),
|
|
67
|
+
);
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
const { value: response } = await postFormDataToApi({
|
|
@@ -12,6 +12,8 @@ export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
|
|
|
12
12
|
stream,
|
|
13
13
|
getError,
|
|
14
14
|
isOutputChunk,
|
|
15
|
+
isAcceptedChunk,
|
|
16
|
+
acceptedGraceMs = 50,
|
|
15
17
|
url,
|
|
16
18
|
requestBodyValues,
|
|
17
19
|
responseHeaders,
|
|
@@ -19,16 +21,44 @@ export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
|
|
|
19
21
|
stream: ReadableStream<ParseResult<T>>;
|
|
20
22
|
getError: (chunk: T) => unknown | undefined;
|
|
21
23
|
isOutputChunk: (chunk: T) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Marks a chunk that proves the request was accepted and generation has
|
|
26
|
+
* started (e.g. the Responses API `response.in_progress` event). Once seen,
|
|
27
|
+
* the early-error peek stops blocking indefinitely: each subsequent read is
|
|
28
|
+
* raced against `acceptedGraceMs` so error frames that are flushed together
|
|
29
|
+
* with the accepted chunk (e.g. `insufficient_quota`) still throw, while a
|
|
30
|
+
* healthy stream becomes available without waiting for the first output
|
|
31
|
+
* token.
|
|
32
|
+
*/
|
|
33
|
+
isAcceptedChunk?: (chunk: T) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* How long to keep peeking for an error frame after an accepted chunk was
|
|
36
|
+
* seen. Only used when `isAcceptedChunk` is provided.
|
|
37
|
+
*/
|
|
38
|
+
acceptedGraceMs?: number;
|
|
22
39
|
url: string;
|
|
23
40
|
requestBodyValues: unknown;
|
|
24
41
|
responseHeaders?: Record<string, string>;
|
|
25
42
|
}): Promise<ReadableStream<ParseResult<T>>> {
|
|
26
43
|
const [streamForEarlyError, streamForConsumer] = stream.tee();
|
|
27
44
|
const reader = streamForEarlyError.getReader();
|
|
45
|
+
let drainAfterError = false;
|
|
28
46
|
|
|
29
47
|
try {
|
|
48
|
+
let accepted = false;
|
|
49
|
+
|
|
30
50
|
while (true) {
|
|
31
|
-
|
|
51
|
+
let result: ReadableStreamReadResult<ParseResult<T>>;
|
|
52
|
+
|
|
53
|
+
if (accepted) {
|
|
54
|
+
const raced = await raceWithTimeout(reader.read(), acceptedGraceMs);
|
|
55
|
+
if (raced.timedOut) {
|
|
56
|
+
return streamForConsumer;
|
|
57
|
+
}
|
|
58
|
+
result = raced.value;
|
|
59
|
+
} else {
|
|
60
|
+
result = await reader.read();
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
if (result.done) {
|
|
34
64
|
return streamForConsumer;
|
|
@@ -43,7 +73,12 @@ export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
|
|
|
43
73
|
const errorFrame = getError(chunk.value);
|
|
44
74
|
|
|
45
75
|
if (errorFrame != null) {
|
|
46
|
-
|
|
76
|
+
// Let the source finish instead of cancelling its transform pipeline.
|
|
77
|
+
// Node.js 26 can otherwise leave a queued pipe write rejected with
|
|
78
|
+
// the cancellation reason after the API error has already surfaced.
|
|
79
|
+
drainAfterError = true;
|
|
80
|
+
drainReader(reader).catch(() => {});
|
|
81
|
+
drainReader(streamForConsumer.getReader()).catch(() => {});
|
|
47
82
|
throw createOpenAIStreamError({
|
|
48
83
|
frame: errorFrame,
|
|
49
84
|
url,
|
|
@@ -55,13 +90,59 @@ export async function throwIfOpenAIStreamErrorBeforeOutput<T>({
|
|
|
55
90
|
if (isOutputChunk(chunk.value)) {
|
|
56
91
|
return streamForConsumer;
|
|
57
92
|
}
|
|
93
|
+
|
|
94
|
+
if (!accepted && isAcceptedChunk?.(chunk.value) === true) {
|
|
95
|
+
accepted = true;
|
|
96
|
+
}
|
|
58
97
|
}
|
|
59
98
|
} finally {
|
|
60
|
-
|
|
99
|
+
if (!drainAfterError) {
|
|
100
|
+
reader.cancel().catch(() => {});
|
|
101
|
+
reader.releaseLock();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function drainReader<T>(
|
|
107
|
+
reader: ReadableStreamDefaultReader<T>,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
while (!(await reader.read()).done) {
|
|
111
|
+
// Drain the source without retaining its remaining chunks.
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// The API error has already been reported to the caller.
|
|
115
|
+
} finally {
|
|
61
116
|
reader.releaseLock();
|
|
62
117
|
}
|
|
63
118
|
}
|
|
64
119
|
|
|
120
|
+
async function raceWithTimeout<T>(
|
|
121
|
+
promise: Promise<T>,
|
|
122
|
+
timeoutMs: number,
|
|
123
|
+
): Promise<{ timedOut: false; value: T } | { timedOut: true }> {
|
|
124
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
125
|
+
const wrapped = promise.then(value => ({ timedOut: false as const, value }));
|
|
126
|
+
try {
|
|
127
|
+
const raced = await Promise.race([
|
|
128
|
+
wrapped,
|
|
129
|
+
new Promise<{ timedOut: true }>(resolve => {
|
|
130
|
+
timer = setTimeout(() => resolve({ timedOut: true }), timeoutMs);
|
|
131
|
+
}),
|
|
132
|
+
]);
|
|
133
|
+
if (raced.timedOut) {
|
|
134
|
+
// The losing read settles later (typically when the peek branch is
|
|
135
|
+
// cancelled after the stream is handed to the consumer, or rejects if
|
|
136
|
+
// the source errors). Adopt it so it can never surface as an
|
|
137
|
+
// unhandled rejection; the consumer branch sees the same outcome.
|
|
138
|
+
wrapped.catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
return raced;
|
|
141
|
+
} finally {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
65
146
|
function createOpenAIStreamError({
|
|
66
147
|
frame,
|
|
67
148
|
url,
|
|
@@ -798,6 +798,15 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
|
798
798
|
service_tier: z.string().nullish(),
|
|
799
799
|
}),
|
|
800
800
|
}),
|
|
801
|
+
z.object({
|
|
802
|
+
type: z.literal('response.in_progress'),
|
|
803
|
+
response: z.object({
|
|
804
|
+
id: z.string(),
|
|
805
|
+
created_at: z.number(),
|
|
806
|
+
model: z.string(),
|
|
807
|
+
service_tier: z.string().nullish(),
|
|
808
|
+
}),
|
|
809
|
+
}),
|
|
801
810
|
z.object({
|
|
802
811
|
type: z.literal('response.output_item.added'),
|
|
803
812
|
output_index: z.number(),
|
|
@@ -1316,6 +1316,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
1316
1316
|
? chunk
|
|
1317
1317
|
: undefined,
|
|
1318
1318
|
isOutputChunk: isResponseOutputChunk,
|
|
1319
|
+
isAcceptedChunk: isResponseInProgressChunk,
|
|
1319
1320
|
url,
|
|
1320
1321
|
requestBodyValues: body,
|
|
1321
1322
|
responseHeaders,
|
|
@@ -2674,9 +2675,16 @@ function isErrorChunk(
|
|
|
2674
2675
|
return chunk.type === 'error';
|
|
2675
2676
|
}
|
|
2676
2677
|
|
|
2678
|
+
function isResponseInProgressChunk(
|
|
2679
|
+
chunk: OpenAIResponsesChunk,
|
|
2680
|
+
): chunk is OpenAIResponsesChunk & { type: 'response.in_progress' } {
|
|
2681
|
+
return chunk.type === 'response.in_progress';
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2677
2684
|
function isResponseOutputChunk(chunk: OpenAIResponsesChunk): boolean {
|
|
2678
2685
|
return !(
|
|
2679
2686
|
chunk.type === 'response.created' ||
|
|
2687
|
+
chunk.type === 'response.in_progress' ||
|
|
2680
2688
|
chunk.type === 'response.failed' ||
|
|
2681
2689
|
chunk.type === 'error' ||
|
|
2682
2690
|
chunk.type === 'unknown_chunk'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createProviderExecutedToolFactory,
|
|
3
|
+
experimental_toolCaller,
|
|
3
4
|
lazySchema,
|
|
4
5
|
zodSchema,
|
|
5
6
|
} from '@ai-sdk/provider-utils';
|
|
@@ -54,4 +55,25 @@ const programmaticToolCallingFactory = createProviderExecutedToolFactory<
|
|
|
54
55
|
supportsDeferredResults: true,
|
|
55
56
|
});
|
|
56
57
|
|
|
57
|
-
export const programmaticToolCalling = () =>
|
|
58
|
+
export const programmaticToolCalling = () =>
|
|
59
|
+
experimental_toolCaller(programmaticToolCallingFactory({}), {
|
|
60
|
+
type: 'provider',
|
|
61
|
+
prepareProviderOptions: providerOptions => {
|
|
62
|
+
const openaiOptions = providerOptions?.openai as
|
|
63
|
+
| { allowedCallers?: Array<'direct' | 'programmatic'> }
|
|
64
|
+
| undefined;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...providerOptions,
|
|
68
|
+
openai: {
|
|
69
|
+
...openaiOptions,
|
|
70
|
+
allowedCallers: [
|
|
71
|
+
...new Set([
|
|
72
|
+
...(openaiOptions?.allowedCallers ?? []),
|
|
73
|
+
'programmatic' as const,
|
|
74
|
+
]),
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|