@jaypie/llm 1.2.33 → 1.2.35

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.
@@ -0,0 +1,19 @@
1
+ export interface StaleRejectionGuard {
2
+ /** Install the unhandledRejection listener (idempotent) */
3
+ install(): void;
4
+ /** Remove the listener and forget recorded errors */
5
+ remove(): void;
6
+ /** Record an error the retry loop has caught and is handling */
7
+ recordCaught(error: unknown): void;
8
+ }
9
+ /**
10
+ * Create a guard that suppresses unhandled rejections firing as siblings of
11
+ * an error the retry loop has already caught. Provider SDKs occasionally
12
+ * surface a single upstream failure as twin rejections — the retry layer
13
+ * accepts responsibility for the first; this guard prevents the second from
14
+ * crashing the host while the retry is in flight.
15
+ *
16
+ * The guard also continues to suppress transient socket teardown errors
17
+ * (e.g. undici `TypeError: terminated`) emitted between attempts.
18
+ */
19
+ export declare function createStaleRejectionGuard(): StaleRejectionGuard;
package/dist/esm/index.js CHANGED
@@ -4815,6 +4815,78 @@ class RetryPolicy {
4815
4815
  // Export a default policy instance
4816
4816
  const defaultRetryPolicy = new RetryPolicy();
4817
4817
 
4818
+ //
4819
+ //
4820
+ // Helpers
4821
+ //
4822
+ /**
4823
+ * Compare an unhandled rejection reason against errors the retry loop has
4824
+ * already caught. Reference equality first, then fall back to message + name
4825
+ * — providers sometimes surface twin rejections as fresh Error instances
4826
+ * rebuilt from the same upstream failure.
4827
+ */
4828
+ function matchesCaughtError(reason, caught) {
4829
+ if (caught.has(reason))
4830
+ return true;
4831
+ if (!(reason instanceof Error))
4832
+ return false;
4833
+ for (const handled of caught) {
4834
+ if (handled instanceof Error &&
4835
+ handled.name === reason.name &&
4836
+ handled.message === reason.message) {
4837
+ return true;
4838
+ }
4839
+ }
4840
+ return false;
4841
+ }
4842
+ //
4843
+ //
4844
+ // Main
4845
+ //
4846
+ /**
4847
+ * Create a guard that suppresses unhandled rejections firing as siblings of
4848
+ * an error the retry loop has already caught. Provider SDKs occasionally
4849
+ * surface a single upstream failure as twin rejections — the retry layer
4850
+ * accepts responsibility for the first; this guard prevents the second from
4851
+ * crashing the host while the retry is in flight.
4852
+ *
4853
+ * The guard also continues to suppress transient socket teardown errors
4854
+ * (e.g. undici `TypeError: terminated`) emitted between attempts.
4855
+ */
4856
+ function createStaleRejectionGuard() {
4857
+ const log = getLogger$5();
4858
+ const caughtErrors = new Set();
4859
+ let listener;
4860
+ return {
4861
+ install() {
4862
+ if (listener)
4863
+ return;
4864
+ listener = (reason, promise) => {
4865
+ if (isTransientNetworkError(reason)) {
4866
+ promise?.catch?.(() => { });
4867
+ log.trace("Suppressed stale socket error during retry");
4868
+ return;
4869
+ }
4870
+ if (matchesCaughtError(reason, caughtErrors)) {
4871
+ promise?.catch?.(() => { });
4872
+ log.trace("Suppressed sibling rejection of already-handled error");
4873
+ }
4874
+ };
4875
+ process.on("unhandledRejection", listener);
4876
+ },
4877
+ remove() {
4878
+ if (listener) {
4879
+ process.removeListener("unhandledRejection", listener);
4880
+ listener = undefined;
4881
+ }
4882
+ caughtErrors.clear();
4883
+ },
4884
+ recordCaught(error) {
4885
+ caughtErrors.add(error);
4886
+ },
4887
+ };
4888
+ }
4889
+
4818
4890
  //
4819
4891
  //
4820
4892
  // Main
@@ -4843,27 +4915,11 @@ class RetryExecutor {
4843
4915
  async execute(operation, options) {
4844
4916
  const log = getLogger$5();
4845
4917
  let attempt = 0;
4846
- // Persistent guard against stale socket errors (TypeError: terminated).
4847
- // Installed after the first abort and kept alive through subsequent attempts
4848
- // so that asynchronous undici socket teardown errors that fire between
4849
- // sleep completion and the next operation's await are caught.
4850
- let staleGuard;
4851
- const installGuard = () => {
4852
- if (staleGuard)
4853
- return;
4854
- staleGuard = (reason) => {
4855
- if (isTransientNetworkError(reason)) {
4856
- log.trace("Suppressed stale socket error during retry");
4857
- }
4858
- };
4859
- process.on("unhandledRejection", staleGuard);
4860
- };
4861
- const removeGuard = () => {
4862
- if (staleGuard) {
4863
- process.removeListener("unhandledRejection", staleGuard);
4864
- staleGuard = undefined;
4865
- }
4866
- };
4918
+ // Guard against stale rejections firing on a subsequent microtask after
4919
+ // the retry layer has already caught the originating error: undici socket
4920
+ // teardown (TypeError: terminated) and twin upstream-SDK rejections
4921
+ // (e.g. issue #336 OpenRouter SyntaxError siblings).
4922
+ const guard = createStaleRejectionGuard();
4867
4923
  try {
4868
4924
  while (true) {
4869
4925
  const controller = new AbortController();
@@ -4875,12 +4931,9 @@ class RetryExecutor {
4875
4931
  return result;
4876
4932
  }
4877
4933
  catch (error) {
4878
- // Abort the previous request to kill lingering socket callbacks
4879
4934
  controller.abort("retry");
4880
- // Install the guard immediately after abort — stale socket errors
4881
- // can fire on any subsequent microtask boundary (during hook calls,
4882
- // sleep, or the next operation attempt)
4883
- installGuard();
4935
+ guard.recordCaught(error);
4936
+ guard.install();
4884
4937
  // Check if we've exhausted retries
4885
4938
  if (!this.policy.shouldRetry(attempt)) {
4886
4939
  log.error(`API call failed after ${this.policy.maxRetries} retries`);
@@ -4926,7 +4979,7 @@ class RetryExecutor {
4926
4979
  }
4927
4980
  }
4928
4981
  finally {
4929
- removeGuard();
4982
+ guard.remove();
4930
4983
  }
4931
4984
  }
4932
4985
  }
@@ -5229,7 +5282,7 @@ class OperateLoop {
5229
5282
  };
5230
5283
  const toolResultFormatted = this.adapter.formatToolResult(toolCall, errorResult);
5231
5284
  state.responseBuilder.appendToHistory(toolResultFormatted);
5232
- log.error(`Error executing function call ${toolCall.name}`);
5285
+ log.warn(`Error executing function call ${toolCall.name}`);
5233
5286
  log.var({ error });
5234
5287
  // Track consecutive errors and stop if threshold reached
5235
5288
  state.consecutiveToolErrors++;
@@ -5536,25 +5589,10 @@ class StreamLoop {
5536
5589
  // Retry loop for connection-level failures
5537
5590
  let attempt = 0;
5538
5591
  let chunksYielded = false;
5539
- // Persistent guard against stale socket errors (TypeError: terminated).
5540
- // Installed after the first abort and kept alive through subsequent attempts.
5541
- let staleGuard;
5542
- const installGuard = () => {
5543
- if (staleGuard)
5544
- return;
5545
- staleGuard = (reason) => {
5546
- if (isTransientNetworkError(reason)) {
5547
- log.trace("Suppressed stale socket error during retry");
5548
- }
5549
- };
5550
- process.on("unhandledRejection", staleGuard);
5551
- };
5552
- const removeGuard = () => {
5553
- if (staleGuard) {
5554
- process.removeListener("unhandledRejection", staleGuard);
5555
- staleGuard = undefined;
5556
- }
5557
- };
5592
+ // Guard against stale rejections firing after the stream loop has already
5593
+ // caught the originating error: undici socket teardown and twin
5594
+ // upstream-SDK rejections (issue #336).
5595
+ const guard = createStaleRejectionGuard();
5558
5596
  try {
5559
5597
  while (true) {
5560
5598
  const controller = new AbortController();
@@ -5595,10 +5633,9 @@ class StreamLoop {
5595
5633
  break;
5596
5634
  }
5597
5635
  catch (error) {
5598
- // Abort the previous request to kill lingering socket callbacks
5599
5636
  controller.abort("retry");
5600
- // Install the guard immediately after abort
5601
- installGuard();
5637
+ guard.recordCaught(error);
5638
+ guard.install();
5602
5639
  // If chunks were already yielded, we can't transparently retry
5603
5640
  if (chunksYielded) {
5604
5641
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5631,7 +5668,7 @@ class StreamLoop {
5631
5668
  }
5632
5669
  }
5633
5670
  finally {
5634
- removeGuard();
5671
+ guard.remove();
5635
5672
  }
5636
5673
  // Execute afterEachModelResponse hook
5637
5674
  await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
@@ -5740,7 +5777,7 @@ class StreamLoop {
5740
5777
  call_id: toolCall.callId,
5741
5778
  name: toolCall.name,
5742
5779
  });
5743
- log.error(`Error executing function call ${toolCall.name}`);
5780
+ log.warn(`Error executing function call ${toolCall.name}`);
5744
5781
  log.var({ error });
5745
5782
  // Track consecutive errors and stop if threshold reached
5746
5783
  state.consecutiveToolErrors++;