@gajae-code/ai 0.17.2 → 0.17.5

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 (43) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/dist/types/auth-gateway/server.d.ts +23 -1
  3. package/dist/types/auth-storage.d.ts +12 -1
  4. package/dist/types/providers/anthropic.d.ts +1 -1
  5. package/dist/types/providers/cursor.d.ts +10 -0
  6. package/dist/types/providers/openai-completions.d.ts +9 -1
  7. package/dist/types/types.d.ts +16 -0
  8. package/dist/types/utils/discovery/openai-compatible.d.ts +10 -0
  9. package/dist/types/utils/fallback-transport.d.ts +4 -0
  10. package/dist/types/utils/h2-fetch.d.ts +8 -7
  11. package/dist/types/utils/stream-repetition-guard.d.ts +107 -0
  12. package/dist/types/utils/tool-call-healing.d.ts +4 -0
  13. package/dist/types/utils/tool-fence-strip.d.ts +27 -0
  14. package/package.json +3 -3
  15. package/src/auth-gateway/server.ts +48 -9
  16. package/src/auth-storage.ts +194 -52
  17. package/src/model-pricing.ts +22 -0
  18. package/src/model-thinking.ts +40 -4
  19. package/src/models.json +10060 -3164
  20. package/src/providers/anthropic.d.ts +1 -1
  21. package/src/providers/anthropic.ts +1 -1
  22. package/src/providers/cursor.d.ts +10 -0
  23. package/src/providers/cursor.ts +144 -24
  24. package/src/providers/kiro-api-key.ts +7 -0
  25. package/src/providers/openai-completions.d.ts +9 -1
  26. package/src/providers/openai-completions.ts +410 -150
  27. package/src/providers/transform-messages.ts +54 -1
  28. package/src/stream.ts +7 -0
  29. package/src/types.d.ts +16 -0
  30. package/src/types.ts +17 -0
  31. package/src/utils/discovery/openai-compatible.ts +16 -2
  32. package/src/utils/fallback-transport.d.ts +4 -0
  33. package/src/utils/fallback-transport.ts +11 -0
  34. package/src/utils/h2-fetch.ts +65 -26
  35. package/src/utils/http-inspector.ts +2 -0
  36. package/src/utils/idle-iterator.ts +109 -96
  37. package/src/utils/json-parse.ts +12 -4
  38. package/src/utils/stream-repetition-guard.d.ts +107 -0
  39. package/src/utils/stream-repetition-guard.ts +290 -0
  40. package/src/utils/tool-call-healing.d.ts +4 -0
  41. package/src/utils/tool-call-healing.ts +4 -0
  42. package/src/utils/tool-fence-strip.d.ts +27 -0
  43. package/src/utils/tool-fence-strip.ts +64 -0
@@ -85,6 +85,59 @@ function collapseAdjacentThinking<T extends { type: string }>(content: T[]): T[]
85
85
  return dropped ? collapsed : content;
86
86
  }
87
87
 
