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
package/src/client.ts CHANGED
@@ -5,6 +5,7 @@ import type { HTTPMethod, PromiseOrValue, MergedRequestInit, FinalizedRequestIni
5
5
  import { uuid4 } from './internal/utils/uuid';
6
6
  import { validatePositiveInteger, isAbsoluteURL, safeJSON, hasOwn } from './internal/utils/values';
7
7
  import { sleep } from './internal/utils/sleep';
8
+ import { addRequestAbortListener, retainRequestAbortCallback } from './internal/utils/abort';
8
9
  export type { Logger, LogLevel } from './internal/utils/log';
9
10
  import { castToError, isAbortError } from './internal/errors';
10
11
  import { addRequestID, defaultParseResponse, type APIResponseProps } from './internal/parse';
@@ -47,6 +48,7 @@ import {
47
48
  import * as Uploads from './core/uploads';
48
49
  import * as API from './resources/index';
49
50
  import { APIPromise } from './core/api-promise';
51
+ import { resolveRealtimeAPIKey } from './internal/realtime-credentials';
50
52
  import {
51
53
  Batch,
52
54
  BatchCreateParams,
@@ -300,8 +302,9 @@ export interface ClientOptions {
300
302
  *
301
303
  * - Accepts either a static string or an async function that resolves to a string.
302
304
  * - Defaults to process.env['OPENAI_API_KEY'].
303
- * - When a function is provided, it is invoked before each request so you can rotate
304
- * or refresh credentials at runtime.
305
+ * - When a function is provided, it is invoked when building bearer authentication
306
+ * headers for each attempt, including retries and direct `buildRequest()` calls.
307
+ * Each invocation's result is used for its own request.
305
308
  * - The function must return a non-empty string; otherwise an OpenAIError is thrown.
306
309
  * - If the function throws, the error is wrapped in an OpenAIError with the original
307
310
  * error available as `cause`.
@@ -816,10 +819,11 @@ export class OpenAI {
816
819
  : await authentication.getToken();
817
820
  return buildHeaders([{ Authorization: `Bearer ${token}` }]);
818
821
  }
819
- if (this.apiKey == null) {
822
+ const { apiKey } = await resolveRealtimeAPIKey(this);
823
+ if (apiKey == null) {
820
824
  return undefined;
821
825
  }
822
- return buildHeaders([{ Authorization: `Bearer ${this.apiKey}` }]);
826
+ return buildHeaders([{ Authorization: `Bearer ${apiKey}` }]);
823
827
  }
824
828
 
825
829
  protected async adminAPIKeyAuth(opts: FinalRequestOptions): Promise<NullableHeaders | undefined> {
@@ -857,7 +861,7 @@ export class OpenAI {
857
861
  * Returns whether a provider was invoked. Internal callers can capture this
858
862
  * invocation's key before another request updates the shared `apiKey` property.
859
863
  * Overrides should forward `capture` or invoke it with their own resolved key
860
- * to preserve connection-local credentials in concurrent Realtime factories.
864
+ * to preserve invocation-local credentials in concurrent requests and Realtime factories.
861
865
  * @internal
862
866
  */
863
867
  async _callApiKey(capture?: (apiKey: string | null) => void): Promise<boolean> {
@@ -919,15 +923,11 @@ export class OpenAI {
919
923
 
920
924
  /**
921
925
  * Used as a callback for mutating the given `FinalRequestOptions` object.
926
+ * Function-based credentials are resolved later, when building authentication
927
+ * headers, including for direct `buildRequest()` calls. Overriding this hook
928
+ * does not bypass that resolution.
922
929
  */
923
- protected async prepareOptions(options: FinalRequestOptions): Promise<void> {
924
- if (this._provider) return;
925
-
926
- const security = options.__security ?? { bearerAuth: true };
927
- if (security.bearerAuth) {
928
- await this._callApiKey();
929
- }
930
- }
930
+ protected async prepareOptions(options: FinalRequestOptions): Promise<void> {}
931
931
 
932
932
  /**
933
933
  * Used as a callback for mutating the given `RequestInit` object.
@@ -1574,7 +1574,8 @@ export class OpenAI {
1574
1574
  const { signal, method, ...options } = init || {};
1575
1575
  const abort = this._makeAbort(controller);
1576
1576
  const composed = !!signal && composedCallerSignals.get(controller) === signal;
1577
- if (signal && !composed) signal.addEventListener('abort', abort, { once: true });
1577
+ const cleanup =
1578
+ signal && !composed ? addRequestAbortListener(signal, abort, controller.signal) : undefined;
1578
1579
 
1579
1580
  const timeout = setTimeout(abort, ms);
1580
1581
 
@@ -1596,9 +1597,13 @@ export class OpenAI {
1596
1597
 
1597
1598
  try {
1598
1599
  // use undefined this binding; fetch errors if bound to something else in browser/cloudflare
1599
- return await (this.#x509Fetch ?? this.fetch).call(undefined, url, fetchOptions);
1600
+ const response = await (this.#x509Fetch ?? this.fetch).call(undefined, url, fetchOptions);
1601
+ if (cleanup) {
1602
+ retainRequestAbortCallback(response.body ?? response, abort, controller.signal);
1603
+ }
1604
+ return response;
1600
1605
  } catch (err) {
1601
- if (signal && !composed) signal.removeEventListener('abort', abort);
1606
+ cleanup?.();
1602
1607
  throw err;
1603
1608
  } finally {
1604
1609
  clearTimeout(timeout);
@@ -1701,6 +1706,12 @@ export class OpenAI {
1701
1706
  return sleepSeconds * jitter * 1000;
1702
1707
  }
1703
1708
 
1709
+ /**
1710
+ * Builds a request, resolving callback credentials when constructing authentication
1711
+ * headers, after any subclass request-option rewrites. Calling this method directly
1712
+ * also resolves credentials. Complete replacement builders own authentication and
1713
+ * can call `this.authHeaders()` to resolve headers with request-local credentials.
1714
+ */
1704
1715
  async buildRequest(
1705
1716
  inputOptions: FinalRequestOptions,
1706
1717
  { retryCount = 0 }: { retryCount?: number } = {},
@@ -1745,6 +1756,10 @@ export class OpenAI {
1745
1756
  options.signal = snapshot.signal;
1746
1757
  }
1747
1758
  }
1759
+ const authenticationHeaders =
1760
+ this._provider || x509Authentication
1761
+ ? undefined
1762
+ : await this.authHeaders(inputOptions, inputOptions.__security ?? { bearerAuth: true });
1748
1763
  const { bodyHeaders, body, isStreamingBody } = this.buildBody({ options });
1749
1764
 
1750
1765
  if (isStreamingBody) {
@@ -1759,6 +1774,7 @@ export class OpenAI {
1759
1774
  options: inputOptions,
1760
1775
  method,
1761
1776
  bodyHeaders,
1777
+ authenticationHeaders,
1762
1778
  retryCount,
1763
1779
  x509Headers,
1764
1780
  x509Timeout: explicitTimeout ? options.timeout : undefined,
@@ -1783,6 +1799,7 @@ export class OpenAI {
1783
1799
  options,
1784
1800
  method,
1785
1801
  bodyHeaders,
1802
+ authenticationHeaders,
1786
1803
  retryCount,
1787
1804
  x509Headers,
1788
1805
  x509Timeout,
@@ -1791,6 +1808,7 @@ export class OpenAI {
1791
1808
  options: FinalRequestOptions;
1792
1809
  method: HTTPMethod;
1793
1810
  bodyHeaders: HeadersLike;
1811
+ authenticationHeaders: NullableHeaders | undefined;
1794
1812
  retryCount: number;
1795
1813
  x509Headers?: { defaultHeaders: NullableHeaders; requestHeaders: NullableHeaders } | undefined;
1796
1814
  x509Timeout: number | undefined;
@@ -1816,9 +1834,10 @@ export class OpenAI {
1816
1834
  'OpenAI-Organization': x509Tenant ? x509Tenant.organization : this.organization,
1817
1835
  'OpenAI-Project': x509Tenant ? x509Tenant.project : this.project,
1818
1836
  },
1819
- this._provider || this.#x509Authentication?.isPlanningRequest()
1820
- ? undefined
1821
- : await this.authHeaders(options, options.__security ?? { bearerAuth: true }),
1837
+ // X.509 owns streaming uploads before authentication so it can retire them on failure.
1838
+ this.#x509Authentication && !this.#x509Authentication.isPlanningRequest()
1839
+ ? await this.authHeaders(options, options.__security ?? { bearerAuth: true })
1840
+ : authenticationHeaders,
1822
1841
  x509Headers?.defaultHeaders ?? this._options.defaultHeaders,
1823
1842
  bodyHeaders,
1824
1843
  x509Headers?.requestHeaders ?? options.headers,
@@ -2024,6 +2043,11 @@ OpenAI.Evals = Evals;
2024
2043
  OpenAI.Containers = Containers;
2025
2044
  OpenAI.Skills = Skills;
2026
2045
  OpenAI.Videos = Videos;
2046
+ OpenAI.ConversationCursorPage = Pagination.ConversationCursorPage;
2047
+ OpenAI.CursorPage = Pagination.CursorPage;
2048
+ OpenAI.NextCursorPage = Pagination.NextCursorPage;
2049
+ OpenAI.Page = Pagination.Page;
2050
+ OpenAI.TokenPage = Pagination.TokenPage;
2027
2051
 
2028
2052
  const composedCallerSignals = new WeakMap<AbortController, AbortSignal>();
2029
2053
 
@@ -9,7 +9,7 @@ export function getRealtimeAPIKey(
9
9
  }
10
10
 
11
11
  /**
12
- * Captures the key belonging to this factory invocation while retaining the
12
+ * Captures the key belonging to this request or factory invocation while retaining the
13
13
  * existing boolean credential-hook contract. Legacy overrides that do not
14
14
  * capture a key keep their shared-property behavior and remain responsible for
15
15
  * synchronizing concurrent credential updates.
@@ -0,0 +1,111 @@
1
+ type AbortCallback = () => void;
2
+ interface WeakReference<T> {
3
+ deref: () => T | undefined;
4
+ }
5
+ type WeakAbortCallback = WeakReference<AbortCallback>;
6
+ interface AbortFinalizer {
7
+ register: (target: AbortCallback, cleanup: AbortCallback, token: object) => void;
8
+ unregister: (token: object) => boolean;
9
+ }
10
+
11
+ // Keep these optional runtime features out of the SDK's ES2020 type requirements.
12
+ const weakGlobals = globalThis as typeof globalThis & {
13
+ WeakRef?: new <T extends object>(target: T) => WeakReference<T>;
14
+ FinalizationRegistry?: new (cleanup: (value: AbortCallback) => void) => AbortFinalizer;
15
+ };
16
+ const finalizer =
17
+ typeof weakGlobals.FinalizationRegistry === 'function'
18
+ ? new weakGlobals.FinalizationRegistry((cleanup) => {
19
+ try {
20
+ cleanup();
21
+ } catch {
22
+ // Caller-provided signal methods must not throw out of a GC callback.
23
+ }
24
+ })
25
+ : undefined;
26
+ const callbackOwners = new WeakMap<object, Set<AbortCallback>>();
27
+ const subscriptions = new WeakMap<AbortSignal, { callbacks: Set<WeakAbortCallback>; abort: AbortCallback }>();
28
+
29
+ // This scope receives only a weak reference, so its closures cannot retain the callback.
30
+ function subscribeWeakly(signal: AbortSignal, reference: WeakAbortCallback, registry: AbortFinalizer) {
31
+ let subscription = subscriptions.get(signal);
32
+ if (!subscription) {
33
+ const callbacks = new Set<WeakAbortCallback>();
34
+ const abort = () => {
35
+ subscriptions.delete(signal);
36
+ for (const callback of callbacks) {
37
+ registry.unregister(callback);
38
+ callback.deref()?.();
39
+ }
40
+ callbacks.clear();
41
+ };
42
+ subscription = { callbacks, abort };
43
+ signal.addEventListener('abort', abort, { once: true });
44
+ subscriptions.set(signal, subscription);
45
+ }
46
+ const owner = subscription;
47
+ owner.callbacks.add(reference);
48
+ return () => {
49
+ owner.callbacks.delete(reference);
50
+ registry.unregister(reference);
51
+ if (owner.callbacks.size === 0) {
52
+ if (subscriptions.get(signal) === owner) {
53
+ subscriptions.delete(signal);
54
+ }
55
+ signal.removeEventListener('abort', owner.abort);
56
+ }
57
+ };
58
+ }
59
+
60
+ // The listener must not retain the shared owner or its other request callbacks.
61
+ function releaseOnAbort(
62
+ signal: AbortSignal,
63
+ callbacks: WeakReference<Set<AbortCallback>>,
64
+ abort: AbortCallback,
65
+ ) {
66
+ signal.addEventListener('abort', () => callbacks.deref()?.delete(abort), { once: true });
67
+ }
68
+
69
+ /** Keep cancellation alive until abort or collection of the response body or bodyless custom response. */
70
+ export function retainRequestAbortCallback(
71
+ owner: object,
72
+ abort: AbortCallback,
73
+ requestSignal: AbortSignal,
74
+ ): void {
75
+ if (typeof weakGlobals.WeakRef === 'function' && finalizer && !requestSignal.aborted) {
76
+ let callbacks = callbackOwners.get(owner);
77
+ if (!callbacks) {
78
+ callbacks = new Set();
79
+ callbackOwners.set(owner, callbacks);
80
+ }
81
+ callbacks.add(abort);
82
+ releaseOnAbort(requestSignal, new weakGlobals.WeakRef(callbacks), abort);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Share one caller listener without it retaining completed requests. Collection removes
88
+ * weak subscriptions eventually; a live response body or custom response retains its callback.
89
+ * Runtimes without weak references keep the existing listener-based behavior.
90
+ */
91
+ export function addRequestAbortListener(
92
+ signal: AbortSignal,
93
+ abort: AbortCallback,
94
+ requestSignal: AbortSignal,
95
+ ): AbortCallback {
96
+ if (signal.aborted) {
97
+ abort();
98
+ return () => {
99
+ // No listener was installed for an already aborted signal.
100
+ };
101
+ }
102
+ if (typeof weakGlobals.WeakRef !== 'function' || !finalizer) {
103
+ signal.addEventListener('abort', abort, { once: true });
104
+ return () => signal.removeEventListener('abort', abort);
105
+ }
106
+ const reference = new weakGlobals.WeakRef(abort);
107
+ const cleanup = subscribeWeakly(signal, reference, finalizer);
108
+ finalizer.register(abort, cleanup, reference);
109
+ retainRequestAbortCallback(requestSignal, abort, requestSignal);
110
+ return cleanup;
111
+ }
@@ -1,4 +1,26 @@
1
1
  import { concatBytes, encodeUTF8 } from './utils/bytes';
2
+ import { OpenAIError } from '../core/error';
3
+
4
+ /** Options for an independently buffered WebSocket stream iterator. */
5
+ export interface WebSocketStreamOptions {
6
+ /**
7
+ * Maximum queued records, including raw data, errors and lifecycle events.
8
+ * Must be a positive safe integer. Omitted means unlimited. Overflow discards
9
+ * this iterator's backlog and rejects its next() calls with a WebSocketError,
10
+ * without closing the shared socket or affecting other iterators.
11
+ * This bounds event count, not payload bytes or total memory.
12
+ */
13
+ maxBufferedEvents?: number | undefined;
14
+ }
15
+
16
+ /** Snapshots and validates the iterator's limit before listeners are attached. */
17
+ export function getMaxBufferedEvents(options?: WebSocketStreamOptions): number | undefined {
18
+ const limit = options?.maxBufferedEvents;
19
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit <= 0)) {
20
+ throw new OpenAIError('maxBufferedEvents must be a positive safe integer');
21
+ }
22
+ return limit;
23
+ }
2
24
 
3
25
  /** Reconnection event passed to the `onReconnecting` handler and event listeners. */
4
26
  export interface ReconnectingEvent<Parameters = Record<string, unknown>> {
@@ -15,11 +15,17 @@ export type GroupingUpdate =
15
15
  | { type: 'updated'; segment: TranscriptSegment }
16
16
  | { type: 'closed'; segment: TranscriptSegment; reason: TranscriptSegmentCloseReason };
17
17
 
18
+ interface Acknowledgment {
19
+ characters: number;
20
+ text: string | undefined;
21
+ }
22
+
18
23
  type Turn = TranscriptFragment & {
19
24
  id: string;
20
25
  previousId: string | null;
21
26
  emitted: boolean;
22
27
  canDropAsBackchannel: boolean;
28
+ acknowledgment?: Acknowledgment | undefined;
23
29
  };
24
30
 
25
31
  const ACKNOWLEDGMENTS = [
@@ -42,14 +48,16 @@ const ACKNOWLEDGMENTS = [
42
48
  ];
43
49
 
44
50
  function normalizeAcknowledgment(text: string): string {
45
- return text
51
+ const normalized = text
46
52
  .toLowerCase()
47
53
  .split('-')
48
54
  .join(' ')
49
- .replace(/^[\s.,!?;:"'()[\]{}]+/u, '')
50
- .replace(/[\s.,!?;:"'()[\]{}]+$/u, '')
51
- .split(/\s+/u)
52
- .join(' ');
55
+ .replace(/^[\s.,!?;:"'()[\]{}]+/u, '');
56
+ let end = normalized.length;
57
+ while (end > 0 && /[\s.,!?;:"'()[\]{}]/u.test(normalized.charAt(end - 1))) {
58
+ end -= 1;
59
+ }
60
+ return normalized.slice(0, end).split(/\s+/u).join(' ');
53
61
  }
54
62
 
55
63
  /** The v2 grouping policy, driven by timed public text rather than engine frames. */
@@ -63,6 +71,7 @@ export class TranscriptGrouping {
63
71
  private readonly options: Required<TranscriptGrouperOptions>;
64
72
  private readonly idPrefix: string;
65
73
  private readonly acknowledgments: readonly string[];
74
+ private readonly maxAcknowledgmentLength: number;
66
75
 
67
76
  constructor(options: Required<TranscriptGrouperOptions>, idPrefix: string) {
68
77
  this.options = options;
@@ -71,6 +80,11 @@ export class TranscriptGrouping {
71
80
  ...ACKNOWLEDGMENTS,
72
81
  ...options.additionalAcknowledgments.map(normalizeAcknowledgment).filter(Boolean),
73
82
  ];
83
+ let maxLength = 0;
84
+ for (const acknowledgment of this.acknowledgments) {
85
+ maxLength = Math.max(maxLength, acknowledgment.length);
86
+ }
87
+ this.maxAcknowledgmentLength = maxLength;
74
88
  }
75
89
 
76
90
  get speaker(): TranscriptFragment['speaker'] | undefined {
@@ -183,16 +197,22 @@ export class TranscriptGrouping {
183
197
  this.buffer(fragment);
184
198
  return this.promote();
185
199
  }
200
+ const withinDuration =
201
+ fragment.endMs - (this.buffered?.startMs ?? fragment.startMs) < this.options.backchannelMaxDurationMs;
202
+ const acknowledgment = this.acknowledgment(fragment, withinDuration);
203
+ const normalized = withinDuration ? acknowledgment.text : undefined;
186
204
  if (separation < this.options.minTurnSeparationMs) {
187
- this.buffer(fragment, this.possibleAcknowledgment(fragment));
188
- return [];
189
- }
190
- if (this.buffered && this.standaloneAcknowledgment(fragment, this.buffered)) {
191
- this.buffer(fragment, true);
205
+ this.buffer(
206
+ fragment,
207
+ normalized !== undefined &&
208
+ normalized.length > 0 &&
209
+ this.acknowledgments.some((phrase) => phrase.startsWith(normalized)),
210
+ acknowledgment,
211
+ );
192
212
  return [];
193
213
  }
194
- if (!this.buffered && this.standaloneAcknowledgment(fragment)) {
195
- this.buffer(fragment, false);
214
+ if (normalized !== undefined && this.acknowledgments.includes(normalized)) {
215
+ this.buffer(fragment, this.buffered !== undefined, acknowledgment);
196
216
  return [];
197
217
  }
198
218
  this.buffered = this.maybeDropBackchannel(undefined, fragment);
@@ -227,7 +247,7 @@ export class TranscriptGrouping {
227
247
  turn.endMs = Math.max(turn.endMs, fragment.endMs);
228
248
  }
229
249
 
230
- private buffer(fragment: TranscriptFragment, canDrop?: boolean): void {
250
+ private buffer(fragment: TranscriptFragment, canDrop?: boolean, acknowledgment?: Acknowledgment): void {
231
251
  if (this.buffered) {
232
252
  TranscriptGrouping.append(this.buffered, fragment);
233
253
  } else {
@@ -236,6 +256,7 @@ export class TranscriptGrouping {
236
256
  if (canDrop !== undefined) {
237
257
  this.buffered.canDropAsBackchannel = canDrop;
238
258
  }
259
+ this.buffered.acknowledgment = acknowledgment;
239
260
  }
240
261
 
241
262
  private promote(): GroupingUpdate[] {
@@ -344,19 +365,30 @@ export class TranscriptGrouping {
344
365
  return this.buffered.canDropAsBackchannel ? undefined : this.buffered;
345
366
  }
346
367
 
347
- private standaloneAcknowledgment(fragment: TranscriptFragment, previous?: Turn): boolean {
348
- return (
349
- fragment.endMs - (previous?.startMs ?? fragment.startMs) < this.options.backchannelMaxDurationMs &&
350
- this.acknowledgments.includes(normalizeAcknowledgment((previous?.text ?? '') + fragment.text))
351
- );
352
- }
353
-
354
- private possibleAcknowledgment(fragment: TranscriptFragment): boolean {
355
- const text = normalizeAcknowledgment((this.buffered?.text ?? '') + fragment.text);
356
- return (
357
- fragment.endMs - (this.buffered?.startMs ?? fragment.startMs) < this.options.backchannelMaxDurationMs &&
358
- text.length > 0 &&
359
- this.acknowledgments.some((acknowledgment) => acknowledgment.startsWith(text))
360
- );
368
+ private acknowledgment(fragment: TranscriptFragment, withinDuration: boolean): Acknowledgment {
369
+ const previous = this.buffered?.acknowledgment;
370
+ const previousCharacters = previous?.characters ?? 0;
371
+ let characters = previousCharacters;
372
+ const separator = /[\s.,!?;:"'()[\]{}-]/u;
373
+ for (
374
+ let index = 0;
375
+ index < fragment.text.length && characters <= this.maxAcknowledgmentLength;
376
+ index += 1
377
+ ) {
378
+ if (!separator.test(fragment.text.charAt(index))) {
379
+ characters += 1;
380
+ }
381
+ }
382
+ // Significant characters cannot disappear during normalization. Once they
383
+ // outgrow the configured phrases, stop counting without allocating tokens.
384
+ let text: string | undefined;
385
+ if (characters === previousCharacters && previous?.text !== undefined) {
386
+ // Keep the raw suffix in the turn: later text can make punctuation internal.
387
+ ({ text } = previous);
388
+ } else if (withinDuration && characters <= this.maxAcknowledgmentLength) {
389
+ // Retain whole-string Unicode casing (including context-sensitive sigma).
390
+ text = normalizeAcknowledgment((this.buffered?.text ?? '') + fragment.text);
391
+ }
392
+ return { characters, text };
361
393
  }
362
394
  }
@@ -79,6 +79,11 @@ export abstract class ResponsesEmitter extends EventEmitter<WebSocketEvents> {
79
79
  protected _onError(event: WebSocketErrorEvent | null, message?: string | undefined, cause?: any): void {
80
80
  const eventMessage = event && ('error' in event ? event.error?.message : event.message);
81
81
  message = message ?? eventMessage ?? safeJSONStringify(event) ?? 'unknown error';
82
+ try {
83
+ message = String(message);
84
+ } catch {
85
+ message = '[unserializable error value]';
86
+ }
82
87
 
83
88
  if (!this._hasListener('error')) {
84
89
  const error = new WebSocketError(