mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-19 22:24:40 +00:00
133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
import { sleep } from '@antfu/utils';
|
|
import * as retry from 'retry';
|
|
|
|
import type { Logger } from '#/logging/types';
|
|
|
|
import { abortable } from '../utils/abort';
|
|
import type { LoopEventDispatcher } from './events';
|
|
import { isAbortError } from './errors';
|
|
import type { LLM, LLMChatParams, LLMChatResponse } from './llm';
|
|
|
|
export const DEFAULT_MAX_RETRY_ATTEMPTS = 3;
|
|
|
|
const RETRY_MIN_TIMEOUT_MS = 300;
|
|
const RETRY_MAX_TIMEOUT_MS = 5000;
|
|
const RETRY_FACTOR = 2;
|
|
|
|
export interface ChatWithRetryInput {
|
|
readonly llm: LLM;
|
|
readonly params: LLMChatParams;
|
|
readonly dispatchEvent: LoopEventDispatcher;
|
|
readonly turnId: string;
|
|
readonly currentStep: number;
|
|
readonly stepUuid: string;
|
|
readonly maxAttempts?: number;
|
|
readonly log?: Logger | undefined;
|
|
}
|
|
|
|
export async function chatWithRetry(input: ChatWithRetryInput): Promise<LLMChatResponse> {
|
|
const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_RETRY_ATTEMPTS;
|
|
|
|
if (input.llm.isRetryableError === undefined || maxAttempts <= 1) {
|
|
const effectiveMaxAttempts = Math.max(maxAttempts, 1);
|
|
try {
|
|
return await input.llm.chat(paramsForAttempt(input, 1, effectiveMaxAttempts));
|
|
} catch (error) {
|
|
logRequestFailure(input, error, 1, effectiveMaxAttempts);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const delays = retryBackoffDelays(maxAttempts);
|
|
|
|
for (let attempt = 1; ; attempt += 1) {
|
|
try {
|
|
return await input.llm.chat(paramsForAttempt(input, attempt, maxAttempts));
|
|
} catch (error) {
|
|
if (attempt >= maxAttempts || !input.llm.isRetryableError(error)) {
|
|
logRequestFailure(input, error, attempt, maxAttempts);
|
|
throw error;
|
|
}
|
|
|
|
const delayMs = delays[attempt - 1] ?? 0;
|
|
input.params.signal.throwIfAborted();
|
|
input.dispatchEvent({
|
|
type: 'step.retrying',
|
|
turnId: input.turnId,
|
|
step: input.currentStep,
|
|
stepUuid: input.stepUuid,
|
|
failedAttempt: attempt,
|
|
nextAttempt: attempt + 1,
|
|
maxAttempts,
|
|
delayMs,
|
|
...retryErrorFields(error),
|
|
});
|
|
await sleepForRetry(delayMs, input.params.signal);
|
|
}
|
|
}
|
|
}
|
|
|
|
function logRequestFailure(
|
|
input: ChatWithRetryInput,
|
|
error: unknown,
|
|
attempt: number,
|
|
maxAttempts: number,
|
|
): void {
|
|
if (isAbortError(error) || input.params.signal.aborted) return;
|
|
input.log?.warn('llm request failed', {
|
|
turnStep: `${input.turnId}.${String(input.currentStep)}`,
|
|
attempt: `${String(attempt)}/${String(maxAttempts)}`,
|
|
model: input.llm.modelName,
|
|
...retryErrorFields(error),
|
|
});
|
|
}
|
|
|
|
function paramsForAttempt(
|
|
input: ChatWithRetryInput,
|
|
attempt: number,
|
|
maxAttempts: number,
|
|
): LLMChatParams {
|
|
const turnStep = `${input.turnId}.${String(input.currentStep)}`;
|
|
return {
|
|
...input.params,
|
|
requestLogFields:
|
|
attempt === 1
|
|
? { turnStep }
|
|
: { turnStep, attempt: `${String(attempt)}/${String(maxAttempts)}` },
|
|
};
|
|
}
|
|
|
|
export function retryBackoffDelays(maxAttempts: number): number[] {
|
|
return retry.timeouts({
|
|
retries: Math.max(maxAttempts - 1, 0),
|
|
minTimeout: RETRY_MIN_TIMEOUT_MS,
|
|
maxTimeout: RETRY_MAX_TIMEOUT_MS,
|
|
factor: RETRY_FACTOR,
|
|
randomize: true,
|
|
});
|
|
}
|
|
|
|
export async function sleepForRetry(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
signal.throwIfAborted();
|
|
await abortable(sleep(delayMs), signal);
|
|
}
|
|
|
|
interface RetryErrorFields {
|
|
readonly errorName: string;
|
|
readonly errorMessage: string;
|
|
readonly statusCode?: number;
|
|
}
|
|
|
|
function retryErrorFields(error: unknown): RetryErrorFields {
|
|
return {
|
|
errorName: error instanceof Error ? error.name : typeof error,
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
statusCode: maybeStatusCode(error),
|
|
};
|
|
}
|
|
|
|
function maybeStatusCode(error: unknown): number | undefined {
|
|
if (typeof error !== 'object' || error === null) return undefined;
|
|
const statusCode = (error as { statusCode?: unknown }).statusCode;
|
|
return typeof statusCode === 'number' ? statusCode : undefined;
|
|
}
|