88
+ const MIN_CROSS_MODEL_THINKING_REPEAT_COUNT = 64;
89
+ const MIN_CROSS_MODEL_THINKING_REPEAT_SAVED_CHARACTERS = 4_096;
90
+
91
+ /**
92
+ * Bound pathological cross-model reasoning replay without editing the stored
93
+ * thinking block. Only exact adjacent non-empty paragraphs qualify, and the
94
+ * threshold requires both a large run and substantial net savings.
95
+ */
96
+ function compressRepeatedThinkingParagraphs(thinking: string): string {
97
+ const parts = thinking.split(/(\r?\n(?:[ \t]*\r?\n)+)/);
98
+ const compressedParts: string[] = [];
99
+ let compressed = false;
100
+
101
+ for (let paragraphIndex = 0; paragraphIndex < parts.length; ) {
102
+ const paragraph = parts[paragraphIndex];
103
+ let runEnd = paragraphIndex;
104
+ while (runEnd + 2 < parts.length && parts[runEnd + 2] === paragraph) {
105
+ runEnd += 2;
106
+ }
107
+
108
+ const repeatCount = (runEnd - paragraphIndex) / 2 + 1;
109
+ let marker: string | undefined;
110
+ let savedCharacters = 0;
111
+ if (paragraph.length > 0 && repeatCount >= MIN_CROSS_MODEL_THINKING_REPEAT_COUNT) {
112
+ marker = `[Repeated paragraph occurred exactly ${repeatCount} consecutive times; only its first occurrence is shown.]`;
113
+ savedCharacters = (repeatCount - 1) * paragraph.length - marker.length;
114
+ for (let separatorIndex = paragraphIndex + 3; separatorIndex < runEnd; separatorIndex += 2) {
115
+ savedCharacters += parts[separatorIndex].length;
116
+ }
117
+ }
118
+
119
+ if (marker !== undefined && savedCharacters >= MIN_CROSS_MODEL_THINKING_REPEAT_SAVED_CHARACTERS) {
120
+ compressedParts.push(paragraph);
121
+ if (paragraphIndex + 1 < parts.length) compressedParts.push(parts[paragraphIndex + 1]);
122
+ compressedParts.push(marker);
123
+ compressed = true;
124
+ } else {
125
+ for (let partIndex = paragraphIndex; partIndex <= runEnd; partIndex++) {
126
+ compressedParts.push(parts[partIndex]);
127
+ }
128
+ }
129
+
130
+ // The separator after the run belongs to the next paragraph and must
131
+ // survive verbatim, regardless of whether this run was compressed.
132
+ if (runEnd + 1 < parts.length) {
133
+ compressedParts.push(parts[runEnd + 1]);
134
+ }
135
+ paragraphIndex = runEnd + 2;
136
+ }
137
+
138
+ return compressed ? compressedParts.join("") : thinking;
139
+ }
140
+
88
141
  export function transformMessages<TApi extends Api>(
89
142
  messages: Message[],
90
143
  model: Model<TApi>,
@@ -170,7 +223,7 @@ export function transformMessages<TApi extends Api>(
170
223
  if (isSameModel) return sanitized;
171
224
  return {
172
225
  type: "text" as const,
173
- text: sanitized.thinking,
226
+ text: compressRepeatedThinkingParagraphs(sanitized.thinking),
174
227
  };
175
228
  }
176
229
 
package/src/stream.ts CHANGED
@@ -500,6 +500,9 @@ export async function complete<TApi extends Api>(
500
500
  options?: OptionsForApi<TApi>,
501
501
  ): Promise<AssistantMessage> {
502
502
  const s = stream(model, context, options);
503
+ for await (const _event of s) {
504
+ // Completion callers only need the terminal message, not buffered events.
505
+ }
503
506
  return s.result();
504
507
  }
505
508
 
@@ -824,6 +827,9 @@ export async function completeSimple<TApi extends Api>(
824
827
  options?: SimpleStreamOptions,
825
828
  ): Promise<AssistantMessage> {
826
829
  const s = streamSimple(model, context, options);
830
+ for await (const _event of s) {
831
+ // Completion callers only need the terminal message, not buffered events.
832
+ }
827
833
  return s.result();
828
834
  }
829
835
 
@@ -1113,6 +1119,7 @@ function mapOptionsForApi<TApi extends Api>(
1113
1119
  : options?.disableReasoning,
1114
1120
  toolChoice: mapOpenAiToolChoice(options?.toolChoice),
1115
1121
  serviceTier: options?.serviceTier,
1122
+ repetitionGuard: options?.repetitionGuard,
1116
1123
  });
1117
1124
 
1118
1125
  case "openai-responses":
package/src/types.d.ts CHANGED
@@ -338,6 +338,14 @@ export interface AttemptScopeRef {
338
338
  readonly generation: number;
339
339
  readonly lineage: string;
340
340
  }
341
+ /**
342
+ * Runaway-repetition guard thresholds, per stream channel. A number sets the
343
+ * consecutive-repeat threshold; `false` disables that channel's guard.
344
+ */
345
+ export interface RepetitionGuardOptions {
346
+ thinking?: number | false;
347
+ text?: number | false;
348
+ }
341
349
  export interface SimpleStreamOptions extends StreamOptions {
342
350
  reasoning?: Effort;
343
351
  /**
@@ -373,6 +381,14 @@ export interface SimpleStreamOptions extends StreamOptions {
373
381
  syntheticApiFormat?: "openai" | "anthropic";
374
382
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
375
383
  preferWebsockets?: boolean;
384
+ /**
385
+ * Runaway-repetition guard thresholds, per stream channel. Honoured by the
386
+ * openai-completions transport; ignored by providers without a guard.
387
+ * Defaults: thinking = DEFAULT_REPETITION_THRESHOLD, text = false — visible
388
+ * output is a deliverable and intentional repetition there (logs, fixtures,
389
+ * tables, generated code) must survive byte for byte (#5627).
390
+ */
391
+ repetitionGuard?: RepetitionGuardOptions;
376
392
  }
