@wudao-org/providers 0.0.0-stage → 0.4.21
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/LICENSE +21 -0
- package/README.md +8 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/anthropic.d.ts +19 -0
- package/dist/anthropic.d.ts.map +1 -0
- package/dist/anthropic.js +621 -0
- package/dist/anthropic.js.map +1 -0
- package/dist/balance.d.ts +28 -0
- package/dist/balance.d.ts.map +1 -0
- package/dist/balance.js +105 -0
- package/dist/balance.js.map +1 -0
- package/dist/config.d.ts +328 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +623 -0
- package/dist/config.js.map +1 -0
- package/dist/dispatcher.d.ts +5 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +21 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/effort-catalog.d.ts +17 -0
- package/dist/effort-catalog.d.ts.map +1 -0
- package/dist/effort-catalog.js +73 -0
- package/dist/effort-catalog.js.map +1 -0
- package/dist/error-body.d.ts +32 -0
- package/dist/error-body.d.ts.map +1 -0
- package/dist/error-body.js +74 -0
- package/dist/error-body.js.map +1 -0
- package/dist/header-validation.d.ts +57 -0
- package/dist/header-validation.d.ts.map +1 -0
- package/dist/header-validation.js +130 -0
- package/dist/header-validation.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/json-arguments.d.ts +17 -0
- package/dist/json-arguments.d.ts.map +1 -0
- package/dist/json-arguments.js +227 -0
- package/dist/json-arguments.js.map +1 -0
- package/dist/key-pool.d.ts +66 -0
- package/dist/key-pool.d.ts.map +1 -0
- package/dist/key-pool.js +207 -0
- package/dist/key-pool.js.map +1 -0
- package/dist/missing-reasoning-circuit.d.ts +17 -0
- package/dist/missing-reasoning-circuit.d.ts.map +1 -0
- package/dist/missing-reasoning-circuit.js +38 -0
- package/dist/missing-reasoning-circuit.js.map +1 -0
- package/dist/model-failure.d.ts +156 -0
- package/dist/model-failure.d.ts.map +1 -0
- package/dist/model-failure.js +253 -0
- package/dist/model-failure.js.map +1 -0
- package/dist/model-fallback.d.ts +18 -0
- package/dist/model-fallback.d.ts.map +1 -0
- package/dist/model-fallback.js +87 -0
- package/dist/model-fallback.js.map +1 -0
- package/dist/models.d.ts +20 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +55 -0
- package/dist/models.js.map +1 -0
- package/dist/openai-compatible.d.ts +87 -0
- package/dist/openai-compatible.d.ts.map +1 -0
- package/dist/openai-compatible.js +368 -0
- package/dist/openai-compatible.js.map +1 -0
- package/dist/openai-engine.d.ts +18 -0
- package/dist/openai-engine.d.ts.map +1 -0
- package/dist/openai-engine.js +515 -0
- package/dist/openai-engine.js.map +1 -0
- package/dist/openai-images.d.ts +38 -0
- package/dist/openai-images.d.ts.map +1 -0
- package/dist/openai-images.js +301 -0
- package/dist/openai-images.js.map +1 -0
- package/dist/openai-messages.d.ts +40 -0
- package/dist/openai-messages.d.ts.map +1 -0
- package/dist/openai-messages.js +135 -0
- package/dist/openai-messages.js.map +1 -0
- package/dist/openai-wire.d.ts +93 -0
- package/dist/openai-wire.d.ts.map +1 -0
- package/dist/openai-wire.js +151 -0
- package/dist/openai-wire.js.map +1 -0
- package/dist/persona/apply.d.ts +19 -0
- package/dist/persona/apply.d.ts.map +1 -0
- package/dist/persona/apply.js +98 -0
- package/dist/persona/apply.js.map +1 -0
- package/dist/persona/builtins.d.ts +3 -0
- package/dist/persona/builtins.d.ts.map +1 -0
- package/dist/persona/builtins.js +180 -0
- package/dist/persona/builtins.js.map +1 -0
- package/dist/persona/decorator.d.ts +4 -0
- package/dist/persona/decorator.d.ts.map +1 -0
- package/dist/persona/decorator.js +112 -0
- package/dist/persona/decorator.js.map +1 -0
- package/dist/persona/files.d.ts +20 -0
- package/dist/persona/files.d.ts.map +1 -0
- package/dist/persona/files.js +84 -0
- package/dist/persona/files.js.map +1 -0
- package/dist/persona/ids.d.ts +9 -0
- package/dist/persona/ids.d.ts.map +1 -0
- package/dist/persona/ids.js +11 -0
- package/dist/persona/ids.js.map +1 -0
- package/dist/persona/registry.d.ts +12 -0
- package/dist/persona/registry.d.ts.map +1 -0
- package/dist/persona/registry.js +54 -0
- package/dist/persona/registry.js.map +1 -0
- package/dist/persona/runtime.d.ts +23 -0
- package/dist/persona/runtime.d.ts.map +1 -0
- package/dist/persona/runtime.js +32 -0
- package/dist/persona/runtime.js.map +1 -0
- package/dist/persona/schema.d.ts +27 -0
- package/dist/persona/schema.d.ts.map +1 -0
- package/dist/persona/schema.js +165 -0
- package/dist/persona/schema.js.map +1 -0
- package/dist/persona/types.d.ts +38 -0
- package/dist/persona/types.d.ts.map +1 -0
- package/dist/persona/types.js +2 -0
- package/dist/persona/types.js.map +1 -0
- package/dist/pricing.d.ts +40 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +70 -0
- package/dist/pricing.js.map +1 -0
- package/dist/quota-park.d.ts +53 -0
- package/dist/quota-park.d.ts.map +1 -0
- package/dist/quota-park.js +135 -0
- package/dist/quota-park.js.map +1 -0
- package/dist/rate-pause.d.ts +28 -0
- package/dist/rate-pause.d.ts.map +1 -0
- package/dist/rate-pause.js +72 -0
- package/dist/rate-pause.js.map +1 -0
- package/dist/registry.d.ts +142 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +471 -0
- package/dist/registry.js.map +1 -0
- package/dist/responses.d.ts +5 -0
- package/dist/responses.d.ts.map +1 -0
- package/dist/responses.js +290 -0
- package/dist/responses.js.map +1 -0
- package/dist/retry-after.d.ts +13 -0
- package/dist/retry-after.d.ts.map +1 -0
- package/dist/retry-after.js +64 -0
- package/dist/retry-after.js.map +1 -0
- package/dist/retry-ladder.d.ts +176 -0
- package/dist/retry-ladder.d.ts.map +1 -0
- package/dist/retry-ladder.js +664 -0
- package/dist/retry-ladder.js.map +1 -0
- package/dist/split-inband-thinking.d.ts +35 -0
- package/dist/split-inband-thinking.d.ts.map +1 -0
- package/dist/split-inband-thinking.js +117 -0
- package/dist/split-inband-thinking.js.map +1 -0
- package/dist/stream-idle.d.ts +97 -0
- package/dist/stream-idle.d.ts.map +1 -0
- package/dist/stream-idle.js +127 -0
- package/dist/stream-idle.js.map +1 -0
- package/dist/tool-call-ids.d.ts +16 -0
- package/dist/tool-call-ids.d.ts.map +1 -0
- package/dist/tool-call-ids.js +36 -0
- package/dist/tool-call-ids.js.map +1 -0
- package/package.json +32 -3
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
// 拆分运动 C:从 openai-compatible.ts 原样外提的重试梯子 + 失败分类域
|
|
2
|
+
//(M26 P11 梯子本体、M58 §A 上下文溢出分类、M70 §3/§4、梯子判定族)。
|
|
3
|
+
// 本模块零 openai-compatible 依赖(无环);openai-compatible.ts 逐名 re-export
|
|
4
|
+
// 保公共面(anthropic/responses/tests/index 既有导入方零演进)。
|
|
5
|
+
import { findProxyEgressCause, probeModelRetry, truncateSafe } from "@wudao-org/core";
|
|
6
|
+
import { BackgroundOverloadError, ContextLengthError, ProviderHttpError } from "./model-failure.js";
|
|
7
|
+
import { RETRY_AFTER_FAIL_CAP_MS } from "./retry-after.js";
|
|
8
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
9
|
+
const DEFAULT_BASE_DELAY_MS = 500;
|
|
10
|
+
const DEFAULT_OVERLOAD_BASE_DELAY_MS = 2_000;
|
|
11
|
+
const DEFAULT_MAX_DELAY_MS = 32_000;
|
|
12
|
+
const DEFAULT_JITTER_RATIO = 0.25;
|
|
13
|
+
// 124 项 2 — fatal TLS codes are referenced from the network-grace detector
|
|
14
|
+
// above; declare them here (used to be local to isRetryableProviderError —
|
|
15
|
+
// hoisted so the grace classifier can rely on the same set with one source
|
|
16
|
+
// of truth).
|
|
17
|
+
const FATAL_TLS_CODES = new Set([
|
|
18
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
19
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
20
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
21
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
22
|
+
"CERT_HAS_EXPIRED",
|
|
23
|
+
"CERT_NOT_YET_VALID",
|
|
24
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
25
|
+
"ERR_SSL_CERTIFICATE_ALT_NAME_INVALID",
|
|
26
|
+
]);
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// 124 项 2 — the network-outage grace window. The exponential ladder is tuned
|
|
29
|
+
// for transient blips (429/5xx micro-windows); a Wi-Fi drop / router reboot /
|
|
30
|
+
// late-night connectivity loss exceeds it and would let the run fail after
|
|
31
|
+
// ~32s × 4 = ~3min of dead time. The grace window ONLY widens the ladder
|
|
32
|
+
// for NETWORK-class failures (fetch reject / DNS / socket hang / TLS-not-fatal
|
|
33
|
+
// timeouts — undici surfaces all of these as TypeError("fetch failed") with
|
|
34
|
+
// the real cause on .cause); 4xx/5xx/business-5xx-validation errors keep
|
|
35
|
+
// the existing ladder unchanged. The window's wait is the same exponential
|
|
36
|
+
// shape (maxDelayMs bumped from 32s → 5min), the retry count is multiplied
|
|
37
|
+
// (default × 3 = 12); the onRetry / onOverload hooks fire as usual (the CLI
|
|
38
|
+
// notices remain byte-stable). Opt-in via retry.networkGrace.enabled so the
|
|
39
|
+
// default behavior never changes.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/** The transport-class failure predicates used by both isRetryableProviderError
|
|
42
|
+
* and the grace window's widener. A failure qualifies as NETWORK when:
|
|
43
|
+
* • it is NOT a ProviderHttpError (4xx/5xx are business-class and must NEVER
|
|
44
|
+
* enter the grace window),
|
|
45
|
+
* • it is not a typed ContextLengthError / QuotaParkError / fatal TLS /
|
|
46
|
+
* proxy-attributed / request-validation failure (deterministic),
|
|
47
|
+
* • it carries the fetch failure vocabulary (TypeError("fetch failed") and
|
|
48
|
+
* friends) — same family as the existing transport retry path.
|
|
49
|
+
* Pinned regex over the message AND the .cause chain (undici wraps twice:
|
|
50
|
+
* TypeError("fetch failed") → cause Error("socket hang up") → …);
|
|
51
|
+
* duck-typed cause-walking mirrors the fatal-TLS detector above. */
|
|
52
|
+
const NETWORK_FETCH_RE = /fetch failed|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|socket hang up|network timeout/i;
|
|
53
|
+
const MAX_CAUSE_DEPTH = 4;
|
|
54
|
+
function hasFatalTlsCode(err) {
|
|
55
|
+
// Reuses the existing FATAL_TLS_CODES set (declared below in the file) by
|
|
56
|
+
// walking the same depth-capped cause chain — a network fetch reject with
|
|
57
|
+
// a fatal TLS cause is DETERMINISTIC, never worth the grace window.
|
|
58
|
+
if (!(err instanceof Error))
|
|
59
|
+
return false;
|
|
60
|
+
let cur = err;
|
|
61
|
+
for (let depth = 0; depth <= MAX_CAUSE_DEPTH; depth += 1) {
|
|
62
|
+
const code = cur?.code;
|
|
63
|
+
if (typeof code === "string" && FATAL_TLS_CODES.has(code))
|
|
64
|
+
return true;
|
|
65
|
+
cur = cur?.cause;
|
|
66
|
+
if (cur === undefined || cur === null)
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/** True iff `err` looks like a transient network-class failure (NOT a
|
|
72
|
+
* business-class HTTP error and NOT a deterministic fetch failure). */
|
|
73
|
+
export function isNetworkClassFailure(err) {
|
|
74
|
+
if (!(err instanceof Error))
|
|
75
|
+
return false;
|
|
76
|
+
// Business-class HTTP errors never enter the grace window — a 4xx/5xx
|
|
77
|
+
// body is the provider's answer, not the network's silence.
|
|
78
|
+
if (err instanceof ProviderHttpError)
|
|
79
|
+
return false;
|
|
80
|
+
// Deterministic typed failures refuse to wait.
|
|
81
|
+
if (err.name === "AbortError")
|
|
82
|
+
return false;
|
|
83
|
+
if (err.name === "ContextLengthError")
|
|
84
|
+
return false;
|
|
85
|
+
if (err.name === "QuotaParkError")
|
|
86
|
+
return false;
|
|
87
|
+
if (err.name === "ReasoningMandatoryError" && !err.repairable)
|
|
88
|
+
return false;
|
|
89
|
+
if (err.name === "UnsupportedContentError")
|
|
90
|
+
return false;
|
|
91
|
+
if (err.name === "HeaderValidationError")
|
|
92
|
+
return false;
|
|
93
|
+
if (err.name === "ProxyEgressError" || findProxyEgressCause(err) !== undefined)
|
|
94
|
+
return false;
|
|
95
|
+
// Fatal TLS is deterministic on this endpoint.
|
|
96
|
+
if (hasFatalTlsCode(err))
|
|
97
|
+
return false;
|
|
98
|
+
// Walk the message AND cause chain (depth-capped like the TLS detector).
|
|
99
|
+
let cur = err;
|
|
100
|
+
for (let depth = 0; depth <= MAX_CAUSE_DEPTH; depth += 1) {
|
|
101
|
+
if (cur instanceof Error && NETWORK_FETCH_RE.test(cur.message))
|
|
102
|
+
return true;
|
|
103
|
+
cur = cur?.cause;
|
|
104
|
+
if (cur === undefined || cur === null)
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
export const DEFAULT_NETWORK_GRACE_RETRY_MULTIPLIER = 3;
|
|
110
|
+
export const DEFAULT_NETWORK_GRACE_MAX_DELAY_MS = 5 * 60_000;
|
|
111
|
+
/** Statuses that mean "try again later", not "your request was wrong". */
|
|
112
|
+
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 529]);
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// M58 §A — context-overflow classification (413 reactive layer). Providers
|
|
115
|
+
// disagree on how they say "your prompt does not fit the window": HTTP 413,
|
|
116
|
+
// or a 400/500 whose body carries the context-length vocabulary. Every one
|
|
117
|
+
// of those is classified into the typed ContextLengthError (the duck-typed
|
|
118
|
+
// port contract documented on core's isContextLengthError) at the HTTP
|
|
119
|
+
// boundary, BEFORE the generic ProviderHttpError — so the retry ladder never
|
|
120
|
+
// retries an oversized prompt and the loop can react (compact, retry once).
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/** The context-length body vocabulary, vendor-anchored. */
|
|
123
|
+
export const CONTEXT_LENGTH_BODY_SIGNATURES = [
|
|
124
|
+
// The spec-pinned generic wording: "context length exceeded" and friends.
|
|
125
|
+
// Covers OpenAI / DeepSeek / vLLM-hosted endpoints (400): "This model's
|
|
126
|
+
// maximum context length is 65536 tokens. However, you requested 109645
|
|
127
|
+
// tokens …" — and Qwen/DashScope: "The input content (including
|
|
128
|
+
// conversation history and code context) exceeds the model's maximum
|
|
129
|
+
// context length."
|
|
130
|
+
/context length/i,
|
|
131
|
+
// zhipu/bigmodel 1214 (400) — English docs wording: "… parameter is
|
|
132
|
+
// invalid. Prompt too long"
|
|
133
|
+
/prompt\s+too\s+long/i,
|
|
134
|
+
// zhipu/bigmodel 1214 (400) — the Chinese body: "messages 参数非法。 Prompt 超长"
|
|
135
|
+
/prompt\s*超长/i,
|
|
136
|
+
];
|
|
137
|
+
// 120-b B2 (Reasonix #10770) — the GLM/vLLM/SiliconFlow prose family. Live
|
|
138
|
+
// bodies say "Input length 1048826 exceeds the maximum length 1048566" (400):
|
|
139
|
+
// NO "context length" vocabulary anywhere, so every signature above misses it
|
|
140
|
+
// and the overflow fell through to a generic 400 "Malformed request" — the
|
|
141
|
+
// 413 reactive layer never ran. Deliberately NOT a CONTEXT_LENGTH_BODY_
|
|
142
|
+
// SIGNATURES entry: the family carries a strictness invariant a bare
|
|
143
|
+
// regex.test cannot express — the input figure must EXCEED the maximum
|
|
144
|
+
// (N > M). An equal-or-smaller pair worded like an overflow stays
|
|
145
|
+
// unclassified/OPAQUE (a body that says "exceeds" while its own numbers say
|
|
146
|
+
// otherwise must never trigger compact-and-retry). The matcher below drives
|
|
147
|
+
// BOTH halves, classification and trusted parse, under that one invariant.
|
|
148
|
+
/** "input length N exceeds [the] maximum [context] length M" — N and M are
|
|
149
|
+
* literal token counts; the gap tolerators keep "the"/extra spaces; the
|
|
150
|
+
* optional "context" admits the "maximum context length" spelling (which the
|
|
151
|
+
* generic /context length/i signature already classifies — there the pair
|
|
152
|
+
* only feeds the trusted parse). */
|
|
153
|
+
export const CONTEXT_LIMIT_INPUT_LENGTH_RE = /\binput\s+length\s+(\d+)\D{1,80}?exceeds\D{1,80}?maximum(?:\s+context)?\s+length\s+(\d+)\b/i;
|
|
154
|
+
/** The parsed (inputTokens, maximumLength) pair when the body carries the
|
|
155
|
+
* GLM/vLLM spelling AND the strictness invariant holds (input > maximum,
|
|
156
|
+
* both finite and positive); undefined for every near-miss — numberless
|
|
157
|
+
* ("Input length is too long"), equal, or inverted pairs. */
|
|
158
|
+
export function matchContextLimitInputLength(body) {
|
|
159
|
+
const m = CONTEXT_LIMIT_INPUT_LENGTH_RE.exec(body);
|
|
160
|
+
if (m === null)
|
|
161
|
+
return undefined;
|
|
162
|
+
const inputTokens = Number(m[1]);
|
|
163
|
+
const maximumLength = Number(m[2]);
|
|
164
|
+
if (!Number.isFinite(inputTokens) || !Number.isFinite(maximumLength) || inputTokens <= 0 || maximumLength <= 0) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
if (inputTokens <= maximumLength)
|
|
168
|
+
return undefined; // strictness: N > M or OPAQUE
|
|
169
|
+
return { inputTokens, maximumLength };
|
|
170
|
+
}
|
|
171
|
+
/** True when a response body carries a known context-length signature. */
|
|
172
|
+
export function sniffContextLengthBody(body) {
|
|
173
|
+
// The strict GLM/vLLM matcher joins the loose vocabulary table here — the
|
|
174
|
+
// only place classification consults it (single source of truth for both
|
|
175
|
+
// halves of the family).
|
|
176
|
+
return CONTEXT_LENGTH_BODY_SIGNATURES.some((re) => re.test(body)) || matchContextLimitInputLength(body) !== undefined;
|
|
177
|
+
}
|
|
178
|
+
/** True iff the status/body pair means "the prompt does not fit the window":
|
|
179
|
+
* HTTP 413 outright, or a 400/500 whose body carries the vocabulary above. */
|
|
180
|
+
export function isContextLengthFailure(status, body) {
|
|
181
|
+
return status === 413 || ((status === 400 || status === 500) && sniffContextLengthBody(body));
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// ω-2b (I1-a) — the trusted context-limit body parse. A body that already
|
|
185
|
+
// CLASSIFIED as a context-length failure (the pinned signature regexes above)
|
|
186
|
+
// may carry the two numbers the loop needs to react precisely instead of
|
|
187
|
+
// compacting blind: the real window and the real requested size. Parse is
|
|
188
|
+
// deliberately conservative — only the pinned spellings below produce an
|
|
189
|
+
// observation, everything else (zhipu's numberless "Prompt too long" / "Prompt
|
|
190
|
+
// 超长", a bare 413 "payload too large", junk) stays OPAQUE (undefined) and the
|
|
191
|
+
// loop falls back to its char estimates.
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
/** The DeepSeek / vLLM / OpenAI prose pair: "This model's maximum context
|
|
194
|
+
* length is 65536 tokens. However, you requested 109645 tokens …" — window
|
|
195
|
+
* then total request (prompt + the max_tokens the request carried, or the
|
|
196
|
+
* provider's auto-allocated completion budget when it carried none). */
|
|
197
|
+
const CONTEXT_LIMIT_PROSE_RE = /maximum context length is (\d+) tokens\D{0,120}?you requested (\d+) tokens/i;
|
|
198
|
+
/** Parse a classified context-length body for the observed (promptTokens,
|
|
199
|
+
* windowTokens) pair. `maxTokens` is the completion cap THIS request carried
|
|
200
|
+
* (the effective one — a per-input override beats the config's): the prose
|
|
201
|
+
* "you requested M" counts prompt + completion, so the prompt is M − maxTokens
|
|
202
|
+
* when a cap was sent; without one the provider auto-allocated a completion
|
|
203
|
+
* budget and counted it against the request, so M itself is the conservative
|
|
204
|
+
* prompt figure (over-counting is the safe direction for the clip ladder).
|
|
205
|
+
* Undefined when either number is missing or unparseable — trusted-parse-only. */
|
|
206
|
+
export function parseContextLimitBody(body, maxTokens) {
|
|
207
|
+
const prose = CONTEXT_LIMIT_PROSE_RE.exec(body);
|
|
208
|
+
if (prose !== null) {
|
|
209
|
+
const windowTokens = Number(prose[1]);
|
|
210
|
+
const requested = Number(prose[2]);
|
|
211
|
+
if (!Number.isFinite(windowTokens) || !Number.isFinite(requested) || windowTokens <= 0 || requested <= 0) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const promptTokens = maxTokens !== undefined && requested > maxTokens ? requested - maxTokens : requested;
|
|
215
|
+
return { promptTokens, windowTokens };
|
|
216
|
+
}
|
|
217
|
+
// 120-b B2: the GLM/vLLM spelling — vLLM's "input length" counts PROMPT
|
|
218
|
+
// tokens only (no completion budget folded in), so the figure is the
|
|
219
|
+
// promptTokens observation as-is and the request's max_tokens cap plays no
|
|
220
|
+
// decomposition role. windowTokens is the endpoint's configured maximum
|
|
221
|
+
// model length.
|
|
222
|
+
const glm = matchContextLimitInputLength(body);
|
|
223
|
+
if (glm !== undefined) {
|
|
224
|
+
return { promptTokens: glm.inputTokens, windowTokens: glm.maximumLength };
|
|
225
|
+
}
|
|
226
|
+
// The JSON/structured spelling: the same pair the max_tokens self-repair
|
|
227
|
+
// parses (input_tokens + context_limit) — but here it rides a body that
|
|
228
|
+
// already classified as context-length (the two vocabularies are disjoint
|
|
229
|
+
// in practice; where they overlap, the numbers are the same truth).
|
|
230
|
+
const parsed = parseMaxTokensOverflow(body);
|
|
231
|
+
if (parsed === undefined)
|
|
232
|
+
return undefined;
|
|
233
|
+
return { promptTokens: parsed.inputTokens, windowTokens: parsed.contextLimit };
|
|
234
|
+
}
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// M70 §4 (CC) — max_tokens overflow self-repair. A 400 whose body carries
|
|
237
|
+
// parseable input_tokens/context_limit numbers (WITHOUT the context-length
|
|
238
|
+
// vocabulary — that classifies as ContextLengthError and belongs to the
|
|
239
|
+
// loop's compact-and-retry) means the COMPLETION cap pushed the total over
|
|
240
|
+
// the window while the prompt itself fits: parse the numbers, set
|
|
241
|
+
// retryContext.maxTokensOverride, and retry ONCE with the reduced cap.
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
/** Prose + JSON spellings of the two numbers, across vendors. */
|
|
244
|
+
const INPUT_TOKENS_RE = /input_tokens\D{0,24}?(\d+)/i;
|
|
245
|
+
const CONTEXT_LIMIT_RE = /(?:context_limit|maximum_context_length|context_window|maximum_context_window)\D{0,24}?(\d+)/i;
|
|
246
|
+
/** Parse a 400 body for the (inputTokens, contextLimit) pair; undefined when
|
|
247
|
+
* EITHER number is missing or unparseable. */
|
|
248
|
+
export function parseMaxTokensOverflow(body) {
|
|
249
|
+
const input = INPUT_TOKENS_RE.exec(body)?.[1];
|
|
250
|
+
const limit = CONTEXT_LIMIT_RE.exec(body)?.[1];
|
|
251
|
+
if (input === undefined || limit === undefined)
|
|
252
|
+
return undefined;
|
|
253
|
+
const inputTokens = Number(input);
|
|
254
|
+
const contextLimit = Number(limit);
|
|
255
|
+
if (!Number.isFinite(inputTokens) || !Number.isFinite(contextLimit) || inputTokens <= 0 || contextLimit <= 0)
|
|
256
|
+
return undefined;
|
|
257
|
+
return { inputTokens, contextLimit };
|
|
258
|
+
}
|
|
259
|
+
/** The repaired completion cap: max(3000, contextLimit - inputTokens - 1000,
|
|
260
|
+
* thinkingBudget + 1). No thinking budget is configured today (0 — the term
|
|
261
|
+
* is CC's forward-looking floor: a thinking-capable model must keep at least
|
|
262
|
+
* budget+1 tokens or the request fails for a different reason). */
|
|
263
|
+
export function selfRepairMaxTokensValue(parsed, thinkingBudget = 0) {
|
|
264
|
+
return Math.max(3_000, parsed.contextLimit - parsed.inputTokens - 1_000, thinkingBudget + 1);
|
|
265
|
+
}
|
|
266
|
+
/** True when the error is a 400 the self-repair can parse (the retry-once
|
|
267
|
+
* trigger; everything else keeps its existing path). */
|
|
268
|
+
export function isMaxTokensOverflow400(err) {
|
|
269
|
+
return err instanceof ProviderHttpError && err.status === 400 && parseMaxTokensOverflow(err.body) !== undefined;
|
|
270
|
+
}
|
|
271
|
+
/** ω-2b (I1-a): classify + stamp in one step — the trusted-parse observation
|
|
272
|
+
* rides the typed error when the body carried the numbers. The effective
|
|
273
|
+
* completion cap (per-input maxTokens override beats the config's) decides
|
|
274
|
+
* how the prose figures decompose into a prompt size. */
|
|
275
|
+
export function classifyContextLength(status, detail, config, input) {
|
|
276
|
+
return new ContextLengthError(status, detail, parseContextLimitBody(detail, input.maxTokens ?? config.maxTokens));
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// M70 §3 (CC) — the foreground-only retry allowlist. API calls are classified
|
|
280
|
+
// by whether a HUMAN is blocking on the result (ModelInput.querySource):
|
|
281
|
+
// sources in FOREGROUND_QUERY_SOURCES — and untagged calls, the backward-
|
|
282
|
+
// compatible default — walk the ordinary ladder on overload; every OTHER
|
|
283
|
+
// source (the compaction summarizer, the /goal evaluator, title generators)
|
|
284
|
+
// bails immediately with the typed BackgroundOverloadError. During a
|
|
285
|
+
// capacity cascade each background retry is 3-10x gateway amplification and
|
|
286
|
+
// the user never sees those failures — the cheapest highest-leverage place
|
|
287
|
+
// to shed load. Overload-SCOPED: background 500s and transport blips keep
|
|
288
|
+
// the ordinary ladder.
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
/** The QuerySource allow-set that may retry on overload. Untagged calls are
|
|
291
|
+
* foreground by default (every pre-M70 caller keeps today's behavior).
|
|
292
|
+
* ω99-8b: "subagent" joins — core's runSubagent stamps child calls, and the
|
|
293
|
+
* waiting parent (often blocking a human) is foreground; leaving the tag out
|
|
294
|
+
* would silently demote child overload failures to background bail-out, a
|
|
295
|
+
* behavior change beyond ω99-8b's cost-partitioning scope. */
|
|
296
|
+
export const FOREGROUND_QUERY_SOURCES = new Set(["agent", "subagent"]);
|
|
297
|
+
/** True when the failure means "the gateway is out of capacity": the
|
|
298
|
+
* overload-status family (429 / 503 / 529, including a 529 folded into the
|
|
299
|
+
* message body) or a 429 whose body sniffs as fleet overload. */
|
|
300
|
+
export function isOverloadClassFailure(err) {
|
|
301
|
+
if (!(err instanceof ProviderHttpError))
|
|
302
|
+
return false;
|
|
303
|
+
return err.status === 429 || err.status === 503 || err.status === 529 || err.overload || /\b529\b/.test(err.message);
|
|
304
|
+
}
|
|
305
|
+
/** True when this (source, error) pair must NOT be retried: a background
|
|
306
|
+
* query hitting an overload-class failure. */
|
|
307
|
+
function isBackgroundNoRetry(querySource, err) {
|
|
308
|
+
return querySource !== undefined && !FOREGROUND_QUERY_SOURCES.has(querySource) && isOverloadClassFailure(err);
|
|
309
|
+
}
|
|
310
|
+
/** The typed SSE line-buffer overflow abort (r1b). A live stream that keeps
|
|
311
|
+
* delivering bytes but never emits a line/event boundary grows the line
|
|
312
|
+
* buffer without bound — a hole the idle watchdog cannot catch (every read
|
|
313
|
+
* resolves on time; the stream never goes quiet). Fail-loud, NEVER truncate:
|
|
314
|
+
* slicing an SSE line mid-event would fabricate garbage events downstream.
|
|
315
|
+
* Same ladder family as StreamIdleError: retryable transport class (the
|
|
316
|
+
* replay is byte-identical, the prefix cache is unaffected). */
|
|
317
|
+
export class StreamOverflowError extends Error {
|
|
318
|
+
maxChars;
|
|
319
|
+
constructor(maxChars) {
|
|
320
|
+
super(`sse line buffer exceeded ${maxChars} chars without an event boundary — stream overflow abort`);
|
|
321
|
+
this.name = "StreamOverflowError";
|
|
322
|
+
this.maxChars = maxChars;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** ω99-44 (pi ebc374490): the typed EOF-without-terminal abort. The stream
|
|
326
|
+
* ended CLEANLY (read → done, no transport error) but no finish/stop
|
|
327
|
+
* semantics ever arrived — byte-indistinguishable from a proxy EOF cut, so
|
|
328
|
+
* it fails LOUD instead of completing: a synthetic done would lose
|
|
329
|
+
* finish_reason (blinding the M70 stop-reason/truncated-tool-call guard),
|
|
330
|
+
* lose usage (the metering row), and feed empty completions into the
|
|
331
|
+
* zero-output heuristics. Attribution: which adapter's wire, and how many
|
|
332
|
+
* payload bytes arrived before the cut (the shared-sseDataLines consumers
|
|
333
|
+
* count decoded data-line chars — attribution granularity, not an exact
|
|
334
|
+
* wire count). Same ladder family as StreamIdleError/StreamOverflowError:
|
|
335
|
+
* retryable transport class — the pre-first-delta replay is byte-identical
|
|
336
|
+
* (prefix-cache safe); a post-delta cut propagates to the loop untouched
|
|
337
|
+
* (the ω99-9 midstream-recovery surface). */
|
|
338
|
+
export class TruncatedStreamError extends Error {
|
|
339
|
+
provider;
|
|
340
|
+
sawBytes;
|
|
341
|
+
constructor(provider, sawBytes) {
|
|
342
|
+
super(`stream ended without a terminal event (provider=${provider}, sawBytes=${sawBytes}) — truncated by the gateway or connection`);
|
|
343
|
+
this.name = "TruncatedStreamError";
|
|
344
|
+
this.provider = provider;
|
|
345
|
+
this.sawBytes = sawBytes;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Overload-class predicate for the ladder branch (and callers rendering
|
|
349
|
+
* retry state): true only for the 429+signature pair. */
|
|
350
|
+
export function isOverloadError(err) {
|
|
351
|
+
return err instanceof ProviderHttpError && err.overload;
|
|
352
|
+
}
|
|
353
|
+
/** WUDAO_MAX_RETRIES (integer ≥ 0); falls back to the default on junk. */
|
|
354
|
+
export function defaultMaxRetries(env = process.env) {
|
|
355
|
+
const raw = env.WUDAO_MAX_RETRIES;
|
|
356
|
+
if (raw === undefined || raw === "")
|
|
357
|
+
return DEFAULT_MAX_RETRIES;
|
|
358
|
+
const n = Number(raw);
|
|
359
|
+
return Number.isInteger(n) && n >= 0 ? n : DEFAULT_MAX_RETRIES;
|
|
360
|
+
}
|
|
361
|
+
/** Scheduled wait for the n-th retry (0-based): 500ms × 2ⁿ, capped, jittered. */
|
|
362
|
+
export function backoffDelayMs(attempt, opts = {}) {
|
|
363
|
+
const base = opts.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
364
|
+
const cap = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
365
|
+
const jitterRatio = opts.jitterRatio ?? DEFAULT_JITTER_RATIO;
|
|
366
|
+
const random = opts.random ?? Math.random;
|
|
367
|
+
const scheduled = Math.min(base * 2 ** attempt, cap);
|
|
368
|
+
return Math.max(0, scheduled * (1 + jitterRatio * (random() * 2 - 1)));
|
|
369
|
+
}
|
|
370
|
+
function defaultSleep(ms) {
|
|
371
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
372
|
+
}
|
|
373
|
+
/** ω99-315: duck-typed StreamIdleError recognizer (name-keyed like every
|
|
374
|
+
* other ladder classification — the class lives in stream-idle.ts but the
|
|
375
|
+
* ladder treats it as a wire-level vocabulary, not a JS instanceof). */
|
|
376
|
+
function isStreamIdleError(err) {
|
|
377
|
+
return err instanceof Error && err.name === "StreamIdleError";
|
|
378
|
+
}
|
|
379
|
+
/** The provider's own failure vocabulary, compact enough for a status line.
|
|
380
|
+
* M47 §7: overload-class failures name their class ("overload") so the UI
|
|
381
|
+
* can say WHY the wait is long — everything else keeps the status word. */
|
|
382
|
+
function retryReason(err) {
|
|
383
|
+
if (isOverloadError(err))
|
|
384
|
+
return "overload";
|
|
385
|
+
// ω99-42: the repair retry renders as its own reason, not a bare "400".
|
|
386
|
+
if (err instanceof Error && err.name === "ReasoningMandatoryError")
|
|
387
|
+
return "reasoning-mandatory";
|
|
388
|
+
if (err instanceof Error) {
|
|
389
|
+
const status = /^provider (?:request|stream) failed: (\d{3})/.exec(err.message)?.[1];
|
|
390
|
+
if (status !== undefined)
|
|
391
|
+
return status;
|
|
392
|
+
return truncateSafe(err.message.split("\n")[0] ?? "", 80) || "error"; // 113-A4
|
|
393
|
+
}
|
|
394
|
+
return "error";
|
|
395
|
+
}
|
|
396
|
+
/** The overload branch's wait (M47 §7): same doubling/cap/jitter shape as the
|
|
397
|
+
* generic ladder, but from the 2s overload base — a fleet-wide signal earns a
|
|
398
|
+
* longer first pause than a per-key rate limit. Same key, never rotate. */
|
|
399
|
+
export function overloadBackoffDelayMs(attempt, opts = {}) {
|
|
400
|
+
return backoffDelayMs(attempt, {
|
|
401
|
+
baseDelayMs: opts.overloadBaseDelayMs ?? opts.baseDelayMs ?? DEFAULT_OVERLOAD_BASE_DELAY_MS,
|
|
402
|
+
...(opts.maxDelayMs !== undefined ? { maxDelayMs: opts.maxDelayMs } : {}),
|
|
403
|
+
...(opts.jitterRatio !== undefined ? { jitterRatio: opts.jitterRatio } : {}),
|
|
404
|
+
...(opts.random !== undefined ? { random: opts.random } : {}),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/** Should this error walk the ladder? True for transport failures and the
|
|
408
|
+
* overloaded-status set; false for other 4xx, AbortError, context-length
|
|
409
|
+
* overflows (M58 §A — an oversized prompt can never succeed on retry), and
|
|
410
|
+
* semantic response-contract breaks (missing message shapes). */
|
|
411
|
+
/** Walk the `.cause` chain (bounded — cycles defensively impossible, but
|
|
412
|
+
* depth-capped anyway) for a fatal TLS code. */
|
|
413
|
+
function hasFatalTlsCause(err, depth = 0) {
|
|
414
|
+
if (!(err instanceof Error) || depth > 4)
|
|
415
|
+
return false;
|
|
416
|
+
if (FATAL_TLS_CODES.has(err.code))
|
|
417
|
+
return true;
|
|
418
|
+
return hasFatalTlsCause(err.cause, depth + 1);
|
|
419
|
+
}
|
|
420
|
+
/** 波次 8 #46 (hermes #12): the request-validation body vocabulary. */
|
|
421
|
+
const REQUEST_VALIDATION_BODY_SIGNATURES = [
|
|
422
|
+
/unknown parameter/i,
|
|
423
|
+
/unsupported parameter/i,
|
|
424
|
+
/unrecognized request argument/i,
|
|
425
|
+
/invalid_request_error/i,
|
|
426
|
+
/unknown_parameter/i,
|
|
427
|
+
/unsupported_parameter/i,
|
|
428
|
+
];
|
|
429
|
+
/** True when a failure body carries a known request-validation signature. */
|
|
430
|
+
function sniffRequestValidationBody(body) {
|
|
431
|
+
return REQUEST_VALIDATION_BODY_SIGNATURES.some((re) => re.test(body));
|
|
432
|
+
}
|
|
433
|
+
export function isRetryableProviderError(err) {
|
|
434
|
+
if (!(err instanceof Error))
|
|
435
|
+
return false;
|
|
436
|
+
if (err.name === "AbortError")
|
|
437
|
+
return false;
|
|
438
|
+
if (err.name === "ContextLengthError")
|
|
439
|
+
return false;
|
|
440
|
+
// 113-B3: an account-level quota window is park material, not ladder
|
|
441
|
+
// material — every rung would hammer an account the provider just said is
|
|
442
|
+
// capped for hours. Named explicitly (same family as ContextLengthError)
|
|
443
|
+
// so a future tightening of the fall-through below can never silently
|
|
444
|
+
// re-classify it.
|
|
445
|
+
if (err.name === "QuotaParkError")
|
|
446
|
+
return false;
|
|
447
|
+
// ω99-42: the typed mandatory-reasoning rejection rides the ladder ONLY in
|
|
448
|
+
// its repairable flavor — the retry is the repair (re-send with reasoning
|
|
449
|
+
// enabled, messages bytes untouched) and consumes one ladder slot. The
|
|
450
|
+
// terminal flavor (explicit user "none" / repair already tried / a format
|
|
451
|
+
// that cannot express enablement) fails fast like every deterministic 400.
|
|
452
|
+
if (err.name === "ReasoningMandatoryError")
|
|
453
|
+
return err.repairable;
|
|
454
|
+
// ω99-44: an EOF-without-terminal cut is transport-class — the replayed
|
|
455
|
+
// request is byte-identical (prefix-cache safe). Named explicitly so a
|
|
456
|
+
// future tightening of the fall-through below can never silently
|
|
457
|
+
// de-classify it (same family as StreamIdleError/StreamOverflowError).
|
|
458
|
+
if (err.name === "TruncatedStreamError")
|
|
459
|
+
return true;
|
|
460
|
+
// ω99-46: a proxy-attributed egress failure (typed ProxyEgressError in the
|
|
461
|
+
// cause chain — the global dispatcher's attribution arm) is DETERMINISTIC
|
|
462
|
+
// for this process: the ladder cannot heal a dead or refusing proxy, and
|
|
463
|
+
// every retry would burn backoff to reach the same named host:port. Fail
|
|
464
|
+
// fast with the attribution intact; the model-fallback chain (a different
|
|
465
|
+
// endpoint through the same egress) still runs.
|
|
466
|
+
if (err.name === "ProxyEgressError" || findProxyEgressCause(err) !== undefined)
|
|
467
|
+
return false;
|
|
468
|
+
// M79: a capability/role violation can never succeed on retry.
|
|
469
|
+
if (err.name === "UnsupportedContentError")
|
|
470
|
+
return false;
|
|
471
|
+
// ω99-50: an unrepresentable persona header is deterministic — the same
|
|
472
|
+
// bytes fail on every attempt. Fail fast (validatedInputEffort's M58 §C
|
|
473
|
+
// discipline); the model-fallback chain skips it too (isModelFallbackEligible).
|
|
474
|
+
if (err.name === "HeaderValidationError")
|
|
475
|
+
return false;
|
|
476
|
+
// 波次 8 #44 (hermes #12 ssl_cert_verification): a TLS certificate failure
|
|
477
|
+
// is DETERMINISTIC — the same handshake replays the same verification
|
|
478
|
+
// error, so retrying the same endpoint burns the whole ladder for nothing
|
|
479
|
+
// (and, on proxies, every extra TLS handshake is another slow failure).
|
|
480
|
+
// undici surfaces fetch rejects as TypeError("fetch failed") with the real
|
|
481
|
+
// cause one or more `.cause` hops down. Fail fast; the model-fallback chain
|
|
482
|
+
// (a DIFFERENT endpoint) is the correct escape, and it still runs.
|
|
483
|
+
if (hasFatalTlsCause(err))
|
|
484
|
+
return false;
|
|
485
|
+
// Some gateways fold 529 (overloaded) into the body instead of the status.
|
|
486
|
+
const mentions529 = /\b529\b/.test(err.message);
|
|
487
|
+
const status = /^provider (?:request|stream) failed: (\d{3})/.exec(err.message)?.[1];
|
|
488
|
+
if (status !== undefined) {
|
|
489
|
+
if (RETRYABLE_STATUSES.has(Number(status))) {
|
|
490
|
+
// 波次 8 #46 (hermes #12): some OpenAI-compatible gateways return
|
|
491
|
+
// DETERMINISTIC request-validation errors with a 5xx status — the
|
|
492
|
+
// generic "5xx → retryable" rule then hammers the identical rejection
|
|
493
|
+
// once per ladder slot. When the body carries one of these unambiguous
|
|
494
|
+
// validation signals, the request is malformed and every retry gets
|
|
495
|
+
// the same answer: fail fast. The model-fallback chain (a DIFFERENT
|
|
496
|
+
// endpoint) is the correct escape, and it still runs. Hermes' proven
|
|
497
|
+
// six patterns, verbatim family. (The context-length family never
|
|
498
|
+
// reaches here: those classify into the typed ContextLengthError at
|
|
499
|
+
// the HTTP boundary, which already returned false above.)
|
|
500
|
+
if (Number(status) >= 500 && sniffRequestValidationBody(err instanceof ProviderHttpError ? err.body : err.message)) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
return mentions529;
|
|
506
|
+
}
|
|
507
|
+
if (mentions529)
|
|
508
|
+
return true;
|
|
509
|
+
if (err.message.startsWith("provider response missing"))
|
|
510
|
+
return false;
|
|
511
|
+
// Everything else escaping the transport boundary: fetch rejects and
|
|
512
|
+
// truncated/SSE-breaking bodies — transient by nature.
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
/** The error's server deadline, if any (r1a Task 4). The ladder treats it as
|
|
516
|
+
* an explicit wait that REPLACES the exponential guess — exact value, no
|
|
517
|
+
* jitter — while still consuming one ladder slot per retry. */
|
|
518
|
+
function retryAfterMsOf(err) {
|
|
519
|
+
return err instanceof ProviderHttpError ? err.retryAfterMs : undefined;
|
|
520
|
+
}
|
|
521
|
+
/** Retry a promise-returning attempt on the ladder. M47 §7: overload-class
|
|
522
|
+
* failures take the overload branch (same key, longer doubling waits).
|
|
523
|
+
* M70 §3: a background QuerySource never retries an overload — it fails
|
|
524
|
+
* immediately with the typed reason (no wait, no re-issue).
|
|
525
|
+
* r1a Task 4: a server Retry-After beyond the fail cap (60s) is honored as a
|
|
526
|
+
* hard stop — no retry, the original error surfaces as-is; within it, the
|
|
527
|
+
* deadline replaces the backoff value (the wait source only — same sleep,
|
|
528
|
+
* same ladder slots, zero second time mechanism). */
|
|
529
|
+
export async function withRetry(fn, opts = {}) {
|
|
530
|
+
const baseMaxRetries = opts.maxRetries ?? defaultMaxRetries();
|
|
531
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
532
|
+
// 124 项 2: per-failure effective cap. A network-class failure when the
|
|
533
|
+
// grace window is enabled widens maxRetries × multiplier and lifts the
|
|
534
|
+
// per-wait cap to maxDelayMsMs (default 5min). 4xx/5xx business errors
|
|
535
|
+
// and deterministic typed failures always see the base cap — the network
|
|
536
|
+
// test never silently broadens a 401 or 503 quota into a 5-minute wait.
|
|
537
|
+
const graceEnabled = opts.networkGrace?.enabled === true;
|
|
538
|
+
const graceMultiplier = opts.networkGrace?.retryMultiplier ?? DEFAULT_NETWORK_GRACE_RETRY_MULTIPLIER;
|
|
539
|
+
const graceMaxDelayMs = opts.networkGrace?.maxDelayMsMs ?? DEFAULT_NETWORK_GRACE_MAX_DELAY_MS;
|
|
540
|
+
let graceEntered = false;
|
|
541
|
+
for (let attempt = 0;; attempt += 1) {
|
|
542
|
+
try {
|
|
543
|
+
return await fn();
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
// 121 项 1: the capacity observer fires FIRST — every overload-class
|
|
547
|
+
// observation is reported whatever the ladder then decides (retry,
|
|
548
|
+
// background bail, or rethrow past the cap).
|
|
549
|
+
if (isOverloadClassFailure(err))
|
|
550
|
+
opts.onOverload?.();
|
|
551
|
+
// M70 §3: the foreground-only allowlist runs BEFORE the ladder.
|
|
552
|
+
if (isBackgroundNoRetry(opts.querySource, err))
|
|
553
|
+
throw new BackgroundOverloadError(opts.querySource, err);
|
|
554
|
+
// r1a Task 4: the server's explicit deadline beyond the fail cap — a
|
|
555
|
+
// wait longer than a minute is the provider saying "stop", not "back
|
|
556
|
+
// off". Exactly RETRY_AFTER_FAIL_CAP_MS is allowed (the check is >).
|
|
557
|
+
const retryAfterMs = retryAfterMsOf(err);
|
|
558
|
+
if (retryAfterMs !== undefined && retryAfterMs > RETRY_AFTER_FAIL_CAP_MS)
|
|
559
|
+
throw err;
|
|
560
|
+
if (!isRetryableProviderError(err))
|
|
561
|
+
throw err;
|
|
562
|
+
// 124 项 2: network-class failures widen the ladder when grace is on;
|
|
563
|
+
// 4xx/5xx/typed failures keep the base cap so their existing behavior
|
|
564
|
+
// is byte-stable. The grace window fires onEnter on the FIRST observed
|
|
565
|
+
// network failure in this lifetime (idempotent across attempts).
|
|
566
|
+
const networkClass = graceEnabled && isNetworkClassFailure(err);
|
|
567
|
+
if (networkClass && !graceEntered) {
|
|
568
|
+
graceEntered = true;
|
|
569
|
+
opts.networkGrace?.onEnter?.();
|
|
570
|
+
}
|
|
571
|
+
const effectiveMaxRetries = networkClass ? baseMaxRetries * graceMultiplier : baseMaxRetries;
|
|
572
|
+
const effectiveMaxDelayMs = networkClass ? graceMaxDelayMs : opts.maxDelayMs;
|
|
573
|
+
if (attempt >= effectiveMaxRetries)
|
|
574
|
+
throw err;
|
|
575
|
+
// ω99-315 classification backoff: an idle abort is NOT server overload —
|
|
576
|
+
// the connection died, so the retry is a DIRECT resend (0 wait; the M24
|
|
577
|
+
// byte-stable resent request rides the provider prefix cache and costs
|
|
578
|
+
// ≈ incremental tokens — a backoff would only add dead time). 429 keeps
|
|
579
|
+
// honoring Retry-After; 5xx/network keep the exponential rungs.
|
|
580
|
+
const delayOpts = networkClass
|
|
581
|
+
? { baseDelayMs: opts.baseDelayMs, jitterRatio: opts.jitterRatio, random: opts.random, overloadBaseDelayMs: opts.overloadBaseDelayMs }
|
|
582
|
+
: { baseDelayMs: opts.baseDelayMs, maxDelayMs: effectiveMaxDelayMs, jitterRatio: opts.jitterRatio, random: opts.random, overloadBaseDelayMs: opts.overloadBaseDelayMs };
|
|
583
|
+
const delayMs = isStreamIdleError(err)
|
|
584
|
+
? 0
|
|
585
|
+
: retryAfterMs ?? (isOverloadError(err) ? overloadBackoffDelayMs(attempt, delayOpts) : backoffDelayMs(attempt, delayOpts));
|
|
586
|
+
opts.onRetry?.(attempt + 1, delayMs, retryReason(err));
|
|
587
|
+
probeModelRetry({ attempt: attempt + 1, delayMs, reason: retryReason(err) });
|
|
588
|
+
await sleep(delayMs);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/** Retry a generator-producing attempt — but ONLY until the first event has
|
|
593
|
+
* been yielded downstream. After that, an error is rethrown untouched: a
|
|
594
|
+
* replay would duplicate already-delivered output.
|
|
595
|
+
* r1a Task 4: same Retry-After semantics as withRetry (fail cap + the
|
|
596
|
+
* deadline replacing the backoff value; pre-delta retries only). */
|
|
597
|
+
export async function* withRetryStream(fn, opts = {}) {
|
|
598
|
+
const baseMaxRetries = opts.maxRetries ?? defaultMaxRetries();
|
|
599
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
600
|
+
// 124 项 2: same per-failure effective cap as withRetry — see the block
|
|
601
|
+
// comment there. Stream-side widening keeps the byte-stable contract
|
|
602
|
+
// (only pre-delta retries ever run; a post-delta break rethrows untouched).
|
|
603
|
+
const graceEnabled = opts.networkGrace?.enabled === true;
|
|
604
|
+
const graceMultiplier = opts.networkGrace?.retryMultiplier ?? DEFAULT_NETWORK_GRACE_RETRY_MULTIPLIER;
|
|
605
|
+
const graceMaxDelayMs = opts.networkGrace?.maxDelayMsMs ?? DEFAULT_NETWORK_GRACE_MAX_DELAY_MS;
|
|
606
|
+
let graceEntered = false;
|
|
607
|
+
for (let attempt = 0;; attempt += 1) {
|
|
608
|
+
let produced = false;
|
|
609
|
+
try {
|
|
610
|
+
for await (const ev of fn()) {
|
|
611
|
+
produced = true;
|
|
612
|
+
yield ev;
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
// 121 项 1: same capacity observer as withRetry — the FIRST statement
|
|
618
|
+
// of the catch, so it fires for post-delta breaks too (the overload
|
|
619
|
+
// observation is real capacity evidence even when the stream cannot be
|
|
620
|
+
// replayed and must rethrow).
|
|
621
|
+
if (isOverloadClassFailure(err))
|
|
622
|
+
opts.onOverload?.();
|
|
623
|
+
// M70 §3: the foreground-only allowlist runs before the ladder — a
|
|
624
|
+
// background overload never replays the stream.
|
|
625
|
+
if (isBackgroundNoRetry(opts.querySource, err))
|
|
626
|
+
throw new BackgroundOverloadError(opts.querySource, err);
|
|
627
|
+
// r1a Task 4: beyond the fail cap the stream error is rethrown
|
|
628
|
+
// untouched (no wait) — same rule as withRetry.
|
|
629
|
+
const retryAfterMs = retryAfterMsOf(err);
|
|
630
|
+
if (retryAfterMs !== undefined && retryAfterMs > RETRY_AFTER_FAIL_CAP_MS)
|
|
631
|
+
throw err;
|
|
632
|
+
if (produced)
|
|
633
|
+
throw err; // post-delta: rethrow untouched (no replay)
|
|
634
|
+
if (!isRetryableProviderError(err))
|
|
635
|
+
throw err;
|
|
636
|
+
// 124 项 2: same grace-widening logic as withRetry — the effective
|
|
637
|
+
// retry cap here is the SAME effectiveMaxRetries (network-class widens
|
|
638
|
+
// it; 4xx/5xx/deterministic failures stay on the base cap).
|
|
639
|
+
const networkClass = graceEnabled && isNetworkClassFailure(err);
|
|
640
|
+
if (networkClass && !graceEntered) {
|
|
641
|
+
graceEntered = true;
|
|
642
|
+
opts.networkGrace?.onEnter?.();
|
|
643
|
+
}
|
|
644
|
+
const effectiveMaxRetries = networkClass ? baseMaxRetries * graceMultiplier : baseMaxRetries;
|
|
645
|
+
const effectiveMaxDelayMs = networkClass ? graceMaxDelayMs : opts.maxDelayMs;
|
|
646
|
+
if (attempt < effectiveMaxRetries) {
|
|
647
|
+
// ω99-315: same classification backoff as withRetry — idle resends
|
|
648
|
+
// directly (0 wait), everything else keeps its ladder rung.
|
|
649
|
+
const delayOpts = networkClass
|
|
650
|
+
? { baseDelayMs: opts.baseDelayMs, jitterRatio: opts.jitterRatio, random: opts.random, overloadBaseDelayMs: opts.overloadBaseDelayMs }
|
|
651
|
+
: { baseDelayMs: opts.baseDelayMs, maxDelayMs: effectiveMaxDelayMs, jitterRatio: opts.jitterRatio, random: opts.random, overloadBaseDelayMs: opts.overloadBaseDelayMs };
|
|
652
|
+
const delayMs = isStreamIdleError(err)
|
|
653
|
+
? 0
|
|
654
|
+
: retryAfterMs ?? (isOverloadError(err) ? overloadBackoffDelayMs(attempt, delayOpts) : backoffDelayMs(attempt, delayOpts));
|
|
655
|
+
opts.onRetry?.(attempt + 1, delayMs, retryReason(err));
|
|
656
|
+
probeModelRetry({ attempt: attempt + 1, delayMs, reason: retryReason(err) });
|
|
657
|
+
await sleep(delayMs);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
throw err;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
//# sourceMappingURL=retry-ladder.js.map
|