oira666_pi-limits-wait 0.4.9 → 0.4.10

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +67 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-limits-wait",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Extension for PI that allows to automatically wait for limits to reset and retry the request",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -85,7 +85,7 @@ let ambientStatusCleanup: (() => void) | undefined;
85
85
  let modelStatusCleanup: (() => void) | undefined;
86
86
  let activeProviderRequests = 0;
87
87
  let activeFallbackProviderRequests = 0;
88
- let lastObservedRateLimitError: { at: number; message: string } | undefined;
88
+ let lastObservedHttpError: { at: number; message: string } | undefined;
89
89
  const wrappedApis = new Set<Api>();
90
90
  const builtinStreamSimpleByApi = new Map<Api, StreamSimpleFn>();
91
91
  let fallbackModels: FallbackModel[] = [];
@@ -355,11 +355,15 @@ function ensureRateLimitedModelsStatus(): void {
355
355
  };
356
356
  }
357
357
 
358
+ function notifyRetryableError(model: Model<Api>, retryable: RetryableError, errorMessage?: string): void {
359
+ const detail = errorMessage ? ` Error: ${formatErrorDetail(errorMessage)}` : "";
360
+ sharedCtx?.ui.notify(`${formatModel(model)} ${reasonLabel(retryable.reason).toLowerCase()} for ${formatDuration(retryable.waitMs)}.${detail}`, "warning");
361
+ }
362
+
358
363
  function rememberRateLimit(model: Model<Api>, retryable: RetryableError, errorMessage?: string): void {
359
364
  const deadline = Date.now() + retryable.waitMs;
360
365
  rateLimitMemory.set(modelKey(model), { reason: retryable.reason, limitedAt: Date.now(), deadline });
361
- const detail = errorMessage ? ` Error: ${formatErrorDetail(errorMessage)}` : "";
362
- sharedCtx?.ui.notify(`${formatModel(model)} ${reasonLabel(retryable.reason).toLowerCase()} for ${formatDuration(retryable.waitMs)}.${detail}`, "warning");
366
+ notifyRetryableError(model, retryable, errorMessage);
363
367
  ensureRateLimitedModelsStatus();
364
368
  }
365
369
 
@@ -432,10 +436,21 @@ function isAbortErrorEvent(event: AssistantMessageEvent, signal?: AbortSignal):
432
436
  return reason === "aborted" || stopReason === "aborted" || isAbortMessage(message);
433
437
  }
434
438
 
439
+ function recentObservedHttpError(maxAgeMs = 60_000): string | undefined {
440
+ if (!lastObservedHttpError) return undefined;
441
+ if (Date.now() - lastObservedHttpError.at > maxAgeMs) return undefined;
442
+ return lastObservedHttpError.message;
443
+ }
444
+
435
445
  function recentObservedRateLimitError(maxAgeMs = 60_000): string | undefined {
436
- if (!lastObservedRateLimitError) return undefined;
437
- if (Date.now() - lastObservedRateLimitError.at > maxAgeMs) return undefined;
438
- return lastObservedRateLimitError.message;
446
+ const message = recentObservedHttpError(maxAgeMs);
447
+ return message && isRateLimitError(message) ? message : undefined;
448
+ }
449
+
450
+ function errorMessageWithRecentHttpStatus(errorMessage: string): string {
451
+ const observed = recentObservedHttpError();
452
+ if (!observed || /(?:^|\D)[1-5]\d\d(?:\D|$)/.test(errorMessage) || errorMessage.includes(observed)) return errorMessage;
453
+ return `${observed}; Error: ${errorMessage}`;
439
454
  }
440
455
 
