openai 7.15.0 → 7.16.0

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.
Files changed (150) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -0
  3. package/bedrock.d.mts.map +1 -1
  4. package/bedrock.d.ts.map +1 -1
  5. package/bedrock.js +9 -6
  6. package/bedrock.js.map +1 -1
  7. package/bedrock.mjs +9 -6
  8. package/bedrock.mjs.map +1 -1
  9. package/client.d.mts +13 -3
  10. package/client.d.mts.map +1 -1
  11. package/client.d.ts +13 -3
  12. package/client.d.ts.map +1 -1
  13. package/client.js +37 -20
  14. package/client.js.map +1 -1
  15. package/client.mjs +37 -20
  16. package/client.mjs.map +1 -1
  17. package/internal/realtime-credentials.d.mts +1 -1
  18. package/internal/realtime-credentials.d.ts +1 -1
  19. package/internal/realtime-credentials.js +1 -1
  20. package/internal/realtime-credentials.mjs +1 -1
  21. package/internal/utils/abort.d.mts +11 -0
  22. package/internal/utils/abort.d.mts.map +1 -0
  23. package/internal/utils/abort.d.ts +11 -0
  24. package/internal/utils/abort.d.ts.map +1 -0
  25. package/internal/utils/abort.js +87 -0
  26. package/internal/utils/abort.js.map +1 -0
  27. package/internal/utils/abort.mjs +83 -0
  28. package/internal/utils/abort.mjs.map +1 -0
  29. package/internal/ws.d.mts +13 -0
  30. package/internal/ws.d.mts.map +1 -1
  31. package/internal/ws.d.ts +13 -0
  32. package/internal/ws.d.ts.map +1 -1
  33. package/internal/ws.js +10 -0
  34. package/internal/ws.js.map +1 -1
  35. package/internal/ws.mjs +9 -0
  36. package/internal/ws.mjs.map +1 -1
  37. package/lib/live/transcript-grouping.d.mts +2 -2
  38. package/lib/live/transcript-grouping.d.mts.map +1 -1
  39. package/lib/live/transcript-grouping.d.ts +2 -2
  40. package/lib/live/transcript-grouping.d.ts.map +1 -1
  41. package/lib/live/transcript-grouping.js +44 -22
  42. package/lib/live/transcript-grouping.js.map +1 -1
  43. package/lib/live/transcript-grouping.mjs +44 -22
  44. package/lib/live/transcript-grouping.mjs.map +1 -1
  45. package/package.json +1 -1
  46. package/resources/beta/responses/internal-base.d.mts.map +1 -1
  47. package/resources/beta/responses/internal-base.d.ts.map +1 -1
  48. package/resources/beta/responses/internal-base.js +6 -0
  49. package/resources/beta/responses/internal-base.js.map +1 -1
  50. package/resources/beta/responses/internal-base.mjs +6 -0
  51. package/resources/beta/responses/internal-base.mjs.map +1 -1
  52. package/resources/beta/responses/ws-base.d.mts +13 -8
  53. package/resources/beta/responses/ws-base.d.mts.map +1 -1
  54. package/resources/beta/responses/ws-base.d.ts +13 -8
  55. package/resources/beta/responses/ws-base.d.ts.map +1 -1
  56. package/resources/beta/responses/ws-base.js +85 -14
  57. package/resources/beta/responses/ws-base.js.map +1 -1
  58. package/resources/beta/responses/ws-base.mjs +87 -16
  59. package/resources/beta/responses/ws-base.mjs.map +1 -1
  60. package/resources/beta/responses/ws.d.mts +1 -0
  61. package/resources/beta/responses/ws.d.mts.map +1 -1
  62. package/resources/beta/responses/ws.d.ts +1 -0
  63. package/resources/beta/responses/ws.d.ts.map +1 -1
  64. package/resources/beta/responses/ws.js.map +1 -1
  65. package/resources/beta/responses/ws.mjs.map +1 -1
  66. package/resources/live/forks/ws-base.d.mts +13 -8
  67. package/resources/live/forks/ws-base.d.mts.map +1 -1
  68. package/resources/live/forks/ws-base.d.ts +13 -8
  69. package/resources/live/forks/ws-base.d.ts.map +1 -1
  70. package/resources/live/forks/ws-base.js +85 -14
  71. package/resources/live/forks/ws-base.js.map +1 -1
  72. package/resources/live/forks/ws-base.mjs +87 -16
  73. package/resources/live/forks/ws-base.mjs.map +1 -1
  74. package/resources/live/forks/ws.d.mts +1 -0
  75. package/resources/live/forks/ws.d.mts.map +1 -1
  76. package/resources/live/forks/ws.d.ts +1 -0
  77. package/resources/live/forks/ws.d.ts.map +1 -1
  78. package/resources/live/forks/ws.js.map +1 -1
  79. package/resources/live/forks/ws.mjs.map +1 -1
  80. package/resources/live/sideband/ws-base.d.mts +13 -8
  81. package/resources/live/sideband/ws-base.d.mts.map +1 -1
  82. package/resources/live/sideband/ws-base.d.ts +13 -8
  83. package/resources/live/sideband/ws-base.d.ts.map +1 -1
  84. package/resources/live/sideband/ws-base.js +85 -14
  85. package/resources/live/sideband/ws-base.js.map +1 -1
  86. package/resources/live/sideband/ws-base.mjs +87 -16
  87. package/resources/live/sideband/ws-base.mjs.map +1 -1
  88. package/resources/live/sideband/ws.d.mts +1 -0
  89. package/resources/live/sideband/ws.d.mts.map +1 -1
  90. package/resources/live/sideband/ws.d.ts +1 -0
  91. package/resources/live/sideband/ws.d.ts.map +1 -1
  92. package/resources/live/sideband/ws.js.map +1 -1
  93. package/resources/live/sideband/ws.mjs.map +1 -1
  94. package/resources/live/ws-base.d.mts +13 -8
  95. package/resources/live/ws-base.d.mts.map +1 -1
  96. package/resources/live/ws-base.d.ts +13 -8
  97. package/resources/live/ws-base.d.ts.map +1 -1
  98. package/resources/live/ws-base.js +85 -14
  99. package/resources/live/ws-base.js.map +1 -1
  100. package/resources/live/ws-base.mjs +87 -16
  101. package/resources/live/ws-base.mjs.map +1 -1
  102. package/resources/live/ws.d.mts +1 -0
  103. package/resources/live/ws.d.mts.map +1 -1
  104. package/resources/live/ws.d.ts +1 -0
  105. package/resources/live/ws.d.ts.map +1 -1
  106. package/resources/live/ws.js.map +1 -1
  107. package/resources/live/ws.mjs.map +1 -1
  108. package/resources/responses/internal-base.d.mts.map +1 -1
  109. package/resources/responses/internal-base.d.ts.map +1 -1
  110. package/resources/responses/internal-base.js +6 -0
  111. package/resources/responses/internal-base.js.map +1 -1
  112. package/resources/responses/internal-base.mjs +6 -0
  113. package/resources/responses/internal-base.mjs.map +1 -1
  114. package/resources/responses/ws-base.d.mts +13 -8
  115. package/resources/responses/ws-base.d.mts.map +1 -1
  116. package/resources/responses/ws-base.d.ts +13 -8
  117. package/resources/responses/ws-base.d.ts.map +1 -1
  118. package/resources/responses/ws-base.js +85 -14
  119. package/resources/responses/ws-base.js.map +1 -1
  120. package/resources/responses/ws-base.mjs +87 -16
  121. package/resources/responses/ws-base.mjs.map +1 -1
  122. package/resources/responses/ws.d.mts +1 -0
  123. package/resources/responses/ws.d.mts.map +1 -1
  124. package/resources/responses/ws.d.ts +1 -0
  125. package/resources/responses/ws.d.ts.map +1 -1
  126. package/resources/responses/ws.js.map +1 -1
  127. package/resources/responses/ws.mjs.map +1 -1
  128. package/src/bedrock.ts +18 -7
  129. package/src/client.ts +43 -19
  130. package/src/internal/realtime-credentials.ts +1 -1
  131. package/src/internal/utils/abort.ts +111 -0
  132. package/src/internal/ws.ts +22 -0
  133. package/src/lib/live/transcript-grouping.ts +59 -27
  134. package/src/resources/beta/responses/internal-base.ts +5 -0
  135. package/src/resources/beta/responses/ws-base.ts +110 -21
  136. package/src/resources/beta/responses/ws.ts +2 -0
  137. package/src/resources/live/forks/ws-base.ts +110 -21
  138. package/src/resources/live/forks/ws.ts +2 -0
  139. package/src/resources/live/sideband/ws-base.ts +110 -21
  140. package/src/resources/live/sideband/ws.ts +2 -0
  141. package/src/resources/live/ws-base.ts +110 -21
  142. package/src/resources/live/ws.ts +2 -0
  143. package/src/resources/responses/internal-base.ts +5 -0
  144. package/src/resources/responses/ws-base.ts +110 -21
  145. package/src/resources/responses/ws.ts +2 -0
  146. package/src/version.ts +1 -1
  147. package/version.d.mts +1 -1
  148. package/version.d.ts +1 -1
  149. package/version.js +1 -1
  150. package/version.mjs +1 -1
