@just-every/ensemble
Version:
LLM provider abstraction layer with unified streaming interface
124 lines • 3.89 kB
JavaScript
export const DEFAULT_RETRY_OPTIONS = {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
retryableErrors: new Set([
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND',
'ECONNREFUSED',
'EPIPE',
'EHOSTUNREACH',
'EAI_AGAIN',
'ENETUNREACH',
'ECONNABORTED',
'ESOCKETTIMEDOUT',
]),
retryableStatusCodes: new Set([
408,
429,
500,
502,
503,
504,
522,
524,
]),
};
export function isRetryableError(error, options = {}) {
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
if (error.code && opts.retryableErrors.has(error.code)) {
return true;
}
if (error.status && opts.retryableStatusCodes.has(error.status)) {
return true;
}
if (error.message &&
(error.message.includes('fetch failed') ||
error.message.includes('network error') ||
error.message.includes('ECONNRESET') ||
error.message.includes('ETIMEDOUT'))) {
return true;
}
if (error.message &&
(error.message.includes('Incomplete JSON segment') ||
error.message.includes('Connection error') ||
error.message.includes('Request timeout'))) {
return true;
}
return false;
}
export function calculateDelay(attempt, options = {}) {
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
const baseDelay = opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt - 1);
const delay = Math.min(baseDelay, opts.maxDelay);
const jitter = delay * 0.1 * (Math.random() * 2 - 1);
return Math.round(delay + jitter);
}
export async function retryWithBackoff(fn, options = {}) {
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
let lastError;
for (let attempt = 1; attempt <= opts.maxRetries + 1; attempt++) {
try {
return await fn();
}
catch (error) {
lastError = error;
if (!isRetryableError(error, opts) || attempt > opts.maxRetries) {
throw error;
}
const delay = calculateDelay(attempt, opts);
if (opts.onRetry) {
opts.onRetry(error, attempt);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
export async function* retryStreamWithBackoff(createStream, options = {}) {
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
let lastError;
const buffer = [];
let hasStartedYielding = false;
for (let attempt = 1; attempt <= opts.maxRetries + 1; attempt++) {
try {
const stream = createStream();
if (hasStartedYielding) {
let skipCount = buffer.length;
for await (const item of stream) {
if (skipCount > 0) {
skipCount--;
continue;
}
yield item;
}
}
else {
for await (const item of stream) {
buffer.push(item);
hasStartedYielding = true;
yield item;
}
}
return;
}
catch (error) {
lastError = error;
if (hasStartedYielding) {
throw error;
}
if (!isRetryableError(error, opts) || attempt > opts.maxRetries) {
throw error;
}
const delay = calculateDelay(attempt, opts);
if (opts.onRetry) {
opts.onRetry(error, attempt);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
//# sourceMappingURL=retry_handler.js.map