@kolisachint/hoocode-ai 0.5.47 → 0.5.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +7 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.js +3 -1
- package/dist/providers/azure-openai-responses.js.map +1 -1
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +5 -3
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +5 -3
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/utils/retry-delay.d.ts +85 -0
- package/dist/utils/retry-delay.d.ts.map +1 -0
- package/dist/utils/retry-delay.js +163 -0
- package/dist/utils/retry-delay.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-requested retry delays, and the cap that keeps them sane.
|
|
3
|
+
*
|
|
4
|
+
* A provider that has run out of quota answers `429` with a `Retry-After`
|
|
5
|
+
* naming when the quota resets — which for a monthly plan is weeks away, not
|
|
6
|
+
* seconds. The vendor SDKs take that header literally and sleep for it, and a
|
|
7
|
+
* delay that large does not survive the trip: `setTimeout` holds a signed
|
|
8
|
+
* 32-bit millisecond count, so anything past ~24.9 days is silently clamped to
|
|
9
|
+
* 1ms. The "wait 28 days" becomes "retry immediately", the retry draws another
|
|
10
|
+
* 429, and the SDK burns its whole retry budget in a few milliseconds against a
|
|
11
|
+
* quota that will not move for a month.
|
|
12
|
+
*
|
|
13
|
+
* So a requested delay past the cap is not something to wait out — it is a
|
|
14
|
+
* different kind of failure, and the caller needs to see it rather than sit in
|
|
15
|
+
* a hot loop behind it.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The largest delay a timer can actually hold.
|
|
19
|
+
*
|
|
20
|
+
* `setTimeout` stores its delay in a signed 32-bit integer. Larger values do
|
|
21
|
+
* not throw: Node warns and substitutes 1ms, turning the longest wait into the
|
|
22
|
+
* shortest one.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_TIMER_DELAY_MS: number;
|
|
25
|
+
/** Longest server-requested delay worth waiting out, when the caller names none. */
|
|
26
|
+
export declare const DEFAULT_MAX_RETRY_DELAY_MS = 60000;
|
|
27
|
+
/** Anything with a `get` — a `Headers`, or an SDK error's header bag. */
|
|
28
|
+
interface HeaderBag {
|
|
29
|
+
get(name: string): string | null | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The delay a response asks for, in milliseconds, or undefined if it asks for
|
|
33
|
+
* none.
|
|
34
|
+
*
|
|
35
|
+
* Mirrors what the OpenAI and Anthropic SDKs do with these headers, because the
|
|
36
|
+
* point is to predict the sleep they are about to take: `retry-after-ms` wins,
|
|
37
|
+
* then `retry-after` as seconds, then `retry-after` as an HTTP date.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseRetryAfterMs(headers: HeaderBag | undefined): number | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* A duration as a person would say it: the two largest units that carry
|
|
42
|
+
* meaning, because "28d 15h" tells you to go do something else and
|
|
43
|
+
* "2472352s" does not.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatDelay(ms: number): string;
|
|
46
|
+
/**
|
|
47
|
+
* Wrap `fetch` so a response asking to wait longer than `maxRetryDelayMs` is
|
|
48
|
+
* marked as one the SDK must not retry.
|
|
49
|
+
*
|
|
50
|
+
* `x-should-retry: false` is the escape hatch both the OpenAI and Anthropic
|
|
51
|
+
* clients check before anything else, so stamping it is enough to stop the
|
|
52
|
+
* retry loop before it computes an unholdable sleep. The response is otherwise
|
|
53
|
+
* passed through untouched — same status, same body — so the error the caller
|
|
54
|
+
* finally sees is still the provider's own.
|
|
55
|
+
*
|
|
56
|
+
* A cap of zero (or a nonsensical one) disables the wrapper entirely, which is
|
|
57
|
+
* what the documented "set to 0 to disable" means.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRetryDelayCapFetch(maxRetryDelayMs: number, baseFetch?: typeof fetch): typeof fetch;
|
|
60
|
+
/**
|
|
61
|
+
* The `fetch` override to spread into an SDK client's options, or nothing when
|
|
62
|
+
* no cap applies.
|
|
63
|
+
*
|
|
64
|
+
* Spread rather than assigned so a disabled cap leaves the key absent
|
|
65
|
+
* altogether: passing `fetch: undefined` explicitly would override the client's
|
|
66
|
+
* own default instead of leaving it alone.
|
|
67
|
+
*/
|
|
68
|
+
export declare function retryDelayCapFetch(maxRetryDelayMs: number | undefined): {
|
|
69
|
+
fetch?: typeof fetch;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The provider's error message, plus how long it wants us gone.
|
|
73
|
+
*
|
|
74
|
+
* A bare "429 quota exceeded" is the same sentence whether the quota returns in
|
|
75
|
+
* thirty seconds or four weeks, and those call for opposite responses from the
|
|
76
|
+
* person reading it. The wait is already on the error; it just was never shown.
|
|
77
|
+
*/
|
|
78
|
+
export declare function describeProviderError(error: unknown, maxRetryDelayMs?: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* Whether an error message describes a wait too long to sit through — the
|
|
81
|
+
* signal that retrying is pointless rather than merely slow.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isLongRetryDelayError(message: string | undefined): boolean;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=retry-delay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-delay.d.ts","sourceRoot":"","sources":["../../src/utils/retry-delay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C,oFAAoF;AACpF,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAKjD,2EAAyE;AACzE,UAAU,SAAS;IAClB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC7C;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAkBpF;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAsB9C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,KAAK,CAqB/G;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CAAE,CAIhG;AAUD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,eAAe,SAA6B,GAAG,MAAM,CAO1G;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE1E","sourcesContent":["/**\n * Server-requested retry delays, and the cap that keeps them sane.\n *\n * A provider that has run out of quota answers `429` with a `Retry-After`\n * naming when the quota resets — which for a monthly plan is weeks away, not\n * seconds. The vendor SDKs take that header literally and sleep for it, and a\n * delay that large does not survive the trip: `setTimeout` holds a signed\n * 32-bit millisecond count, so anything past ~24.9 days is silently clamped to\n * 1ms. The \"wait 28 days\" becomes \"retry immediately\", the retry draws another\n * 429, and the SDK burns its whole retry budget in a few milliseconds against a\n * quota that will not move for a month.\n *\n * So a requested delay past the cap is not something to wait out — it is a\n * different kind of failure, and the caller needs to see it rather than sit in\n * a hot loop behind it.\n */\n\n/**\n * The largest delay a timer can actually hold.\n *\n * `setTimeout` stores its delay in a signed 32-bit integer. Larger values do\n * not throw: Node warns and substitutes 1ms, turning the longest wait into the\n * shortest one.\n */\nexport const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;\n\n/** Longest server-requested delay worth waiting out, when the caller names none. */\nexport const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\n\n/** Phrase every capped-delay message carries, so callers can recognise one. */\nconst LONG_DELAY_MARKER = \"asked to wait\";\n\n/** Anything with a `get` — a `Headers`, or an SDK error's header bag. */\ninterface HeaderBag {\n\tget(name: string): string | null | undefined;\n}\n\n/**\n * The delay a response asks for, in milliseconds, or undefined if it asks for\n * none.\n *\n * Mirrors what the OpenAI and Anthropic SDKs do with these headers, because the\n * point is to predict the sleep they are about to take: `retry-after-ms` wins,\n * then `retry-after` as seconds, then `retry-after` as an HTTP date.\n */\nexport function parseRetryAfterMs(headers: HeaderBag | undefined): number | undefined {\n\tif (!headers) return undefined;\n\n\tconst afterMs = headers.get(\"retry-after-ms\");\n\tif (afterMs) {\n\t\tconst ms = Number.parseFloat(afterMs);\n\t\tif (!Number.isNaN(ms)) return ms;\n\t}\n\n\tconst after = headers.get(\"retry-after\");\n\tif (!after) return undefined;\n\n\tconst seconds = Number.parseFloat(after);\n\tif (!Number.isNaN(seconds)) return seconds * 1000;\n\n\t// The header's other legal form is an HTTP date.\n\tconst at = Date.parse(after);\n\treturn Number.isNaN(at) ? undefined : at - Date.now();\n}\n\n/**\n * A duration as a person would say it: the two largest units that carry\n * meaning, because \"28d 15h\" tells you to go do something else and\n * \"2472352s\" does not.\n */\nexport function formatDelay(ms: number): string {\n\tif (!Number.isFinite(ms) || ms < 0) return \"an unknown time\";\n\n\tconst totalSeconds = Math.round(ms / 1000);\n\tif (totalSeconds < 60) return `${totalSeconds}s`;\n\n\tconst units: Array<[number, string]> = [\n\t\t[86400, \"d\"],\n\t\t[3600, \"h\"],\n\t\t[60, \"m\"],\n\t\t[1, \"s\"],\n\t];\n\n\tconst parts: string[] = [];\n\tlet remaining = totalSeconds;\n\tfor (const [size, suffix] of units) {\n\t\tconst value = Math.floor(remaining / size);\n\t\tremaining -= value * size;\n\t\tif (value > 0) parts.push(`${value}${suffix}`);\n\t\tif (parts.length === 2) break;\n\t}\n\treturn parts.join(\" \");\n}\n\n/**\n * Wrap `fetch` so a response asking to wait longer than `maxRetryDelayMs` is\n * marked as one the SDK must not retry.\n *\n * `x-should-retry: false` is the escape hatch both the OpenAI and Anthropic\n * clients check before anything else, so stamping it is enough to stop the\n * retry loop before it computes an unholdable sleep. The response is otherwise\n * passed through untouched — same status, same body — so the error the caller\n * finally sees is still the provider's own.\n *\n * A cap of zero (or a nonsensical one) disables the wrapper entirely, which is\n * what the documented \"set to 0 to disable\" means.\n */\nexport function createRetryDelayCapFetch(maxRetryDelayMs: number, baseFetch: typeof fetch = fetch): typeof fetch {\n\tif (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs <= 0) return baseFetch;\n\n\treturn async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]): Promise<Response> => {\n\t\tconst response = await baseFetch(input, init);\n\n\t\t// Only a failure is ever retried, and a server that already stated its\n\t\t// own preference outranks the cap.\n\t\tif (response.ok || response.headers.get(\"x-should-retry\") !== null) return response;\n\n\t\tconst delayMs = parseRetryAfterMs(response.headers);\n\t\tif (delayMs === undefined || delayMs <= maxRetryDelayMs) return response;\n\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.set(\"x-should-retry\", \"false\");\n\t\treturn new Response(response.body, {\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t\theaders,\n\t\t});\n\t};\n}\n\n/**\n * The `fetch` override to spread into an SDK client's options, or nothing when\n * no cap applies.\n *\n * Spread rather than assigned so a disabled cap leaves the key absent\n * altogether: passing `fetch: undefined` explicitly would override the client's\n * own default instead of leaving it alone.\n */\nexport function retryDelayCapFetch(maxRetryDelayMs: number | undefined): { fetch?: typeof fetch } {\n\tif (maxRetryDelayMs === undefined) return {};\n\tconst capped = createRetryDelayCapFetch(maxRetryDelayMs);\n\treturn capped === fetch ? {} : { fetch: capped };\n}\n\n/** The header bag an SDK error carries, if it is that kind of error. */\nfunction errorHeaders(error: unknown): HeaderBag | undefined {\n\tif (!error || typeof error !== \"object\") return undefined;\n\tconst headers = (error as { headers?: unknown }).headers;\n\tif (!headers || typeof headers !== \"object\") return undefined;\n\treturn typeof (headers as HeaderBag).get === \"function\" ? (headers as HeaderBag) : undefined;\n}\n\n/**\n * The provider's error message, plus how long it wants us gone.\n *\n * A bare \"429 quota exceeded\" is the same sentence whether the quota returns in\n * thirty seconds or four weeks, and those call for opposite responses from the\n * person reading it. The wait is already on the error; it just was never shown.\n */\nexport function describeProviderError(error: unknown, maxRetryDelayMs = DEFAULT_MAX_RETRY_DELAY_MS): string {\n\tconst message = error instanceof Error ? error.message : JSON.stringify(error);\n\n\tconst delayMs = parseRetryAfterMs(errorHeaders(error));\n\tif (delayMs === undefined || delayMs <= Math.max(0, maxRetryDelayMs)) return message;\n\n\treturn `${message} (the provider ${LONG_DELAY_MARKER} ${formatDelay(delayMs)} before retrying, so no retry was attempted)`;\n}\n\n/**\n * Whether an error message describes a wait too long to sit through — the\n * signal that retrying is pointless rather than merely slow.\n */\nexport function isLongRetryDelayError(message: string | undefined): boolean {\n\treturn message?.includes(`provider ${LONG_DELAY_MARKER}`) ?? false;\n}\n"]}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-requested retry delays, and the cap that keeps them sane.
|
|
3
|
+
*
|
|
4
|
+
* A provider that has run out of quota answers `429` with a `Retry-After`
|
|
5
|
+
* naming when the quota resets — which for a monthly plan is weeks away, not
|
|
6
|
+
* seconds. The vendor SDKs take that header literally and sleep for it, and a
|
|
7
|
+
* delay that large does not survive the trip: `setTimeout` holds a signed
|
|
8
|
+
* 32-bit millisecond count, so anything past ~24.9 days is silently clamped to
|
|
9
|
+
* 1ms. The "wait 28 days" becomes "retry immediately", the retry draws another
|
|
10
|
+
* 429, and the SDK burns its whole retry budget in a few milliseconds against a
|
|
11
|
+
* quota that will not move for a month.
|
|
12
|
+
*
|
|
13
|
+
* So a requested delay past the cap is not something to wait out — it is a
|
|
14
|
+
* different kind of failure, and the caller needs to see it rather than sit in
|
|
15
|
+
* a hot loop behind it.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The largest delay a timer can actually hold.
|
|
19
|
+
*
|
|
20
|
+
* `setTimeout` stores its delay in a signed 32-bit integer. Larger values do
|
|
21
|
+
* not throw: Node warns and substitutes 1ms, turning the longest wait into the
|
|
22
|
+
* shortest one.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
25
|
+
/** Longest server-requested delay worth waiting out, when the caller names none. */
|
|
26
|
+
export const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
|
|
27
|
+
/** Phrase every capped-delay message carries, so callers can recognise one. */
|
|
28
|
+
const LONG_DELAY_MARKER = "asked to wait";
|
|
29
|
+
/**
|
|
30
|
+
* The delay a response asks for, in milliseconds, or undefined if it asks for
|
|
31
|
+
* none.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors what the OpenAI and Anthropic SDKs do with these headers, because the
|
|
34
|
+
* point is to predict the sleep they are about to take: `retry-after-ms` wins,
|
|
35
|
+
* then `retry-after` as seconds, then `retry-after` as an HTTP date.
|
|
36
|
+
*/
|
|
37
|
+
export function parseRetryAfterMs(headers) {
|
|
38
|
+
if (!headers)
|
|
39
|
+
return undefined;
|
|
40
|
+
const afterMs = headers.get("retry-after-ms");
|
|
41
|
+
if (afterMs) {
|
|
42
|
+
const ms = Number.parseFloat(afterMs);
|
|
43
|
+
if (!Number.isNaN(ms))
|
|
44
|
+
return ms;
|
|
45
|
+
}
|
|
46
|
+
const after = headers.get("retry-after");
|
|
47
|
+
if (!after)
|
|
48
|
+
return undefined;
|
|
49
|
+
const seconds = Number.parseFloat(after);
|
|
50
|
+
if (!Number.isNaN(seconds))
|
|
51
|
+
return seconds * 1000;
|
|
52
|
+
// The header's other legal form is an HTTP date.
|
|
53
|
+
const at = Date.parse(after);
|
|
54
|
+
return Number.isNaN(at) ? undefined : at - Date.now();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A duration as a person would say it: the two largest units that carry
|
|
58
|
+
* meaning, because "28d 15h" tells you to go do something else and
|
|
59
|
+
* "2472352s" does not.
|
|
60
|
+
*/
|
|
61
|
+
export function formatDelay(ms) {
|
|
62
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
63
|
+
return "an unknown time";
|
|
64
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
65
|
+
if (totalSeconds < 60)
|
|
66
|
+
return `${totalSeconds}s`;
|
|
67
|
+
const units = [
|
|
68
|
+
[86400, "d"],
|
|
69
|
+
[3600, "h"],
|
|
70
|
+
[60, "m"],
|
|
71
|
+
[1, "s"],
|
|
72
|
+
];
|
|
73
|
+
const parts = [];
|
|
74
|
+
let remaining = totalSeconds;
|
|
75
|
+
for (const [size, suffix] of units) {
|
|
76
|
+
const value = Math.floor(remaining / size);
|
|
77
|
+
remaining -= value * size;
|
|
78
|
+
if (value > 0)
|
|
79
|
+
parts.push(`${value}${suffix}`);
|
|
80
|
+
if (parts.length === 2)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return parts.join(" ");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Wrap `fetch` so a response asking to wait longer than `maxRetryDelayMs` is
|
|
87
|
+
* marked as one the SDK must not retry.
|
|
88
|
+
*
|
|
89
|
+
* `x-should-retry: false` is the escape hatch both the OpenAI and Anthropic
|
|
90
|
+
* clients check before anything else, so stamping it is enough to stop the
|
|
91
|
+
* retry loop before it computes an unholdable sleep. The response is otherwise
|
|
92
|
+
* passed through untouched — same status, same body — so the error the caller
|
|
93
|
+
* finally sees is still the provider's own.
|
|
94
|
+
*
|
|
95
|
+
* A cap of zero (or a nonsensical one) disables the wrapper entirely, which is
|
|
96
|
+
* what the documented "set to 0 to disable" means.
|
|
97
|
+
*/
|
|
98
|
+
export function createRetryDelayCapFetch(maxRetryDelayMs, baseFetch = fetch) {
|
|
99
|
+
if (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs <= 0)
|
|
100
|
+
return baseFetch;
|
|
101
|
+
return async (input, init) => {
|
|
102
|
+
const response = await baseFetch(input, init);
|
|
103
|
+
// Only a failure is ever retried, and a server that already stated its
|
|
104
|
+
// own preference outranks the cap.
|
|
105
|
+
if (response.ok || response.headers.get("x-should-retry") !== null)
|
|
106
|
+
return response;
|
|
107
|
+
const delayMs = parseRetryAfterMs(response.headers);
|
|
108
|
+
if (delayMs === undefined || delayMs <= maxRetryDelayMs)
|
|
109
|
+
return response;
|
|
110
|
+
const headers = new Headers(response.headers);
|
|
111
|
+
headers.set("x-should-retry", "false");
|
|
112
|
+
return new Response(response.body, {
|
|
113
|
+
status: response.status,
|
|
114
|
+
statusText: response.statusText,
|
|
115
|
+
headers,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The `fetch` override to spread into an SDK client's options, or nothing when
|
|
121
|
+
* no cap applies.
|
|
122
|
+
*
|
|
123
|
+
* Spread rather than assigned so a disabled cap leaves the key absent
|
|
124
|
+
* altogether: passing `fetch: undefined` explicitly would override the client's
|
|
125
|
+
* own default instead of leaving it alone.
|
|
126
|
+
*/
|
|
127
|
+
export function retryDelayCapFetch(maxRetryDelayMs) {
|
|
128
|
+
if (maxRetryDelayMs === undefined)
|
|
129
|
+
return {};
|
|
130
|
+
const capped = createRetryDelayCapFetch(maxRetryDelayMs);
|
|
131
|
+
return capped === fetch ? {} : { fetch: capped };
|
|
132
|
+
}
|
|
133
|
+
/** The header bag an SDK error carries, if it is that kind of error. */
|
|
134
|
+
function errorHeaders(error) {
|
|
135
|
+
if (!error || typeof error !== "object")
|
|
136
|
+
return undefined;
|
|
137
|
+
const headers = error.headers;
|
|
138
|
+
if (!headers || typeof headers !== "object")
|
|
139
|
+
return undefined;
|
|
140
|
+
return typeof headers.get === "function" ? headers : undefined;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The provider's error message, plus how long it wants us gone.
|
|
144
|
+
*
|
|
145
|
+
* A bare "429 quota exceeded" is the same sentence whether the quota returns in
|
|
146
|
+
* thirty seconds or four weeks, and those call for opposite responses from the
|
|
147
|
+
* person reading it. The wait is already on the error; it just was never shown.
|
|
148
|
+
*/
|
|
149
|
+
export function describeProviderError(error, maxRetryDelayMs = DEFAULT_MAX_RETRY_DELAY_MS) {
|
|
150
|
+
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
|
151
|
+
const delayMs = parseRetryAfterMs(errorHeaders(error));
|
|
152
|
+
if (delayMs === undefined || delayMs <= Math.max(0, maxRetryDelayMs))
|
|
153
|
+
return message;
|
|
154
|
+
return `${message} (the provider ${LONG_DELAY_MARKER} ${formatDelay(delayMs)} before retrying, so no retry was attempted)`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether an error message describes a wait too long to sit through — the
|
|
158
|
+
* signal that retrying is pointless rather than merely slow.
|
|
159
|
+
*/
|
|
160
|
+
export function isLongRetryDelayError(message) {
|
|
161
|
+
return message?.includes(`provider ${LONG_DELAY_MARKER}`) ?? false;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=retry-delay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-delay.js","sourceRoot":"","sources":["../../src/utils/retry-delay.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-requested retry delays, and the cap that keeps them sane.\n *\n * A provider that has run out of quota answers `429` with a `Retry-After`\n * naming when the quota resets — which for a monthly plan is weeks away, not\n * seconds. The vendor SDKs take that header literally and sleep for it, and a\n * delay that large does not survive the trip: `setTimeout` holds a signed\n * 32-bit millisecond count, so anything past ~24.9 days is silently clamped to\n * 1ms. The \"wait 28 days\" becomes \"retry immediately\", the retry draws another\n * 429, and the SDK burns its whole retry budget in a few milliseconds against a\n * quota that will not move for a month.\n *\n * So a requested delay past the cap is not something to wait out — it is a\n * different kind of failure, and the caller needs to see it rather than sit in\n * a hot loop behind it.\n */\n\n/**\n * The largest delay a timer can actually hold.\n *\n * `setTimeout` stores its delay in a signed 32-bit integer. Larger values do\n * not throw: Node warns and substitutes 1ms, turning the longest wait into the\n * shortest one.\n */\nexport const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;\n\n/** Longest server-requested delay worth waiting out, when the caller names none. */\nexport const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\n\n/** Phrase every capped-delay message carries, so callers can recognise one. */\nconst LONG_DELAY_MARKER = \"asked to wait\";\n\n/** Anything with a `get` — a `Headers`, or an SDK error's header bag. */\ninterface HeaderBag {\n\tget(name: string): string | null | undefined;\n}\n\n/**\n * The delay a response asks for, in milliseconds, or undefined if it asks for\n * none.\n *\n * Mirrors what the OpenAI and Anthropic SDKs do with these headers, because the\n * point is to predict the sleep they are about to take: `retry-after-ms` wins,\n * then `retry-after` as seconds, then `retry-after` as an HTTP date.\n */\nexport function parseRetryAfterMs(headers: HeaderBag | undefined): number | undefined {\n\tif (!headers) return undefined;\n\n\tconst afterMs = headers.get(\"retry-after-ms\");\n\tif (afterMs) {\n\t\tconst ms = Number.parseFloat(afterMs);\n\t\tif (!Number.isNaN(ms)) return ms;\n\t}\n\n\tconst after = headers.get(\"retry-after\");\n\tif (!after) return undefined;\n\n\tconst seconds = Number.parseFloat(after);\n\tif (!Number.isNaN(seconds)) return seconds * 1000;\n\n\t// The header's other legal form is an HTTP date.\n\tconst at = Date.parse(after);\n\treturn Number.isNaN(at) ? undefined : at - Date.now();\n}\n\n/**\n * A duration as a person would say it: the two largest units that carry\n * meaning, because \"28d 15h\" tells you to go do something else and\n * \"2472352s\" does not.\n */\nexport function formatDelay(ms: number): string {\n\tif (!Number.isFinite(ms) || ms < 0) return \"an unknown time\";\n\n\tconst totalSeconds = Math.round(ms / 1000);\n\tif (totalSeconds < 60) return `${totalSeconds}s`;\n\n\tconst units: Array<[number, string]> = [\n\t\t[86400, \"d\"],\n\t\t[3600, \"h\"],\n\t\t[60, \"m\"],\n\t\t[1, \"s\"],\n\t];\n\n\tconst parts: string[] = [];\n\tlet remaining = totalSeconds;\n\tfor (const [size, suffix] of units) {\n\t\tconst value = Math.floor(remaining / size);\n\t\tremaining -= value * size;\n\t\tif (value > 0) parts.push(`${value}${suffix}`);\n\t\tif (parts.length === 2) break;\n\t}\n\treturn parts.join(\" \");\n}\n\n/**\n * Wrap `fetch` so a response asking to wait longer than `maxRetryDelayMs` is\n * marked as one the SDK must not retry.\n *\n * `x-should-retry: false` is the escape hatch both the OpenAI and Anthropic\n * clients check before anything else, so stamping it is enough to stop the\n * retry loop before it computes an unholdable sleep. The response is otherwise\n * passed through untouched — same status, same body — so the error the caller\n * finally sees is still the provider's own.\n *\n * A cap of zero (or a nonsensical one) disables the wrapper entirely, which is\n * what the documented \"set to 0 to disable\" means.\n */\nexport function createRetryDelayCapFetch(maxRetryDelayMs: number, baseFetch: typeof fetch = fetch): typeof fetch {\n\tif (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs <= 0) return baseFetch;\n\n\treturn async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]): Promise<Response> => {\n\t\tconst response = await baseFetch(input, init);\n\n\t\t// Only a failure is ever retried, and a server that already stated its\n\t\t// own preference outranks the cap.\n\t\tif (response.ok || response.headers.get(\"x-should-retry\") !== null) return response;\n\n\t\tconst delayMs = parseRetryAfterMs(response.headers);\n\t\tif (delayMs === undefined || delayMs <= maxRetryDelayMs) return response;\n\n\t\tconst headers = new Headers(response.headers);\n\t\theaders.set(\"x-should-retry\", \"false\");\n\t\treturn new Response(response.body, {\n\t\t\tstatus: response.status,\n\t\t\tstatusText: response.statusText,\n\t\t\theaders,\n\t\t});\n\t};\n}\n\n/**\n * The `fetch` override to spread into an SDK client's options, or nothing when\n * no cap applies.\n *\n * Spread rather than assigned so a disabled cap leaves the key absent\n * altogether: passing `fetch: undefined` explicitly would override the client's\n * own default instead of leaving it alone.\n */\nexport function retryDelayCapFetch(maxRetryDelayMs: number | undefined): { fetch?: typeof fetch } {\n\tif (maxRetryDelayMs === undefined) return {};\n\tconst capped = createRetryDelayCapFetch(maxRetryDelayMs);\n\treturn capped === fetch ? {} : { fetch: capped };\n}\n\n/** The header bag an SDK error carries, if it is that kind of error. */\nfunction errorHeaders(error: unknown): HeaderBag | undefined {\n\tif (!error || typeof error !== \"object\") return undefined;\n\tconst headers = (error as { headers?: unknown }).headers;\n\tif (!headers || typeof headers !== \"object\") return undefined;\n\treturn typeof (headers as HeaderBag).get === \"function\" ? (headers as HeaderBag) : undefined;\n}\n\n/**\n * The provider's error message, plus how long it wants us gone.\n *\n * A bare \"429 quota exceeded\" is the same sentence whether the quota returns in\n * thirty seconds or four weeks, and those call for opposite responses from the\n * person reading it. The wait is already on the error; it just was never shown.\n */\nexport function describeProviderError(error: unknown, maxRetryDelayMs = DEFAULT_MAX_RETRY_DELAY_MS): string {\n\tconst message = error instanceof Error ? error.message : JSON.stringify(error);\n\n\tconst delayMs = parseRetryAfterMs(errorHeaders(error));\n\tif (delayMs === undefined || delayMs <= Math.max(0, maxRetryDelayMs)) return message;\n\n\treturn `${message} (the provider ${LONG_DELAY_MARKER} ${formatDelay(delayMs)} before retrying, so no retry was attempted)`;\n}\n\n/**\n * Whether an error message describes a wait too long to sit through — the\n * signal that retrying is pointless rather than merely slow.\n */\nexport function isLongRetryDelayError(message: string | undefined): boolean {\n\treturn message?.includes(`provider ${LONG_DELAY_MARKER}`) ?? false;\n}\n"]}
|