@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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/anthropic.d.ts +19 -0
  5. package/dist/anthropic.d.ts.map +1 -0
  6. package/dist/anthropic.js +621 -0
  7. package/dist/anthropic.js.map +1 -0
  8. package/dist/balance.d.ts +28 -0
  9. package/dist/balance.d.ts.map +1 -0
  10. package/dist/balance.js +105 -0
  11. package/dist/balance.js.map +1 -0
  12. package/dist/config.d.ts +328 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +623 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/dispatcher.d.ts +5 -0
  17. package/dist/dispatcher.d.ts.map +1 -0
  18. package/dist/dispatcher.js +21 -0
  19. package/dist/dispatcher.js.map +1 -0
  20. package/dist/effort-catalog.d.ts +17 -0
  21. package/dist/effort-catalog.d.ts.map +1 -0
  22. package/dist/effort-catalog.js +73 -0
  23. package/dist/effort-catalog.js.map +1 -0
  24. package/dist/error-body.d.ts +32 -0
  25. package/dist/error-body.d.ts.map +1 -0
  26. package/dist/error-body.js +74 -0
  27. package/dist/error-body.js.map +1 -0
  28. package/dist/header-validation.d.ts +57 -0
  29. package/dist/header-validation.d.ts.map +1 -0
  30. package/dist/header-validation.js +130 -0
  31. package/dist/header-validation.js.map +1 -0
  32. package/dist/index.d.ts +42 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +110 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/json-arguments.d.ts +17 -0
  37. package/dist/json-arguments.d.ts.map +1 -0
  38. package/dist/json-arguments.js +227 -0
  39. package/dist/json-arguments.js.map +1 -0
  40. package/dist/key-pool.d.ts +66 -0
  41. package/dist/key-pool.d.ts.map +1 -0
  42. package/dist/key-pool.js +207 -0
  43. package/dist/key-pool.js.map +1 -0
  44. package/dist/missing-reasoning-circuit.d.ts +17 -0
  45. package/dist/missing-reasoning-circuit.d.ts.map +1 -0
  46. package/dist/missing-reasoning-circuit.js +38 -0
  47. package/dist/missing-reasoning-circuit.js.map +1 -0
  48. package/dist/model-failure.d.ts +156 -0
  49. package/dist/model-failure.d.ts.map +1 -0
  50. package/dist/model-failure.js +253 -0
  51. package/dist/model-failure.js.map +1 -0
  52. package/dist/model-fallback.d.ts +18 -0
  53. package/dist/model-fallback.d.ts.map +1 -0
  54. package/dist/model-fallback.js +87 -0
  55. package/dist/model-fallback.js.map +1 -0
  56. package/dist/models.d.ts +20 -0
  57. package/dist/models.d.ts.map +1 -0
  58. package/dist/models.js +55 -0
  59. package/dist/models.js.map +1 -0
  60. package/dist/openai-compatible.d.ts +87 -0
  61. package/dist/openai-compatible.d.ts.map +1 -0
  62. package/dist/openai-compatible.js +368 -0
  63. package/dist/openai-compatible.js.map +1 -0
  64. package/dist/openai-engine.d.ts +18 -0
  65. package/dist/openai-engine.d.ts.map +1 -0
  66. package/dist/openai-engine.js +515 -0
  67. package/dist/openai-engine.js.map +1 -0
  68. package/dist/openai-images.d.ts +38 -0
  69. package/dist/openai-images.d.ts.map +1 -0
  70. package/dist/openai-images.js +301 -0
  71. package/dist/openai-images.js.map +1 -0
  72. package/dist/openai-messages.d.ts +40 -0
  73. package/dist/openai-messages.d.ts.map +1 -0
  74. package/dist/openai-messages.js +135 -0
  75. package/dist/openai-messages.js.map +1 -0
  76. package/dist/openai-wire.d.ts +93 -0
  77. package/dist/openai-wire.d.ts.map +1 -0
  78. package/dist/openai-wire.js +151 -0
  79. package/dist/openai-wire.js.map +1 -0
  80. package/dist/persona/apply.d.ts +19 -0
  81. package/dist/persona/apply.d.ts.map +1 -0
  82. package/dist/persona/apply.js +98 -0
  83. package/dist/persona/apply.js.map +1 -0
  84. package/dist/persona/builtins.d.ts +3 -0
  85. package/dist/persona/builtins.d.ts.map +1 -0
  86. package/dist/persona/builtins.js +180 -0
  87. package/dist/persona/builtins.js.map +1 -0
  88. package/dist/persona/decorator.d.ts +4 -0
  89. package/dist/persona/decorator.d.ts.map +1 -0
  90. package/dist/persona/decorator.js +112 -0
  91. package/dist/persona/decorator.js.map +1 -0
  92. package/dist/persona/files.d.ts +20 -0
  93. package/dist/persona/files.d.ts.map +1 -0
  94. package/dist/persona/files.js +84 -0
  95. package/dist/persona/files.js.map +1 -0
  96. package/dist/persona/ids.d.ts +9 -0
  97. package/dist/persona/ids.d.ts.map +1 -0
  98. package/dist/persona/ids.js +11 -0
  99. package/dist/persona/ids.js.map +1 -0
  100. package/dist/persona/registry.d.ts +12 -0
  101. package/dist/persona/registry.d.ts.map +1 -0
  102. package/dist/persona/registry.js +54 -0
  103. package/dist/persona/registry.js.map +1 -0
  104. package/dist/persona/runtime.d.ts +23 -0
  105. package/dist/persona/runtime.d.ts.map +1 -0
  106. package/dist/persona/runtime.js +32 -0
  107. package/dist/persona/runtime.js.map +1 -0
  108. package/dist/persona/schema.d.ts +27 -0
  109. package/dist/persona/schema.d.ts.map +1 -0
  110. package/dist/persona/schema.js +165 -0
  111. package/dist/persona/schema.js.map +1 -0
  112. package/dist/persona/types.d.ts +38 -0
  113. package/dist/persona/types.d.ts.map +1 -0
  114. package/dist/persona/types.js +2 -0
  115. package/dist/persona/types.js.map +1 -0
  116. package/dist/pricing.d.ts +40 -0
  117. package/dist/pricing.d.ts.map +1 -0
  118. package/dist/pricing.js +70 -0
  119. package/dist/pricing.js.map +1 -0
  120. package/dist/quota-park.d.ts +53 -0
  121. package/dist/quota-park.d.ts.map +1 -0
  122. package/dist/quota-park.js +135 -0
  123. package/dist/quota-park.js.map +1 -0
  124. package/dist/rate-pause.d.ts +28 -0
  125. package/dist/rate-pause.d.ts.map +1 -0
  126. package/dist/rate-pause.js +72 -0
  127. package/dist/rate-pause.js.map +1 -0
  128. package/dist/registry.d.ts +142 -0
  129. package/dist/registry.d.ts.map +1 -0
  130. package/dist/registry.js +471 -0
  131. package/dist/registry.js.map +1 -0
  132. package/dist/responses.d.ts +5 -0
  133. package/dist/responses.d.ts.map +1 -0
  134. package/dist/responses.js +290 -0
  135. package/dist/responses.js.map +1 -0
  136. package/dist/retry-after.d.ts +13 -0
  137. package/dist/retry-after.d.ts.map +1 -0
  138. package/dist/retry-after.js +64 -0
  139. package/dist/retry-after.js.map +1 -0
  140. package/dist/retry-ladder.d.ts +176 -0
  141. package/dist/retry-ladder.d.ts.map +1 -0
  142. package/dist/retry-ladder.js +664 -0
  143. package/dist/retry-ladder.js.map +1 -0
  144. package/dist/split-inband-thinking.d.ts +35 -0
  145. package/dist/split-inband-thinking.d.ts.map +1 -0
  146. package/dist/split-inband-thinking.js +117 -0
  147. package/dist/split-inband-thinking.js.map +1 -0
  148. package/dist/stream-idle.d.ts +97 -0
  149. package/dist/stream-idle.d.ts.map +1 -0
  150. package/dist/stream-idle.js +127 -0
  151. package/dist/stream-idle.js.map +1 -0
  152. package/dist/tool-call-ids.d.ts +16 -0
  153. package/dist/tool-call-ids.d.ts.map +1 -0
  154. package/dist/tool-call-ids.js +36 -0
  155. package/dist/tool-call-ids.js.map +1 -0
  156. 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