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.
- package/package.json +1 -1
- package/src/index.ts +67 -17
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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.
|
|
808
|
-
const waitMs = retryAfterHeaderMs(response.headers) ?? DEFAULT_RATE_LIMIT_WAIT_MS;
|
|
832
|
+
if (!response.ok) {
|
|
809
833
|
const message = await responseErrorMessage(response);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|