@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.24",
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": "4.0.4",
39
- "@ai-sdk/provider-utils": "5.0.15"
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-sdk/test-server": "2.0.0",
47
- "@vercel/ai-tsconfig": "0.0.0"
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', String(openaiOptions.expiresAfter));
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
- const result = await reader.read();
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
- streamForConsumer.cancel().catch(() => {});
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
- reader.cancel().catch(() => {});
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 = () => programmaticToolCallingFactory({});
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
+ });