377
393
  export type StreamFunction<TApi extends Api> = (model: Model<TApi>, context: Context, options: OptionsForApi<TApi>) => AssistantMessageEventStream;
378
394
  export interface TextSignatureV1 {
package/src/types.ts CHANGED
@@ -556,6 +556,15 @@ export interface AttemptScopeRef {
556
556
  readonly lineage: string;
557
557
  }
558
558
 
559
+ /**
560
+ * Runaway-repetition guard thresholds, per stream channel. A number sets the
561
+ * consecutive-repeat threshold; `false` disables that channel's guard.
562
+ */
563
+ export interface RepetitionGuardOptions {
564
+ thinking?: number | false;
565
+ text?: number | false;
566
+ }
567
+
559
568
  // Unified options with reasoning passed to streamSimple() and completeSimple()
560
569
  export interface SimpleStreamOptions extends StreamOptions {
561
570
  reasoning?: Effort;
@@ -592,6 +601,14 @@ export interface SimpleStreamOptions extends StreamOptions {
592
601
  syntheticApiFormat?: "openai" | "anthropic";
593
602
  /** Hint that websocket transport should be preferred when supported by the provider implementation. */
594
603
  preferWebsockets?: boolean;
604
+ /**
605
+ * Runaway-repetition guard thresholds, per stream channel. Honoured by the
606
+ * openai-completions transport; ignored by providers without a guard.
607
+ * Defaults: thinking = DEFAULT_REPETITION_THRESHOLD, text = false — visible
608
+ * output is a deliverable and intentional repetition there (logs, fixtures,
609
+ * tables, generated code) must survive byte for byte (#5627).
610
+ */
611
+ repetitionGuard?: RepetitionGuardOptions;
595
612
  }
596
613
 
597
614
  // Generic StreamFunction with typed options
@@ -5,6 +5,15 @@ import type { Api, FetchImpl, Model, Provider } from "../../types";
5
5
  import { toNumber } from "../../utils";
6
6
 
7
7
  const MODELS_PATH = "/models";
8
+
9
+ /**
10
+ * Shared `/models` request deadline for setup-time probing and runtime
11
+ * discovery. One policy so an endpoint that passes setup validation cannot
12
+ * be unreachable under the runtime deadline (previously 10s vs 5s: a 6s
13
+ * endpoint passed setup, saved discovery-only config, then stayed
14
+ * unavailable at runtime with no recovery hint).
15
+ */
16
+ export const MODELS_LIST_REQUEST_TIMEOUT_MS = 10_000;
8
17
  const MAX_MODELS_RESPONSE_BYTES = 1_000_000;
9
18
  const MAX_CATALOG_MODEL_ID_LENGTH = 200;
10
19
 
@@ -233,8 +242,8 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
233
242
  method: "GET",
234
243
  headers: requestHeaders,
235
244
  signal: options.signal
236
- ? AbortSignal.any([options.signal, AbortSignal.timeout(5_000)])
237
- : AbortSignal.timeout(5_000),
245
+ ? AbortSignal.any([options.signal, AbortSignal.timeout(MODELS_LIST_REQUEST_TIMEOUT_MS)])
246
+ : AbortSignal.timeout(MODELS_LIST_REQUEST_TIMEOUT_MS),
238
247
  });
239
248
  } catch {
240
249
  return null;
@@ -308,6 +317,11 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
308
317
  return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
309
318
  }
310
319
 