@@ -6,6 +6,8 @@ import { sleep } from '../../../internal/utils/sleep';
6
6
  import { type WebSocketLike, ReadyState } from '../../../internal/ws-adapter';
7
7
  import {
8
8
  SendQueue,
9
+ getMaxBufferedEvents,
10
+ type WebSocketStreamOptions,
9
11
  flattenRawData,
10
12
  isRecoverableClose,
11
13
  type RawWebSocketData,
@@ -51,11 +53,11 @@ export interface ResponsesWSBaseOptions {
51
53
  reconnect?: ResponsesWSReconnectOptions | null | undefined;
52
54
 
53
55
  /**
54
- * Maximum size of the outgoing message queue in bytes.
55
- * Messages queued while the socket is connecting or reconnecting are held
56
- * in memory up to this limit. Once the limit is reached, new messages are
57
- * discarded and an `error` event is emitted.
58
- * Default: 1 MB
56
+ * Byte budget for outgoing messages queued while the socket is connecting
57
+ * or reconnecting. An empty queue accepts one message even if it exceeds
58
+ * this budget. Further messages are discarded and an `error` event is emitted
59
+ * if the total queued size would exceed the budget.
60
+ * Default: 1 MiB (1,048,576 bytes).
59
61
  */
60
62
  maxQueueSize?: number | undefined;
61
63
  }
@@ -169,6 +171,11 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
169
171
  * The iterator will exit if the socket closes but exiting the iterator
170
172
  * does not close the socket.
171
173
  *
174
+ * Pass `maxBufferedEvents` to limit queued records for this iterator, including
175
+ * lifecycle events. Overflow discards its backlog and rejects `next()` with a
176
+ * WebSocketError; the shared socket and other iterators remain active.
177
+ * Omitted means unlimited. This is an event-count limit, not a byte limit.
178
+ *
172
179
  * @example
173
180
  * ```ts
174
181
  * for await (const event of client.stream()) {
@@ -186,26 +193,59 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
186
193
  * }
187
194
  * ```
188
195
  */
189
- stream(): AsyncIterableIterator<ResponsesStreamMessage> {
190
- return this[Symbol.asyncIterator]();
196
+ stream(options?: WebSocketStreamOptions): AsyncIterableIterator<ResponsesStreamMessage> {
197
+ return options === undefined ? this[Symbol.asyncIterator]() : this[Symbol.asyncIterator](options);
191
198
  }
192
199
 
193
- [Symbol.asyncIterator](): AsyncIterableIterator<ResponsesStreamMessage> {
200
+ [Symbol.asyncIterator](options?: WebSocketStreamOptions): AsyncIterableIterator<ResponsesStreamMessage> {
194
201
  if (!this.socket) {
195
202
  throw new OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
196
203
  }
197
204
 
205
+ const maxBufferedEvents = getMaxBufferedEvents(options);
206
+
198
207
  // Two-queue async iterator: `queue` buffers incoming messages,
199
208
  // `resolvers` buffers waiting next() calls. A push wakes the
200
209
  // oldest next(); a next() drains the oldest message.
201
- const queue: ResponsesStreamMessage[] = [];
202
- const resolvers: (() => void)[] = [];
210
+ const queue: (ResponsesStreamMessage | undefined)[] = [];
211
+ const resolvers: ((() => void) | undefined)[] = [];
212
+ let queueHead = 0;
213
+ let resolverHead = 0;
203
214
  let done = false;
215
+ let failure: WebSocketError | undefined;
204
216
  let currentSocket = this.socket;
205
217
 
218
+ const wakeResolver = () => {
219
+ if (resolverHead >= resolvers.length) return;
220
+
221
+ const resolver = resolvers[resolverHead];
222
+ resolvers[resolverHead] = undefined;
223
+ resolverHead += 1;
224
+ if (resolverHead >= 64 && resolverHead * 2 >= resolvers.length) {
225
+ resolvers.splice(0, resolverHead);
226
+ resolverHead = 0;
227
+ }
228
+ resolver?.();
229
+ };
230
+
206
231
  const push = (msg: ResponsesStreamMessage) => {
232
+ if (done) return;
233
+
234
+ if (maxBufferedEvents !== undefined && queue.length - queueHead >= maxBufferedEvents) {
235
+ failure = new WebSocketError(
236
+ `WebSocket stream exceeded maxBufferedEvents (${maxBufferedEvents})`,
237
+ null,
238
+ );
239
+ done = true;
240
+ queue.length = 0;
241
+ queueHead = 0;
242
+ cleanup();
243
+ flushResolvers();
244
+ return;
245
+ }
246
+
207
247
  queue.push(msg);
208
- resolvers.shift()?.();
248
+ wakeResolver();
209
249
  };
210
250
 
211
251
  const onEvent = (event: ResponsesAPI.BetaResponsesServerEvent) => {
@@ -235,9 +275,14 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
235
275
  };
236
276
 
237
277
  const flushResolvers = () => {
238
- for (let resolver = resolvers.shift(); resolver; resolver = resolvers.shift()) {
239
- resolver();
278
+ while (resolverHead < resolvers.length) {
279
+ const resolver = resolvers[resolverHead];
280
+ resolvers[resolverHead] = undefined;
281
+ resolverHead += 1;
282
+ resolver?.();
240
283
  }
284
+ resolvers.length = 0;
285
+ resolverHead = 0;
241
286
  };
242
287
 
243
288
  const onClose = (
@@ -251,8 +296,9 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
251
296
  cleanup();
252
297
  };
253
298
 
254
- const onSocketSwap = (oldSocket: TSocket, newSocket: TSocket) => {
255
- oldSocket.off('open', onOpen);
299
+ const onSocketSwap = (_oldSocket: TSocket, newSocket: TSocket) => {
300
+ if (currentSocket === newSocket) return;
301
+ currentSocket.off('open', onOpen);
256
302
  newSocket.on('open', onOpen);
257
303
  currentSocket = newSocket;
258
304
  };
@@ -309,9 +355,21 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
309
355
  }
310
356
  }
311
357
 
312
- const resolve = (res: (value: IteratorResult<ResponsesStreamMessage>) => void) => {
313
- if (queue.length > 0) {
314
- res({ value: queue.shift()!, done: false });
358
+ const resolve = (
359
+ res: (value: IteratorResult<ResponsesStreamMessage>) => void,
360
+ reject: (error: WebSocketError) => void,
361
+ ) => {
362
+ if (failure) {
363
+ reject(failure);
364
+ } else if (queueHead < queue.length) {
365
+ const queued = queue[queueHead]!;
366
+ queue[queueHead] = undefined;
367
+ queueHead += 1;
368
+ if (queueHead >= 64 && queueHead * 2 >= queue.length) {
369
+ queue.splice(0, queueHead);
370
+ queueHead = 0;
371
+ }
372
+ res({ value: queued, done: false });
315
373
  } else if (done) {
316
374
  res({ value: undefined, done: true });
317
375
  } else {
@@ -321,10 +379,10 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
321
379
  };
322
380
 
323
381
  const next = (): Promise<IteratorResult<ResponsesStreamMessage>> =>
324
- new Promise((res) => {
325
- if (resolve(res)) return;
382
+ new Promise((res, reject) => {
383
+ if (resolve(res, reject)) return;
326
384
  resolvers.push(() => {
327
- resolve(res);
385
+ resolve(res, reject);
328
386
  });
329
387
  });
330
388
 
@@ -332,6 +390,8 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
332
390
  next,
333
391
  return: (): Promise<IteratorReturnResult<undefined>> => {
334
392
  done = true;
393
+ queue.length = 0;
394
+ queueHead = 0;
335
395
  cleanup();
336
396
  flushResolvers();
337
397
  return Promise.resolve({ value: undefined, done: true });
@@ -364,6 +424,35 @@ export abstract class ResponsesWSBase<TSocket extends WebSocketLike> extends Res
364
424
  return;
365
425
  }
366
426
 
427
+ if (
428
+ event === null ||
429
+ typeof event !== 'object' ||
430
+ Array.isArray(event) ||
431
+ !Object.prototype.hasOwnProperty.call(event, 'type') ||
432
+ typeof event.type !== 'string'
433
+ ) {
434
+ this._onError(
435
+ null,
436
+ 'received invalid WebSocket event: expected an object with an own string type',
437
+ undefined,
438
+ );
439
+ return;
440
+ }
441
+
442
+ const eventType: string = event.type;
443
+ const reservedEventType =
444
+ eventType === 'raw' ||
445
+ eventType === 'close' ||
446
+ eventType === 'event' ||
447
+ eventType === 'reconnecting' ||
448
+ eventType === 'reconnected' ||
449
+ eventType === 'open' ||
450
+ eventType === 'error';
451
+ if (reservedEventType && eventType !== 'error') {
452
+ this._onError(null, 'received reserved WebSocket event type', undefined);
453
+ return;
454
+ }
455
+
367
456
  this._emit('event', event);
368
457
 
369
458
  if (event.type === 'error') {
@@ -6,6 +6,8 @@ import { ResponsesWSBase, type ResponsesWSBaseOptions } from './ws-base';
6
6
  import { OpenAI } from '../../../client';
7
7
  import { VERSION } from '../../../version';
8
8
 
9
+ export type { WebSocketStreamOptions } from '../../../internal/ws';
10
+
9
11
  export type { ResponsesWSReconnectOptions } from './ws-base';
10
12
 
11
13
  export interface ResponsesWSClientOptions extends WS.ClientOptions, ResponsesWSBaseOptions {}
@@ -6,6 +6,8 @@ import { sleep } from '../../../internal/utils/sleep';
6
6
  import { type WebSocketLike, ReadyState } from '../../../internal/ws-adapter';
7
7
  import {
8
8
  SendQueue,
9
+ getMaxBufferedEvents,
10
+ type WebSocketStreamOptions,
9
11
  flattenRawData,
10
12
  isRecoverableClose,
11
13
  type RawWebSocketData,
@@ -55,11 +57,11 @@ export interface ForksWSBaseOptions {
55
57
  reconnect?: ForksWSReconnectOptions | null | undefined;
56
58
 
57
59
  /**
58
- * Maximum size of the outgoing message queue in bytes.
59
- * Messages queued while the socket is connecting or reconnecting are held
60
- * in memory up to this limit. Once the limit is reached, new messages are
61
- * discarded and an `error` event is emitted.
62
- * Default: 1 MB
60
+ * Byte budget for outgoing messages queued while the socket is connecting
61
+ * or reconnecting. An empty queue accepts one message even if it exceeds
62
+ * this budget. Further messages are discarded and an `error` event is emitted
63
+ * if the total queued size would exceed the budget.
64
+ * Default: 1 MiB (1,048,576 bytes).
63
65
  */
64
66
  maxQueueSize?: number | undefined;
65
67
  }
@@ -169,6 +171,11 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
169
171
  * The iterator will exit if the socket closes but exiting the iterator
170
172
  * does not close the socket.
171
173
  *
174
+ * Pass `maxBufferedEvents` to limit queued records for this iterator, including
175
+ * lifecycle events. Overflow discards its backlog and rejects `next()` with a
176
+ * WebSocketError; the shared socket and other iterators remain active.
177
+ * Omitted means unlimited. This is an event-count limit, not a byte limit.
178
+ *
172
179
  * @example
173
180
  * ```ts
174
181
  * for await (const event of client.stream()) {
@@ -186,26 +193,59 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
186
193
  * }
187
194
  * ```
188
195
  */
189
- stream(): AsyncIterableIterator<ForksStreamMessage> {
190
- return this[Symbol.asyncIterator]();
196
+ stream(options?: WebSocketStreamOptions): AsyncIterableIterator<ForksStreamMessage> {
197
+ return options === undefined ? this[Symbol.asyncIterator]() : this[Symbol.asyncIterator](options);
191
198
  }
192
199
 
193
- [Symbol.asyncIterator](): AsyncIterableIterator<ForksStreamMessage> {
200
+ [Symbol.asyncIterator](options?: WebSocketStreamOptions): AsyncIterableIterator<ForksStreamMessage> {
194
201
  if (!this.socket) {
195
202
  throw new OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
196
203
  }
197
204
 
205
+ const maxBufferedEvents = getMaxBufferedEvents(options);
206
+
198
207
  // Two-queue async iterator: `queue` buffers incoming messages,
199
208
  // `resolvers` buffers waiting next() calls. A push wakes the
200
209
  // oldest next(); a next() drains the oldest message.
201
- const queue: ForksStreamMessage[] = [];
202
- const resolvers: (() => void)[] = [];
210
+ const queue: (ForksStreamMessage | undefined)[] = [];
211
+ const resolvers: ((() => void) | undefined)[] = [];
212
+ let queueHead = 0;
213
+ let resolverHead = 0;
203
214
  let done = false;
215
+ let failure: WebSocketError | undefined;
204
216
  let currentSocket = this.socket;
205
217
 
218
+ const wakeResolver = () => {
219
+ if (resolverHead >= resolvers.length) return;
220
+
221
+ const resolver = resolvers[resolverHead];
222
+ resolvers[resolverHead] = undefined;
223
+ resolverHead += 1;
224
+ if (resolverHead >= 64 && resolverHead * 2 >= resolvers.length) {
225
+ resolvers.splice(0, resolverHead);
226
+ resolverHead = 0;
227
+ }
228
+ resolver?.();
229
+ };
230
+
206
231
  const push = (msg: ForksStreamMessage) => {
232
+ if (done) return;
233
+
234
+ if (maxBufferedEvents !== undefined && queue.length - queueHead >= maxBufferedEvents) {
235
+ failure = new WebSocketError(
236
+ `WebSocket stream exceeded maxBufferedEvents (${maxBufferedEvents})`,
237
+ null,
238
+ );
239
+ done = true;
240
+ queue.length = 0;
241
+ queueHead = 0;
242
+ cleanup();
243
+ flushResolvers();
244
+ return;
245
+ }
246
+
207
247
  queue.push(msg);
208
- resolvers.shift()?.();
248
+ wakeResolver();
209
249
  };
210
250
 
211
251
  const onEvent = (event: ForksAPI.ForkServerEvent) => {
@@ -235,9 +275,14 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
235
275
  };
236
276
 
237
277
  const flushResolvers = () => {
238
- for (let resolver = resolvers.shift(); resolver; resolver = resolvers.shift()) {
239
- resolver();
278
+ while (resolverHead < resolvers.length) {
279
+ const resolver = resolvers[resolverHead];
280
+ resolvers[resolverHead] = undefined;
281
+ resolverHead += 1;
282
+ resolver?.();
240
283
  }
284
+ resolvers.length = 0;
285
+ resolverHead = 0;
241
286
  };
242
287
 
243
288
  const onClose = (code: number, reason: string, unsent: UnsentMessage<ForksAPI.ForkClientEvent>[]) => {
@@ -247,8 +292,9 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
247
292
  cleanup();
248
293
  };
249
294
 
250
- const onSocketSwap = (oldSocket: TSocket, newSocket: TSocket) => {
251
- oldSocket.off('open', onOpen);
295
+ const onSocketSwap = (_oldSocket: TSocket, newSocket: TSocket) => {
296
+ if (currentSocket === newSocket) return;
297
+ currentSocket.off('open', onOpen);
252
298
  newSocket.on('open', onOpen);
253
299
  currentSocket = newSocket;
254
300
  };
@@ -305,9 +351,21 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
305
351
  }
306
352
  }
307
353
 
308
- const resolve = (res: (value: IteratorResult<ForksStreamMessage>) => void) => {
309
- if (queue.length > 0) {
310
- res({ value: queue.shift()!, done: false });
354
+ const resolve = (
355
+ res: (value: IteratorResult<ForksStreamMessage>) => void,
356
+ reject: (error: WebSocketError) => void,
357
+ ) => {
358
+ if (failure) {
359
+ reject(failure);
360
+ } else if (queueHead < queue.length) {
361
+ const queued = queue[queueHead]!;
362
+ queue[queueHead] = undefined;
363
+ queueHead += 1;
364
+ if (queueHead >= 64 && queueHead * 2 >= queue.length) {
365
+ queue.splice(0, queueHead);
366
+ queueHead = 0;
367
+ }
368
+ res({ value: queued, done: false });
311
369
  } else if (done) {
312
370
  res({ value: undefined, done: true });
313
371
  } else {
@@ -317,10 +375,10 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
317
375
  };
318
376
 
319
377
  const next = (): Promise<IteratorResult<ForksStreamMessage>> =>
320
- new Promise((res) => {
321
- if (resolve(res)) return;
378
+ new Promise((res, reject) => {
379
+ if (resolve(res, reject)) return;
322
380
  resolvers.push(() => {
323
- resolve(res);
381
+ resolve(res, reject);
324
382
  });
325
383
  });
326
384
 
@@ -328,6 +386,8 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
328
386
  next,
329
387
  return: (): Promise<IteratorReturnResult<undefined>> => {
330
388
  done = true;
389
+ queue.length = 0;
390
+ queueHead = 0;
331
391
  cleanup();
332
392
  flushResolvers();
333
393
  return Promise.resolve({ value: undefined, done: true });
@@ -360,6 +420,35 @@ export abstract class ForksWSBase<TSocket extends WebSocketLike> extends ForksEm
360
420
  return;
361
421
  }
362
422
 
423
+ if (
424
+ event === null ||
425
+ typeof event !== 'object' ||
426
+ Array.isArray(event) ||
427
+ !Object.prototype.hasOwnProperty.call(event, 'type') ||
428
+ typeof event.type !== 'string'
429
+ ) {
430
+ this._onError(
431
+ null,
432
+ 'received invalid WebSocket event: expected an object with an own string type',
433
+ undefined,
434
+ );
435
+ return;
436
+ }
437
+
438
+ const eventType: string = event.type;
439
+ const reservedEventType =
440
+ eventType === 'raw' ||
441
+ eventType === 'close' ||
442
+ eventType === 'event' ||
443
+ eventType === 'reconnecting' ||
444
+ eventType === 'reconnected' ||
445
+ eventType === 'open' ||
446
+ eventType === 'error';
447
+ if (reservedEventType && eventType !== 'error') {
448
+ this._onError(null, 'received reserved WebSocket event type', undefined);
449
+ return;
450
+ }
451
+
363
452
  this._emit('event', event);
364
453
 
365
454
  if (event.type === 'error') {
@@ -6,6 +6,8 @@ import { ForksWSBase, type ForksWSBaseOptions, type ForksWSParameters } from './
6
6
  import { OpenAI } from '../../../client';
7
7
  import { VERSION } from '../../../version';
8
8
 
9
+ export type { WebSocketStreamOptions } from '../../../internal/ws';
10
+
9
11
  export type { ForksWSParameters, ForksWSReconnectOptions } from './ws-base';
10
12
 
11
13
  export interface ForksWSClientOptions extends WS.ClientOptions, ForksWSBaseOptions {}
@@ -6,6 +6,8 @@ import { sleep } from '../../../internal/utils/sleep';
6
6
  import { type WebSocketLike, ReadyState } from '../../../internal/ws-adapter';
7
7
  import {
8
8
  SendQueue,
9
+ getMaxBufferedEvents,
10
+ type WebSocketStreamOptions,
9
11
  flattenRawData,
10
12
  isRecoverableClose,
11
13
  type RawWebSocketData,
@@ -61,11 +63,11 @@ export interface SidebandWSBaseOptions {
61
63
  reconnect?: SidebandWSReconnectOptions | null | undefined;
62
64
 
63
65
  /**
64
- * Maximum size of the outgoing message queue in bytes.
65
- * Messages queued while the socket is connecting or reconnecting are held
66
- * in memory up to this limit. Once the limit is reached, new messages are
67
- * discarded and an `error` event is emitted.
68
- * Default: 1 MB
66
+ * Byte budget for outgoing messages queued while the socket is connecting
67
+ * or reconnecting. An empty queue accepts one message even if it exceeds
68
+ * this budget. Further messages are discarded and an `error` event is emitted
69
+ * if the total queued size would exceed the budget.
70
+ * Default: 1 MiB (1,048,576 bytes).
69
71
  */
70
72
  maxQueueSize?: number | undefined;
71
73
  }
@@ -175,6 +177,11 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
175
177
  * The iterator will exit if the socket closes but exiting the iterator
176
178
  * does not close the socket.
177
179
  *
180
+ * Pass `maxBufferedEvents` to limit queued records for this iterator, including
181
+ * lifecycle events. Overflow discards its backlog and rejects `next()` with a
182
+ * WebSocketError; the shared socket and other iterators remain active.
183
+ * Omitted means unlimited. This is an event-count limit, not a byte limit.
184
+ *
178
185
  * @example
179
186
  * ```ts
180
187
  * for await (const event of client.stream()) {
@@ -192,26 +199,59 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
192
199
  * }
193
200
  * ```
194
201
  */
195
- stream(): AsyncIterableIterator<SidebandStreamMessage> {
196
- return this[Symbol.asyncIterator]();
202
+ stream(options?: WebSocketStreamOptions): AsyncIterableIterator<SidebandStreamMessage> {
203
+ return options === undefined ? this[Symbol.asyncIterator]() : this[Symbol.asyncIterator](options);
197
204
  }
198
205
 
199
- [Symbol.asyncIterator](): AsyncIterableIterator<SidebandStreamMessage> {
206
+ [Symbol.asyncIterator](options?: WebSocketStreamOptions): AsyncIterableIterator<SidebandStreamMessage> {
200
207
  if (!this.socket) {
201
208
  throw new OpenAIError('Internal error: failed to initialize socket. Please report this issue.');
202
209
  }
203
210
 
211
+ const maxBufferedEvents = getMaxBufferedEvents(options);
212
+
204
213
  // Two-queue async iterator: `queue` buffers incoming messages,
205
214
  // `resolvers` buffers waiting next() calls. A push wakes the
206
215
  // oldest next(); a next() drains the oldest message.
207
- const queue: SidebandStreamMessage[] = [];
208
- const resolvers: (() => void)[] = [];
216
+ const queue: (SidebandStreamMessage | undefined)[] = [];
217
+ const resolvers: ((() => void) | undefined)[] = [];
218
+ let queueHead = 0;
219
+ let resolverHead = 0;
209
220
  let done = false;
221
+ let failure: WebSocketError | undefined;
210
222
  let currentSocket = this.socket;
211
223
 
224
+ const wakeResolver = () => {
225
+ if (resolverHead >= resolvers.length) return;
226
+
227
+ const resolver = resolvers[resolverHead];
228
+ resolvers[resolverHead] = undefined;
229
+ resolverHead += 1;
230
+ if (resolverHead >= 64 && resolverHead * 2 >= resolvers.length) {
231
+ resolvers.splice(0, resolverHead);
232
+ resolverHead = 0;
233
+ }
234
+ resolver?.();
235
+ };
236
+
212
237
  const push = (msg: SidebandStreamMessage) => {
238
+ if (done) return;
239
+
240
+ if (maxBufferedEvents !== undefined && queue.length - queueHead >= maxBufferedEvents) {
241
+ failure = new WebSocketError(
242
+ `WebSocket stream exceeded maxBufferedEvents (${maxBufferedEvents})`,
243
+ null,
244
+ );
245
+ done = true;
246
+ queue.length = 0;
247
+ queueHead = 0;
248
+ cleanup();
249
+ flushResolvers();
250
+ return;
251
+ }
252
+
213
253
  queue.push(msg);
214
- resolvers.shift()?.();
254
+ wakeResolver();
215
255
  };
216
256
 
217
257
  const onEvent = (event: SidebandAPI.ConnectServerEvent) => {
@@ -241,9 +281,14 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
241
281
  };
242
282
 
243
283
  const flushResolvers = () => {
244
- for (let resolver = resolvers.shift(); resolver; resolver = resolvers.shift()) {
245
- resolver();
284
+ while (resolverHead < resolvers.length) {
285
+ const resolver = resolvers[resolverHead];
286
+ resolvers[resolverHead] = undefined;
287
+ resolverHead += 1;
288
+ resolver?.();
246
289
  }
290
+ resolvers.length = 0;
291
+ resolverHead = 0;
247
292
  };
248
293
 
249
294
  const onClose = (
@@ -257,8 +302,9 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
257
302
  cleanup();
258
303
  };
259
304
 
260
- const onSocketSwap = (oldSocket: TSocket, newSocket: TSocket) => {
261
- oldSocket.off('open', onOpen);
305
+ const onSocketSwap = (_oldSocket: TSocket, newSocket: TSocket) => {
306
+ if (currentSocket === newSocket) return;
307
+ currentSocket.off('open', onOpen);
262
308
  newSocket.on('open', onOpen);
263
309
  currentSocket = newSocket;
264
310
  };
@@ -315,9 +361,21 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
315
361
  }
316
362
  }
317
363
 
318
- const resolve = (res: (value: IteratorResult<SidebandStreamMessage>) => void) => {
319
- if (queue.length > 0) {
320
- res({ value: queue.shift()!, done: false });
364
+ const resolve = (
365
+ res: (value: IteratorResult<SidebandStreamMessage>) => void,
366
+ reject: (error: WebSocketError) => void,
367
+ ) => {
368
+ if (failure) {
369
+ reject(failure);
370
+ } else if (queueHead < queue.length) {
371
+ const queued = queue[queueHead]!;
372
+ queue[queueHead] = undefined;
373
+ queueHead += 1;
374
+ if (queueHead >= 64 && queueHead * 2 >= queue.length) {
375
+ queue.splice(0, queueHead);
376
+ queueHead = 0;
377
+ }
378
+ res({ value: queued, done: false });
321
379
  } else if (done) {
322
380
  res({ value: undefined, done: true });
323
381
  } else {
@@ -327,10 +385,10 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
327
385
  };
328
386
 
329
387
  const next = (): Promise<IteratorResult<SidebandStreamMessage>> =>
330
- new Promise((res) => {
331
- if (resolve(res)) return;
388
+ new Promise((res, reject) => {
389
+ if (resolve(res, reject)) return;
332
390
  resolvers.push(() => {
333
- resolve(res);
391
+ resolve(res, reject);
334
392
  });
335
393
  });
336
394
 
@@ -338,6 +396,8 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
338
396
  next,
339
397
  return: (): Promise<IteratorReturnResult<undefined>> => {
340
398
  done = true;
399
+ queue.length = 0;
400
+ queueHead = 0;
341
401
  cleanup();
342
402
  flushResolvers();
343
403
  return Promise.resolve({ value: undefined, done: true });
@@ -370,6 +430,35 @@ export abstract class SidebandWSBase<TSocket extends WebSocketLike> extends Side
370
430
  return;
371
431
  }
372
432
 
433
+ if (
434
+ event === null ||
435
+ typeof event !== 'object' ||
436
+ Array.isArray(event) ||
437
+ !Object.prototype.hasOwnProperty.call(event, 'type') ||
438
+ typeof event.type !== 'string'
439
+ ) {
440
+ this._onError(
441
+ null,
442
+ 'received invalid WebSocket event: expected an object with an own string type',
443
+ undefined,
444
+ );
445
+ return;
446
+ }
447
+
448
+ const eventType: string = event.type;
449
+ const reservedEventType =
450
+ eventType === 'raw' ||
451
+ eventType === 'close' ||
452
+ eventType === 'event' ||
453
+ eventType === 'reconnecting' ||
454
+ eventType === 'reconnected' ||
455
+ eventType === 'open' ||
456
+ eventType === 'error';
457
+ if (reservedEventType && eventType !== 'error') {
458
+ this._onError(null, 'received reserved WebSocket event type', undefined);
459
+ return;
460
+ }
461
+
373
462
  this._emit('event', event);
374
463
 
375
464
  if (event.type === 'error') {
@@ -6,6 +6,8 @@ import { SidebandWSBase, type SidebandWSBaseOptions, type SidebandWSParameters }
6
6
  import { OpenAI } from '../../../client';
7
7
  import { VERSION } from '../../../version';
8
8
 
9
+ export type { WebSocketStreamOptions } from '../../../internal/ws';
10
+
9
11
  export type { SidebandWSParameters, SidebandWSReconnectOptions } from './ws-base';
10
12
 
11
13
  export interface SidebandWSClientOptions extends WS.ClientOptions, SidebandWSBaseOptions {}