# Conflicts: # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md # docs/core-data-structures/llm-streaming.i18n.yaml # packages/llm/llm-retry/README.md # packages/llm/llm-retry/src/index.ts # packages/llm/llm-retry/tests/retry.spec.ts
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
resolveRetryPolicy,
|
|
RetryPolicySchema,
|
|
} from '@deepseek-ai/dsh-llm'
|
|
import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
|
|
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
|
|
|
describe('provider retry policy', () => {
|
|
it('resolves immutable normal defaults', () => {
|
|
const policy = resolveRetryPolicy(undefined, 'provider.retryPolicy')
|
|
|
|
expect(policy).toEqual({
|
|
mode: 'normal',
|
|
maxRetries: 2,
|
|
retryableCodes: ['EMPTY_RESPONSE', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT'],
|
|
initialDelayMs: 500,
|
|
maxDelayMs: 10_000,
|
|
jitterRatio: 0.1,
|
|
})
|
|
expect(Object.isFrozen(policy)).toBe(true)
|
|
if (policy.mode !== 'normal') throw new Error('expected normal policy')
|
|
expect(Object.isFrozen(policy.retryableCodes)).toBe(true)
|
|
})
|
|
|
|
it('resolves and detaches a configured normal policy', () => {
|
|
const retryableCodes = ['BUSY']
|
|
const config: RetryPolicyConfig = {
|
|
mode: 'normal',
|
|
maxRetries: 4,
|
|
retryableCodes,
|
|
backoff: {
|
|
initialDelayMs: 25,
|
|
maxDelayMs: 100,
|
|
jitterRatio: 0,
|
|
},
|
|
}
|
|
|
|
const policy = resolveRetryPolicy(config, 'provider.retryPolicy')
|
|
retryableCodes.push('LATE')
|
|
|
|
expect(policy).toEqual({
|
|
mode: 'normal',
|
|
maxRetries: 4,
|
|
retryableCodes: ['BUSY'],
|
|
initialDelayMs: 25,
|
|
maxDelayMs: 100,
|
|
jitterRatio: 0,
|
|
})
|
|
})
|
|
|
|
it('resolves always mode with default backoff', () => {
|
|
expect(resolveRetryPolicy({ mode: 'always' }, 'provider.retryPolicy')).toEqual({
|
|
mode: 'always',
|
|
initialDelayMs: 500,
|
|
maxDelayMs: 10_000,
|
|
jitterRatio: 0.1,
|
|
})
|
|
expect(RetryPolicySchema).toBeDefined()
|
|
})
|
|
|
|
it.each([
|
|
[{ mode: 'normal', maxRetries: -1 }, /maxRetries/],
|
|
[{ mode: 'normal', maxRetries: 1.5 }, /maxRetries/],
|
|
[{ mode: 'normal', maxRetries: Number.MAX_SAFE_INTEGER + 1 }, /maxRetries/],
|
|
[{ mode: 'always', backoff: { initialDelayMs: 0 } }, /initialDelayMs/],
|
|
[{ mode: 'normal', backoff: { maxDelayMs: Number.POSITIVE_INFINITY } }, /maxDelayMs/],
|
|
[{ mode: 'normal', backoff: { initialDelayMs: MAX_TIMER_DELAY_MS + 1 } }, /initialDelayMs/],
|
|
[{ mode: 'always', backoff: { maxDelayMs: MAX_TIMER_DELAY_MS + 1 } }, /maxDelayMs/],
|
|
[{ mode: 'normal', backoff: { initialDelayMs: 20, maxDelayMs: 10 } }, /less than or equal/],
|
|
[{ mode: 'always', backoff: { jitterRatio: 1.1 } }, /jitterRatio/],
|
|
[{ mode: 'normal', retryableCodes: [] }, /must not be empty/],
|
|
[{ mode: 'normal', retryableCodes: ['SERVER', 'SERVER'] }, /duplicates/],
|
|
[{ mode: 'normal', retryableCodes: [''] }, /non-empty strings/],
|
|
[{ mode: 'normal', retryableCodes: [429] }, /non-empty strings/],
|
|
[{ mode: 'normal', maxRetires: 1 }, /unknown key "maxRetires"/],
|
|
[{ mode: 'always', maxRetries: 1 }, /unknown key "maxRetries"/],
|
|
[{ mode: 'always', backoff: { initialDelay: 1 } }, /unknown key "initialDelay"/],
|
|
[{ mode: 'sometimes' }, /mode must be "normal" or "always"/],
|
|
] as const)('rejects invalid policy %#', (config, message) => {
|
|
expect(() => {
|
|
resolveRetryPolicy(config as unknown as RetryPolicyConfig, 'provider.retryPolicy')
|
|
}).toThrow(message)
|
|
})
|
|
})
|