@earendil-works/pi-ai
Version:
Unified LLM API with automatic model discovery and provider configuration
97 lines • 4.13 kB
JavaScript
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
function isProviderError(error) {
if (!(error instanceof Error) || !("status" in error) || !("headers" in error))
return false;
return ((error.status === undefined || typeof error.status === "number") &&
(error.headers === undefined || error.headers instanceof Headers));
}
/** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */
function isRetryableProviderError(error) {
const shouldRetry = error.headers?.get("x-should-retry");
if (shouldRetry === "true")
return true;
if (shouldRetry === "false")
return false;
if (error.status === undefined)
return true;
return (error.status === 408 ||
error.status === 409 ||
error.status === 429 ||
(typeof error.status === "number" && error.status >= 500));
}
function validateServerRetryDelayMs(delayMs, maxRetryDelayMs, providerErrorMessage) {
const maxDelayMs = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
if (maxDelayMs > 0 && delayMs > maxDelayMs) {
throw new Error(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${providerErrorMessage}`);
}
return delayMs;
}
function getRetryDelayMs(error, retryIndex, maxRetryDelayMs) {
const retryAfterMs = error.headers?.get("retry-after-ms");
if (retryAfterMs) {
const value = Number.parseFloat(retryAfterMs);
if (Number.isFinite(value))
return validateServerRetryDelayMs(value, maxRetryDelayMs, error.message);
}
const retryAfter = error.headers?.get("retry-after");
if (retryAfter) {
const seconds = Number.parseFloat(retryAfter);
const delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
if (Number.isFinite(delayMs))
return validateServerRetryDelayMs(delayMs, maxRetryDelayMs, error.message);
}
const exponentialDelay = Math.min(0.5 * 2 ** retryIndex, 8) * 1000;
return exponentialDelay * (1 - Math.random() * 0.25);
}
function createAbortError() {
const error = new Error("Request aborted");
error.name = "AbortError";
return error;
}
function abortableSleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(createAbortError());
return;
}
const onAbort = () => {
clearTimeout(timeout);
reject(createAbortError());
};
const timeout = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, Math.max(0, ms));
signal?.addEventListener("abort", onAbort, { once: true });
});
}
/**
* Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making
* their backoff sleep interruptible. Their built-in retry timers ignore the
* request AbortSignal, so callers must invoke the SDK with `maxRetries: 0` and
* wrap the request with this helper. Provider-requested delays above
* `maxRetryDelayMs` fail immediately (60 seconds by default); set it to zero to
* disable the limit.
*/
export async function retryProviderRequest(request, options = {}) {
const maxRetries = options.maxRetries ?? 0;
let retriesRemaining = maxRetries;
for (;;) {
try {
// Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.
return await request();
}
catch (error) {
if (options.signal?.aborted)
throw createAbortError();
if (retriesRemaining <= 0 || !isProviderError(error) || !isRetryableProviderError(error))
throw error;
if (error.status !== undefined && options.noRetryStatuses?.includes(error.status))
throw error;
const retryIndex = maxRetries - retriesRemaining;
retriesRemaining--;
await abortableSleep(getRetryDelayMs(error, retryIndex, options.maxRetryDelayMs), options.signal);
}
}
}
//# sourceMappingURL=provider-retry.js.map