-
Notifications
You must be signed in to change notification settings - Fork 388
Expand file tree
/
Copy pathretry_utils.ts
More file actions
70 lines (65 loc) · 2.82 KB
/
Copy pathretry_utils.ts
File metadata and controls
70 lines (65 loc) · 2.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 判断是否可重试(429 / 5xx / 网络错误,不含 4xx 客户端错误)
export function isRetryableError(e: Error): boolean {
const msg = e.message;
return /429|5\d\d|network|fetch|ECONNRESET/i.test(msg) && !/40[0134]/.test(msg);
}
// 指数退避重试,aborted 时立即退出
// delayFn 仅供测试注入,生产代码不传
export async function withRetry<T>(
fn: () => Promise<T>,
signal: AbortSignal,
maxRetries = 3,
delayFn?: (ms: number, signal: AbortSignal) => Promise<void>
): Promise<T> {
const wait =
delayFn ??
((ms, sig) =>
new Promise<void>((r) => {
const t = setTimeout(r, ms);
sig.addEventListener(
"abort",
() => {
clearTimeout(t);
r();
},
{ once: true }
);
}));
let lastError!: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
if (signal.aborted) throw lastError ?? new Error("Aborted");
try {
return await fn();
} catch (e: any) {
if (signal.aborted) throw e;
lastError = e;
if (!isRetryableError(e) || attempt === maxRetries) throw e;
const delay = 1000 * Math.pow(2, attempt) + Math.random() * 1000;
await wait(delay, signal);
}
}
throw lastError;
}
// provider 侧因上下文过长拒绝请求时的常见措辞(OpenAI/Anthropic 及兼容实现)。
// 本地的字节数估算是保守启发式,可能低估真实 token 数:估算认为"能放下"
// 而放行的请求仍可能被 provider 真正拒绝。没有逐 provider 精确计数的前提下,把这类错误
// 识别出来并归到与本地预判一致的 errorCode,是唯一可行的兜底恢复路径——至少能让调用方
// (UI/自动压缩)用同一套"上下文超限"处理逻辑响应,而不是当成不透明的 api_error。
const CONTEXT_LENGTH_ERROR_PATTERN =
/context.{0,20}(length|window|too long|exceed)|exceed.{0,20}context|maximum context length|too many tokens|prompt is too long|input is too long/i;
const STRUCTURED_ERROR_CODES = new Set(["context_too_large", "persist_indeterminate", "tool_timeout"]);
// 将 Error 分类为 errorCode 字符串
export function classifyErrorCode(e: Error): string {
// 受信任的内部错误码比错误文案更精确;未知值不能直接透传,避免把外部错误对象的任意字段
// 当作对外协议错误码。
const structuredErrorCode = (e as Error & { errorCode?: unknown }).errorCode;
if (typeof structuredErrorCode === "string" && STRUCTURED_ERROR_CODES.has(structuredErrorCode)) {
return structuredErrorCode;
}
const msg = e.message;
if (CONTEXT_LENGTH_ERROR_PATTERN.test(msg)) return "context_too_large";
if (/429/.test(msg)) return "rate_limit";
if (/401|403/.test(msg)) return "auth";
if (/timed out/.test(msg)) return "tool_timeout";
return "api_error";
}