320
+ /** Bounded JSON body reader for `/models` responses (shared by runtime and setup probe). */
321
+ export async function readBoundedModelsJson(response: Response): Promise<unknown> {
322
+ return JSON.parse(await readModelsResponse(response));
323
+ }
324
+
311
325
  async function readModelsResponse(response: Response): Promise<string> {
312
326
  const contentLength = Number(response.headers.get("content-length"));
313
327
  if (Number.isFinite(contentLength) && contentLength > MAX_MODELS_RESPONSE_BYTES) {
@@ -43,6 +43,10 @@ export type TransportHeaders = Headers | Record<string, string | undefined>;
43
43
  */
44
44
  export interface TransportFailureFacts {
45
45
  kind: "transport";
46
+ /** Diagnostic HTTP/2 reset code; not HTTP status or retry authority. */
47
+ http2RstCode?: number;
48
+ /** Native HTTP/2 error code; diagnostic only, not retry authority. */
49
+ nativeErrorCode?: string;
46
50
  status?: number;
47
51
  /** Canonical provider error code used for fallback classification. */
48
52
  providerCode?: string;
@@ -48,6 +48,10 @@ export type TransportHeaders = Headers | Record<string, string | undefined>;
48
48
  */
49
49
  export interface TransportFailureFacts {
50
50
  kind: "transport";
51
+ /** Diagnostic HTTP/2 reset code; not HTTP status or retry authority. */
52
+ http2RstCode?: number;
53
+ /** Native HTTP/2 error code; diagnostic only, not retry authority. */
54
+ nativeErrorCode?: string;
51
55
  status?: number;
52
56
  /** Canonical provider error code used for fallback classification. */
53
57
  providerCode?: string;
@@ -235,6 +239,9 @@ export function transportFailureFacts(
235
239
  // (consumers deliberately re-run transportFailureFacts on embedded facts).
236
240
  const headers = retainedHeaderRecord(rawHeaders);
237
241
  const normalizedCode = providerCode?.toLowerCase();
242
+ const http2RstCode = finiteNonNegativeInteger(propertyOf(value, "http2RstCode"));
243
+ const nativeCode = stringValue(propertyOf(value, "nativeErrorCode")) ?? stringValue(propertyOf(value, "code"));
244
+ const nativeErrorCode = nativeCode && /^ERR_HTTP2_[A-Z_]+$/.test(nativeCode) ? nativeCode : undefined;
238
245
  const requestBytes = finiteNonNegativeInteger(propertyOf(value, "requestBytes"));
239
246
  const firstEventElapsedMs = finiteNonNegativeInteger(propertyOf(value, "firstEventElapsedMs"));
240
247
  const firstEventTimeoutMs = finiteNonNegativeInteger(propertyOf(value, "firstEventTimeoutMs"));
@@ -244,6 +251,8 @@ export function transportFailureFacts(
244
251
  endpointClassValue === "canonical" || endpointClassValue === "custom" ? endpointClassValue : undefined;
245
252
  const credentialModelUnavailable = propertyOf(value, "credentialModelUnavailable") === true;
246
253
  if (
254
+ http2RstCode === undefined &&
255
+ nativeErrorCode === undefined &&
247
256
  status === undefined &&
248
257
  headers === undefined &&
249
258
  !isQuotaCode(normalizedCode) &&
@@ -268,6 +277,8 @@ export function transportFailureFacts(
268
277
  }
269
278
  return {
270
279
  kind: "transport",
280
+ ...(http2RstCode === undefined ? {} : { http2RstCode }),
281
+ ...(nativeErrorCode === undefined ? {} : { nativeErrorCode }),
271
282
  status,
272
283
  providerCode,
273
284
  anthropicErrorType,
@@ -11,13 +11,14 @@
11
11
  * Some HTTPS endpoints (e.g. corporate API gateways behind reverse proxies)
12
12
  * advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
13
13
  * framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
14
- * or `ConnectionClosed` rather than `HTTP2Unsupported`, so we treat those
15
- * codes as h2-fallback triggers as well. `ConnectionRefused` is raised before
16
- * the request is written, but a reset or a close does not prove the peer never
17
- * consumed the body — it may have processed the request and died before
18
- * answering. Replaying those two on h1 would duplicate the side effect, so
19
- * `ConnectionReset` and `ConnectionClosed` fall back only for replay-safe
20
- * methods; anything else rethrows the original error.
14
+ * `ConnectionClosed`, or `HTTP2StreamReset` rather than `HTTP2Unsupported`.
15
+ * `ConnectionRefused` is raised before the request is written, while
16
+ * `HTTP2RefusedStream` is the explicit HTTP/2 promise that a stream was never
17
+ * processed (including a stream above a graceful GOAWAY last-stream-id).
18
+ * `ConnectionReset`, `ConnectionClosed`, and a generic `HTTP2StreamReset` do
19
+ * not carry that promise: the peer may have consumed the body before the
20
+ * connection failed. Retrying those codes would duplicate non-idempotent side
21
+ * effects, so they preserve the original error instead of falling back.
21
22
  *
22
23
  * ALPN-refusing hosts (notably zcode.z.ai, the GLM ZCode OAuth broker) abort
23
24
  * the TLS handshake entirely when the client offers ALPN h2. Bun reports that
@@ -46,23 +47,21 @@ export function installH2Fetch(): void {
46
47
  const h2FallbackCodes: ReadonlySet<string> = new Set([
47
48
  "HTTP2Unsupported", // Server selected h1 in ALPN
48
49
  "ConnectionRefused", // Server refused the h2 connection
49
- "ConnectionReset", // Server reset during h2 handshake
50
- "ConnectionClosed", // Server closed before h2 response
50
+ "HTTP2RefusedStream", // REFUSED_STREAM / never-processed h2 stream
51
51
  // Bun's h2 client reports an ALPN-refusing host's TLS abort with this
52
52
  // code; the h1 fallback below re-verifies the certificate itself.
53
53
  "UNKNOWN_CERTIFICATE_VERIFICATION_ERROR",
54
54
  ]);
55
- /** Fallback codes that may fire *after* the peer consumed the body — replay only when safe. */
56
- const replayGatedCodes: ReadonlySet<string> = new Set(["ConnectionReset", "ConnectionClosed"]);
57
55
  const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
58
56
  if (!isHttps(input)) return original(input, init);
57
+ const snapshot = snapshotRequest(input, init);
59
58
  try {
60
- return await original(input, { ...init, protocol: "http2" });
59
+ return await original(snapshot.input, { ...snapshot.init, protocol: "http2" });
61
60
  } catch (err) {
62
61
  const code = (err as { code?: string }).code ?? "";
63
62
  if (!h2FallbackCodes.has(code)) throw err;
64
- if (replayGatedCodes.has(code) && !isReplaySafeRequest(input, init)) throw err;
65
- return original(input, init);
63
+ if (code === "HTTP2RefusedStream" && !isReplayableRequest(snapshot.input, snapshot.init)) throw err;
64
+ return original(snapshot.input, snapshot.init);
66
65
  }
67
66
  } as typeof fetch & PatchedFetch;
68
67
 
@@ -72,19 +71,31 @@ export function installH2Fetch(): void {
72
71
  globalThis.fetch = wrapper;
73
72
  }
74
73
 
75
- /** Methods a transport-layer retry cannot turn into a second side effect. */
76
- const replaySafeMethods: ReadonlySet<string> = new Set(["GET", "HEAD", "OPTIONS"]);
74
+ function snapshotRequest(
75
+ input: string | URL | Request,
76
+ init?: RequestInit,
77
+ ): { input: string | URL | Request; init?: RequestInit } {
78
+ const snapshotInput = input instanceof URL ? new URL(input.href) : input;
79
+ const requestLike = typeof input !== "string" && !(input instanceof URL);
80
+ if (init === undefined && !requestLike) {
81
+ return { input: snapshotInput };
82
+ }
77
83
 
78
- /**
79
- * Whether replaying this request on a fresh connection is side-effect free.
80
- *
81
- * PR #5614 introduces an identically-named helper with the same semantics for
82
- * `HTTP2StreamReset`; whichever of the two lands second should collapse into
83
- * this one rather than leaving the repo with two devices doing the same job.
84
- */
85
- function isReplaySafeRequest(input: string | URL | Request, init?: RequestInit): boolean {
86
- const method = init?.method ?? (input instanceof Request ? input.method : "GET");
87
- return replaySafeMethods.has(method.toUpperCase());
84
+ const snapshotInit: RequestInit = { ...(init ?? {}) };
85
+ if (init?.headers !== undefined) {
86
+ snapshotInit.headers = snapshotHeaders(init.headers);
87
+ } else if (requestLike) {
88
+ snapshotInit.headers = new Headers(input.headers);
89
+ }
90
+ return { input: snapshotInput, init: snapshotInit };
91
+ }
92
+
93
+ function snapshotHeaders(headers: NonNullable<RequestInit["headers"]>): NonNullable<RequestInit["headers"]> {
94
+ if (headers instanceof Headers) return new Headers(headers);
95
+ if (Array.isArray(headers)) {
96
+ return headers.map(([name, value]) => [name, value] as [string, string]);
97
+ }
98
+ return { ...headers };
88
99
  }
89
100
 
90
101
  function isHttps(input: string | URL | Request): boolean {
@@ -92,3 +103,31 @@ function isHttps(input: string | URL | Request): boolean {
92
103
  if (input instanceof URL) return input.protocol === "https:";
93
104
  return input.url.startsWith("https:");
94
105
  }
106
+
107
+ /**
108
+ * Whether the request body can be handed to fetch a second time. This is only
109
+ * used after `HTTP2RefusedStream`, which proves that the peer did not consume
110
+ * request bytes; a one-shot ReadableStream is still not reusable locally.
111
+ */
112
+ function isReplayableRequest(input: string | URL | Request, init?: RequestInit): boolean {
113
+ try {
114
+ if (init?.body !== undefined) return isReplayableBody(init.body);
115
+ if (typeof input === "string" || input instanceof URL) return true;
116
+ // Request bodies are exposed as ReadableStreams. The same Request object
117
+ // cannot be used for the second fetch once the first attempt touched it.
118
+ return input.body === null;
119
+ } catch {
120
+ // Cross-realm or proxy Request objects may throw while exposing their
121
+ // method/body. Do not retry when replayability cannot be established.
122
+ return false;
123
+ }
124
+ }
125
+
126
+ function isReplayableBody(body: RequestInit["body"]): boolean {
127
+ if (body === null || typeof body === "string") return true;
128
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
129
+ // ArrayBuffer/views, FormData, and URLSearchParams are mutable caller-owned
130
+ // values. Fail closed instead of replaying a potentially changed payload if
131
+ // the asynchronous refusal arrives after the caller mutates one.
132
+ return false;
133
+ }
@@ -43,6 +43,8 @@ const TRANSPORT_FAILURE_CODES: ReadonlySet<string> = new Set([
43
43
  "ConnectionReset",
44
44
  "ConnectionTimeout",
45
45
  "FailedToOpenSocket",
46
+ "HTTP2RefusedStream",
47
+ "HTTP2StreamReset",
46
48
  "HTTP2Unsupported",
47
49
  "EAI_AGAIN",
48
50
  "ECONNREFUSED",
@@ -255,11 +255,19 @@ export async function* iterateWithIdleTimeout<T>(
255
255
  const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
256
256
  const abortSignal = options.abortSignal;
257
257
  const iterator = iterable[Symbol.asyncIterator]();
258
+ let naturallyExhausted = false;
259
+ let iteratorClosed = false;
258
260
 
259
261
  const closeIterator = (): void => {
260
- const returnPromise = iterator.return?.();
261
- if (returnPromise) {
262
- void returnPromise.catch(() => {});
262
+ if (iteratorClosed) return;
263
+ iteratorClosed = true;
264
+ try {
265
+ const returnPromise = iterator.return?.();
266
+ if (returnPromise) {
267
+ void returnPromise.catch(() => {});
268
+ }
269
+ } catch {
270
+ // Cleanup must not replace the source error or timeout that triggered it.
263
271
  }
264
272
  };
265
273
 
@@ -296,109 +304,114 @@ export async function* iterateWithIdleTimeout<T>(
296
304
  (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
297
305
  (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0);
298
306
 
299
- while (true) {
300
- let activeTimeoutMs: number | undefined;
301
- if (awaitingFirstItem) {
302
- activeTimeoutMs =
303
- firstItemDeadlineAt === undefined ? undefined : Math.max(0, firstItemDeadlineAt - Date.now());
304
- } else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
305
- activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
306
- // The idle deadline may already have elapsed because the *consumer*
307
- // was slow, not because the provider stalled — and the next item may
308
- // already be buffered and ready to deliver. Clamp to 0 instead of
309
- // throwing eagerly so the next() race below still gets a chance to
310
- // win (it settles on a microtask, ahead of the 0ms timer). Only a
311
- // genuinely hung iterator loses that race and surfaces as a stall.
312
- if (activeTimeoutMs < 0) {
313
- activeTimeoutMs = 0;
307
+ try {
308
+ while (true) {
309
+ let activeTimeoutMs: number | undefined;
310
+ if (awaitingFirstItem) {
311
+ activeTimeoutMs =
312
+ firstItemDeadlineAt === undefined ? undefined : Math.max(0, firstItemDeadlineAt - Date.now());
313
+ } else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
314
+ activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
315
+ // The idle deadline may already have elapsed because the *consumer*
316
+ // was slow, not because the provider stalled — and the next item may
317
+ // already be buffered and ready to deliver. Clamp to 0 instead of
318
+ // throwing eagerly so the next() race below still gets a chance to
319
+ // win (it settles on a microtask, ahead of the 0ms timer). Only a
320
+ // genuinely hung iterator loses that race and surfaces as a stall.
321
+ if (activeTimeoutMs < 0) {
322
+ activeTimeoutMs = 0;
323
+ }
314
324
  }
315
- }
316
325
 
317
- const racers: Array<
318
- Promise<
319
- | { kind: "next"; result: IteratorResult<T> }
320
- | { kind: "error"; error: unknown }
321
- | { kind: "timeout" }
322
- | { kind: "abort" }
323
- >
324
- > = [];
326
+ const racers: Array<
327
+ Promise<
328
+ | { kind: "next"; result: IteratorResult<T> }
329
+ | { kind: "error"; error: unknown }
330
+ | { kind: "timeout" }
331
+ | { kind: "abort" }
332
+ >
333
+ > = [];
325
334
 
326
- let timer: NodeJS.Timeout | undefined;
327
- let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
328
- const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs >= 0;
329
- if (enforceTimeout) {
330
- const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
331
- resolveTimeout = resolve;
332
- timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
333
- racers.push(promise);
334
- }
335
+ let timer: NodeJS.Timeout | undefined;
336
+ let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
337
+ const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs >= 0;
338
+ if (enforceTimeout) {
339
+ const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
340
+ resolveTimeout = resolve;
341
+ timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
342
+ racers.push(promise);
343
+ }
335
344
 
336
- let abortListener: (() => void) | undefined;
337
- let resolveAbort: ((value: { kind: "abort" }) => void) | undefined;
338
- if (abortSignal) {
339
- const { promise, resolve } = Promise.withResolvers<{ kind: "abort" }>();
340
- resolveAbort = resolve;
341
- abortListener = () => resolve({ kind: "abort" });
342
- abortSignal.addEventListener("abort", abortListener, { once: true });
343
- racers.push(promise);
344
- }
345
+ let abortListener: (() => void) | undefined;
346
+ let resolveAbort: ((value: { kind: "abort" }) => void) | undefined;
347
+ if (abortSignal) {
348
+ const { promise, resolve } = Promise.withResolvers<{ kind: "abort" }>();
349
+ resolveAbort = resolve;
350
+ abortListener = () => resolve({ kind: "abort" });
351
+ abortSignal.addEventListener("abort", abortListener, { once: true });
352
+ racers.push(promise);
353
+ }
345
354
 
346
- // Arm timeout/abort races before asking the source for its next item. A
347
- // periodic keepalive iterator commonly registers its own timer inside
348
- // `next()`; registering that first lets equal-deadline keepalives win every
349
- // race and extend the idle window forever. Already-buffered items still
350
- // settle as microtasks before a 0ms watchdog.
351
- racers.unshift(withRacy(iterator.next()));
355
+ // Arm timeout/abort races before asking the source for its next item. A
356
+ // periodic keepalive iterator commonly registers its own timer inside
357
+ // `next()`; registering that first lets equal-deadline keepalives win every
358
+ // race and extend the idle window forever. Already-buffered items still
359
+ // settle as microtasks before a 0ms watchdog.
360
+ racers.unshift(withRacy(iterator.next()));
352
361
 
353
- try {
354
- const outcome = await Promise.race(racers);
355
- if (outcome.kind === "abort") {
356
- closeIterator();
357
- throw abortReason(abortSignal!);
358
- }
359
- if (outcome.kind === "timeout") {
360
- if (!awaitingFirstItem) {
361
- options.onIdle?.();
362
- } else {
362
+ try {
363
+ const outcome = await Promise.race(racers);
364
+ if (outcome.kind === "abort") {
365
+ closeIterator();
366
+ throw abortReason(abortSignal!);
367
+ }
368
+ if (outcome.kind === "timeout") {
369
+ if (!awaitingFirstItem) {
370
+ options.onIdle?.();
371
+ } else {
372
+ options.onFirstItemTimeout?.();
373
+ }
374
+ closeIterator();
375
+ throw awaitingFirstItem
376
+ ? new FirstEventTimeoutError(options.firstItemErrorMessage ?? options.errorMessage)
377
+ : new Error(options.errorMessage);
378
+ }
379
+ if (outcome.kind === "error") {
380
+ throw outcome.error;
381
+ }
382
+ if (awaitingFirstItem && firstItemDeadlineAt !== undefined && Date.now() >= firstItemDeadlineAt) {
363
383
  options.onFirstItemTimeout?.();
384
+ closeIterator();
385
+ throw new FirstEventTimeoutError(options.firstItemErrorMessage ?? options.errorMessage);
364
386
  }
365
- closeIterator();
366
- throw awaitingFirstItem
367
- ? new FirstEventTimeoutError(options.firstItemErrorMessage ?? options.errorMessage)
368
- : new Error(options.errorMessage);
369
- }
370
- if (outcome.kind === "error") {
371
- throw outcome.error;
372
- }
373
- if (awaitingFirstItem && firstItemDeadlineAt !== undefined && Date.now() >= firstItemDeadlineAt) {
374
- options.onFirstItemTimeout?.();
375
- closeIterator();
376
- throw new FirstEventTimeoutError(options.firstItemErrorMessage ?? options.errorMessage);
377
- }
378
- if (outcome.result.done) {
379
- markFirstItemReceived();
380
- return;
381
- }
382
- const item = outcome.result.value;
383
- // Non-progress items (e.g. provider keepalives, synthetic `start` events that
384
- // arrive before the model has produced any tokens) MUST NOT flip us out of
385
- // `awaitingFirstItem`. Otherwise the next iteration switches from the (longer)
386
- // first-item watchdog to the (shorter) idle watchdog while we're still waiting
387
- // on the model's first real output.
388
- if (isProgressItem(item)) {
389
- markFirstItemReceived();
390
- lastProgressAt = Date.now();
391
- }
392
- yield item;
393
- } finally {
394
- if (timer !== undefined) clearTimeout(timer);
395
- // Resolve dangling promises so the racers don't leak (Promise.race is one-shot).
396
- resolveTimeout?.({ kind: "timeout" });
397
- if (abortListener && abortSignal) {
398
- abortSignal.removeEventListener("abort", abortListener);
387
+ if (outcome.result.done) {
388
+ naturallyExhausted = true;
389
+ markFirstItemReceived();
390
+ return;
391
+ }
392
+ const item = outcome.result.value;
393
+ // Non-progress items (e.g. provider keepalives, synthetic `start` events that
394
+ // arrive before the model has produced any tokens) MUST NOT flip us out of
395
+ // `awaitingFirstItem`. Otherwise the next iteration switches from the (longer)
396
+ // first-item watchdog to the (shorter) idle watchdog while we're still waiting
397
+ // on the model's first real output.
398
+ if (isProgressItem(item)) {
399
+ markFirstItemReceived();
400
+ lastProgressAt = Date.now();
401
+ }
402
+ yield item;
403
+ } finally {
404
+ if (timer !== undefined) clearTimeout(timer);
405
+ // Resolve dangling promises so the racers don't leak (Promise.race is one-shot).
406
+ resolveTimeout?.({ kind: "timeout" });
407
+ if (abortListener && abortSignal) {
408
+ abortSignal.removeEventListener("abort", abortListener);
409
+ }
410
+ resolveAbort?.({ kind: "abort" });
399
411
  }
400
- resolveAbort?.({ kind: "abort" });
401
412
  }
413
+ } finally {
414
+ if (!naturallyExhausted) closeIterator();
402
415
  }
403
416
  }
404
417
 
@@ -249,6 +249,8 @@ export function findUnnecessaryUnicodeEscape(json: string): string | undefined {
249
249
  const len = json.length;
250
250
  let inString = false;
251
251
  let i = 0;
252
+ let nextQuote = -1;
253
+ let nextBackslash = -1;
252
254
 
253
255
  const hexAt = (start: number): number | undefined => {
254
256
  if (start + 3 >= len) return undefined;
@@ -268,10 +270,16 @@ export function findUnnecessaryUnicodeEscape(json: string): string | undefined {
268
270
  // Jump straight to the next quote or backslash. A per-character walk costs
269
271
  // ~40ms on a 1MB literal-UTF-8 payload (a large `write`), and every byte in
270
272
  // between is by definition uninteresting.
271
- const nextQuote = json.indexOf('"', i);
272
- const nextBackslash = json.indexOf("\\", i);
273
- if (nextQuote === -1 && nextBackslash === -1) return undefined;
274
- i = nextBackslash === -1 || (nextQuote !== -1 && nextQuote < nextBackslash) ? nextQuote : nextBackslash;
273
+ if (nextQuote < i) {
274
+ const offset = json.indexOf('"', i);
275
+ nextQuote = offset === -1 ? len : offset;
276
+ }
277
+ if (nextBackslash < i) {
278
+ const offset = json.indexOf("\\", i);
279
+ nextBackslash = offset === -1 ? len : offset;
280
+ }
281
+ i = Math.min(nextQuote, nextBackslash);
282
+ if (i === len) return undefined;
275
283
 
276
284
  if (json.charCodeAt(i) === QUOTE) {
277
285
  inString = false;