fix: clarify provider retry delay contract

This commit is contained in:
Tianyi Cui
2026-07-20 18:38:22 +08:00
parent 3b0b0cefeb
commit 3293d56a06
17 changed files with 41 additions and 50 deletions

View File

@@ -76,18 +76,19 @@ function failureSnapshot(value: unknown): LlmFailure | undefined {
const message = candidate.message
const code = candidate.code
const status = candidate.status
const retryAfterMs = candidate.retryAfterMs
const providerRetryAfterMs = candidate.providerRetryAfterMs
const requestId = candidate.requestId
if (typeof message !== 'string' || message.length === 0
|| typeof code !== 'string' || code.length === 0
|| (status !== undefined && (!Number.isInteger(status) || status < 100 || status > 599))
|| (retryAfterMs !== undefined && (!Number.isFinite(retryAfterMs) || retryAfterMs <= 0))
|| (providerRetryAfterMs !== undefined
&& (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0))
|| (requestId !== undefined && (typeof requestId !== 'string' || requestId.length === 0))) return undefined
return Object.freeze({
message,
code,
...status === undefined ? {} : { status },
...retryAfterMs === undefined ? {} : { retryAfterMs },
...providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs },
...requestId === undefined ? {} : { requestId },
})
} catch (_sdkFailureGetter) {

View File

@@ -50,7 +50,7 @@ export interface LlmErrorOptions extends ErrorOptions {
/** Valid HTTP status observed at the provider boundary. */
status?: number
/** Positive finite provider-requested delay in milliseconds. */
retryAfterMs?: number
providerRetryAfterMs?: number
/** Non-empty opaque provider request id. */
requestId?: ProviderRequestId
}
@@ -75,9 +75,9 @@ export class LlmError extends HarnessError {
&& (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) {
throw new Error('LlmError status must be an integer from 100 through 599')
}
if (options?.retryAfterMs !== undefined
&& (!Number.isFinite(options.retryAfterMs) || options.retryAfterMs <= 0)) {
throw new Error('LlmError retryAfterMs must be a positive finite number')
if (options?.providerRetryAfterMs !== undefined
&& (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) {
throw new Error('LlmError providerRetryAfterMs must be a positive finite number')
}
if (options?.requestId !== undefined
&& (typeof options.requestId !== 'string' || options.requestId.length === 0)) {
@@ -89,7 +89,7 @@ export class LlmError extends HarnessError {
message,
code,
...options?.status === undefined ? {} : { status: options.status },
...options?.retryAfterMs === undefined ? {} : { retryAfterMs: options.retryAfterMs },
...options?.providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
...options?.requestId === undefined ? {} : { requestId: options.requestId },
})
}

View File

@@ -16,7 +16,7 @@ export interface LlmFailure {
/** HTTP status observed at the provider boundary, when available. */
readonly status?: number
/** Provider-requested delay in milliseconds, when valid and available. */
readonly retryAfterMs?: number
readonly providerRetryAfterMs?: number
/** Opaque provider-issued request identifier for diagnostics. */
readonly requestId?: ProviderRequestId
}

View File

@@ -191,7 +191,7 @@ describe('LlmService', () => {
it('keeps structured provider facts beside a frozen third-party Error', async () => {
const original = new LlmError('provider busy', 'RATE_LIMIT', {
status: 429,
retryAfterMs: 1_500,
providerRetryAfterMs: 1_500,
requestId: ProviderRequestId('req-7'),
})
Object.freeze(original)
@@ -212,7 +212,7 @@ describe('LlmService', () => {
message: 'provider busy',
code: 'RATE_LIMIT',
status: 429,
retryAfterMs: 1_500,
providerRetryAfterMs: 1_500,
requestId: ProviderRequestId('req-7'),
})
})
@@ -747,7 +747,8 @@ describe('LlmService', () => {
it('rejects non-serializable structured failure facts at construction', () => {
expect(() => new LlmError('busy', 'RATE_LIMIT', { status: 42 })).toThrow(/status/)
expect(() => new LlmError('busy', 'RATE_LIMIT', { retryAfterMs: Number.NaN })).toThrow(/retryAfterMs/)
expect(() => new LlmError('busy', 'RATE_LIMIT', { providerRetryAfterMs: Number.NaN }))
.toThrow(/providerRetryAfterMs/)
expect(() => new LlmError('busy', 'RATE_LIMIT', { requestId: ProviderRequestId('') })).toThrow(/requestId/)
expect(() => new LlmError(1 as never, 'RATE_LIMIT')).toThrow(/message/)
expect(() => new LlmError('busy', 1 as never)).toThrow(/code/)