441
456
  function earliestCandidateDeadline(current: Model<Api>): number | undefined {
@@ -490,7 +505,7 @@ async function switchPiModel(entry: FallbackModel): Promise<void> {
490
505
 
491
506
  // ─── Retryable error detection ────────────────────────────────────────────────
492
507
 
493
- export function isRateLimitError(msg: string): boolean {
508
+ function isExplicitRateLimitError(msg: string): boolean {
494
509
  const lower = msg.toLowerCase();
495
510
  return (
496
511
  /(?:^|\D)429(?:\D|$)/.test(msg) ||
@@ -498,7 +513,14 @@ export function isRateLimitError(msg: string): boolean {
498
513
  /rate\s*limit/.test(lower) ||
499
514
  lower.includes("too many requests") ||
500
515
  lower.includes("quota exceeded") ||
501
- lower.includes("quota will reset") ||
516
+ lower.includes("quota will reset")
517
+ );
518
+ }
519
+
520
+ export function isRateLimitError(msg: string): boolean {
521
+ const lower = msg.toLowerCase();
522
+ return (
523
+ isExplicitRateLimitError(msg) ||
502
524
  lower.includes("retry delay") ||
503
525
  lower.includes("retry-after")
504
526
  );
@@ -629,6 +651,9 @@ export function getRetryableError(msg: string): RetryableError | undefined {
629
651
  if (isAuthenticationRefreshError(msg)) {
630
652
  return { reason: "authentication", waitMs: parseRetryDelayMs(msg) ?? DEFAULT_RATE_LIMIT_WAIT_MS };
631
653
  }
654
+ if (isTransientNetworkError(msg) && !isExplicitRateLimitError(msg)) {
655
+ return { reason: "network", waitMs: parseRetryDelayMs(msg) ?? DEFAULT_NETWORK_WAIT_MS };
656
+ }
632
657
  if (isRateLimitError(msg)) {
633
658
  return { reason: "rate-limit", waitMs: rateLimitWaitMs(msg) };
634
659
  }
@@ -804,13 +829,15 @@ function installFetchRateLimitObserver(): void {
804
829
  const response = await originalFetch(...args);
805
830
 
806
831
  if (activeProviderRequests > 0) {
807
- if (response.status === 429) {
808
- const waitMs = retryAfterHeaderMs(response.headers) ?? DEFAULT_RATE_LIMIT_WAIT_MS;
832
+ if (!response.ok) {
809
833
  const message = await responseErrorMessage(response);
810
- lastObservedRateLimitError = { at: Date.now(), message };
811
- showAmbientRetryStatus("rate-limit", waitMs);
812
- if (activeFallbackProviderRequests > 0) {
813
- throw new Error(message);
834
+ lastObservedHttpError = { at: Date.now(), message };
835
+ if (response.status === 429) {
836
+ const waitMs = retryAfterHeaderMs(response.headers) ?? DEFAULT_RATE_LIMIT_WAIT_MS;
837
+ showAmbientRetryStatus("rate-limit", waitMs);
838
+ if (activeFallbackProviderRequests > 0) {
839
+ throw new Error(message);
840
+ }
814
841
  }
815
842
  } else if (ambientStatusCleanup && response.ok) {
816
843
  clearAmbientRetryStatus();
@@ -849,7 +876,7 @@ export function __configureFallbackModelsForTests(
849
876
  primaryThinkingLevel = undefined;
850
877
  rateLimitMemory.clear();
851
878
  nonRetryableFailureMemory.clear();
852
- lastObservedRateLimitError = undefined;
879
+ lastObservedHttpError = undefined;
853
880
  ambientStatusCleanup?.();
854
881
  modelStatusCleanup?.();
855
882
  }
@@ -925,7 +952,8 @@ export function streamWithLimitsRetry(
925
952
  for await (const event of inner) {
926
953
  if (!committed) {
927
954
  if (event.type === "error") {
928
- const errMsg = event.error.errorMessage ?? "";
955
+ const rawErrMsg = event.error.errorMessage ?? "";
956
+ const errMsg = errorMessageWithRecentHttpStatus(rawErrMsg);
929
957
 
930
958
  if (isAbortErrorEvent(event, options?.signal)) {
931
959
  if (buffer.length > 0) {
@@ -990,7 +1018,7 @@ export function streamWithLimitsRetry(
990
1018
  }
991
1019
  }
992
1020
  } catch (err) {
993
- const errMsg = errorMessageWithCauses(err);
1021
+ const errMsg = errorMessageWithRecentHttpStatus(errorMessageWithCauses(err));
994
1022
  const aborted = Boolean(options?.signal?.aborted) || isAbortMessage(errMsg);
995
1023
  retryable = !aborted ? getRetryableError(errMsg) : undefined;
996
1024
  if (retryable) retryableErrorMessage = errMsg;
@@ -1120,6 +1148,28 @@ export function streamWithLimitsRetry(
1120
1148
  }
1121
1149
 
1122
1150
  if (allowFallback) {
1151
+ // Network/transport failures usually mean the user's connection dropped
1152
+ // or a socket timed out. Do not mark the model as unavailable, switch
1153
+ // to another model, or enter model-freeze handling; keep retrying the
1154
+ // same model with the short network backoff until it works again.
1155
+ if (retryable.reason === "network") {
1156
+ notifyRetryableError(attempt.model, retryable, retryableErrorMessage);
1157
+ const waitResult = await waitForRetry(retryable.reason, retryable.waitMs, options?.signal);
1158
+ if (waitResult === "aborted") {
1159
+ if (!committed) {
1160
+ output.push({ type: "start", partial: freshMessage(attempt.model) });
1161
+ committed = true;
1162
+ }
1163
+ const error = freshMessage(attempt.model);
1164
+ error.stopReason = "aborted";
1165
+ error.errorMessage = `Request aborted during ${reasonLabel(retryable.reason).toLowerCase()} wait.`;
1166
+ output.push({ type: "error", reason: "aborted", error });
1167
+ output.end();
1168
+ return;
1169
+ }
1170
+ continue;
1171
+ }
1172
+
1123
1173
  rememberRateLimit(attempt.model, retryable, retryableErrorMessage);
1124
1174
  const next = nextAvailableCandidate(attempt.model);
1125
1175
  if (next) {