/** * 重试纯函数 —— 瞬态错误判定 + 指数退避计算。 * * 零副作用、零依赖:被本包 `withRetry` 与 `@drama/llm` 的网关编排层调用,可独立单测。 * 不在此处 sleep、不读时钟、不发请求;只做判定与算术。 */ /** @typedef {'auth'|'rateLimit'|'transient'|'badRequest'|'timeout'|'aborted'|'unknown'} CallErrorReason */ /** * 默认重试策略。调用方可在包装时覆盖。 * @typedef {object} RetryPolicy * @property {number} maxRetries 最大重试次数(不含首次尝试)。 * @property {number} baseDelayMs 首次退避基数。 * @property {number} maxDelayMs 退避上限。 * @property {number} factor 指数因子。 */ /** @type {RetryPolicy} */ export const DEFAULT_RETRY_POLICY = { maxRetries: 2, baseDelayMs: 500, maxDelayMs: 8000, factor: 2, } /** * 该 reason 是否属于「可重试的瞬态类」。 * rateLimit / transient / timeout = true;auth / badRequest / aborted / unknown = false。 * @param {CallErrorReason} reason * @returns {boolean} */ export function isRetryableReason(reason) { return reason === 'rateLimit' || reason === 'transient' || reason === 'timeout' } /** * 在给定策略下,第 `attempt` 次失败后是否还应再试。 * @param {number} attempt 已失败的次数(首次失败时为 1)。 * @param {boolean} retryable 该错误是否可重试。 * @param {RetryPolicy} [policy] * @returns {boolean} */ export function shouldRetry(attempt, retryable, policy = DEFAULT_RETRY_POLICY) { if (!retryable) return false return attempt <= policy.maxRetries } /** * 第 `attempt` 次重试前的退避毫秒数(指数退避 + 抖动可选)。 * attempt 从 1 起算(第一次重试)。`retryAfterMs` 若由 429 头给出则优先采用。 * @param {number} attempt * @param {RetryPolicy} [policy] * @param {number} [retryAfterMs] 厂商显式给出的等待时间(如 Retry-After)。 * @returns {number} */ export function backoffDelayMs(attempt, policy = DEFAULT_RETRY_POLICY, retryAfterMs) { if (typeof retryAfterMs === 'number' && retryAfterMs >= 0) { return Math.min(retryAfterMs, policy.maxDelayMs) } const raw = policy.baseDelayMs * Math.pow(policy.factor, attempt - 1) return Math.min(raw, policy.maxDelayMs) }