@juspay/neurolink 10.8.4 → 10.8.6

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 (217) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/browser/neurolink.min.js +497 -478
  3. package/dist/factories/providerRegistry.js +11 -11
  4. package/dist/lib/factories/providerRegistry.js +11 -11
  5. package/dist/lib/providers/{amazonBedrock.d.ts → amazonBedrock/client.d.ts} +4 -4
  6. package/dist/{providers/amazonBedrock.js → lib/providers/amazonBedrock/client.js} +15 -18
  7. package/dist/lib/providers/amazonBedrock/constants.d.ts +1 -0
  8. package/dist/lib/providers/amazonBedrock/constants.js +3 -0
  9. package/dist/lib/providers/amazonBedrock/index.d.ts +3 -0
  10. package/dist/lib/providers/amazonBedrock/index.js +4 -0
  11. package/dist/lib/providers/amazonBedrock/utils.d.ts +1 -0
  12. package/dist/lib/providers/amazonBedrock/utils.js +4 -0
  13. package/dist/{providers/anthropic.d.ts → lib/providers/anthropic/client.d.ts} +6 -21
  14. package/dist/{providers/anthropic.js → lib/providers/anthropic/client.js} +28 -45
  15. package/dist/lib/providers/anthropic/constants.d.ts +4 -0
  16. package/dist/lib/providers/anthropic/constants.js +9 -0
  17. package/dist/lib/providers/anthropic/index.d.ts +3 -0
  18. package/dist/lib/providers/anthropic/index.js +4 -0
  19. package/dist/lib/providers/anthropic/utils.d.ts +7 -0
  20. package/dist/lib/providers/anthropic/utils.js +150 -0
  21. package/dist/lib/providers/{googleAiStudio.d.ts → googleAiStudio/client.d.ts} +4 -4
  22. package/dist/{providers/googleAiStudio.js → lib/providers/googleAiStudio/client.js} +16 -15
  23. package/dist/lib/providers/googleAiStudio/index.d.ts +2 -0
  24. package/dist/lib/providers/googleAiStudio/index.js +3 -0
  25. package/dist/lib/providers/googleAiStudio/utils.d.ts +2 -0
  26. package/dist/lib/providers/googleAiStudio/utils.js +19 -0
  27. package/dist/lib/providers/googleNativeGemini3/constants.d.ts +1 -0
  28. package/dist/lib/providers/googleNativeGemini3/constants.js +2 -0
  29. package/dist/lib/providers/googleNativeGemini3/index.d.ts +2 -0
  30. package/dist/lib/providers/googleNativeGemini3/index.js +3 -0
  31. package/dist/lib/providers/{googleNativeGemini3.d.ts → googleNativeGemini3/utils.d.ts} +2 -2
  32. package/dist/lib/providers/{googleNativeGemini3.js → googleNativeGemini3/utils.js} +8 -8
  33. package/dist/{providers/googleVertex.d.ts → lib/providers/googleVertex/client.d.ts} +4 -4
  34. package/dist/lib/providers/{googleVertex.js → googleVertex/client.js} +32 -32
  35. package/dist/lib/providers/googleVertex/constants.d.ts +5 -0
  36. package/dist/lib/providers/googleVertex/constants.js +27 -0
  37. package/dist/lib/providers/googleVertex/index.d.ts +3 -0
  38. package/dist/lib/providers/googleVertex/index.js +4 -0
  39. package/dist/lib/providers/googleVertex/utils.d.ts +12 -0
  40. package/dist/lib/providers/googleVertex/utils.js +287 -0
  41. package/dist/lib/providers/{huggingFace.d.ts → huggingFace/client.d.ts} +3 -3
  42. package/dist/lib/providers/{huggingFace.js → huggingFace/client.js} +7 -7
  43. package/dist/lib/providers/huggingFace/index.d.ts +2 -0
  44. package/dist/lib/providers/huggingFace/index.js +3 -0
  45. package/dist/lib/providers/huggingFace/utils.d.ts +2 -0
  46. package/dist/lib/providers/huggingFace/utils.js +8 -0
  47. package/dist/lib/providers/index.d.ts +10 -10
  48. package/dist/lib/providers/index.js +10 -10
  49. package/dist/{providers/litellm.d.ts → lib/providers/litellm/client.d.ts} +3 -3
  50. package/dist/lib/providers/{litellm.js → litellm/client.js} +13 -13
  51. package/dist/lib/providers/litellm/constants.d.ts +1 -0
  52. package/dist/lib/providers/litellm/constants.js +3 -0
  53. package/dist/lib/providers/litellm/index.d.ts +3 -0
  54. package/dist/lib/providers/litellm/index.js +4 -0
  55. package/dist/lib/providers/litellm/utils.d.ts +5 -0
  56. package/dist/lib/providers/litellm/utils.js +11 -0
  57. package/dist/{providers/nvidiaNim.d.ts → lib/providers/nvidiaNim/client.d.ts} +3 -3
  58. package/dist/lib/providers/{nvidiaNim.js → nvidiaNim/client.js} +8 -8
  59. package/dist/lib/providers/nvidiaNim/constants.d.ts +1 -0
  60. package/dist/lib/providers/nvidiaNim/constants.js +2 -0
  61. package/dist/lib/providers/nvidiaNim/index.d.ts +3 -0
  62. package/dist/lib/providers/nvidiaNim/index.js +4 -0
  63. package/dist/lib/providers/nvidiaNim/utils.d.ts +9 -0
  64. package/dist/lib/providers/nvidiaNim/utils.js +101 -0
  65. package/dist/{providers/ollama.d.ts → lib/providers/ollama/client.d.ts} +3 -3
  66. package/dist/{providers/ollama.js → lib/providers/ollama/client.js} +10 -13
  67. package/dist/lib/providers/ollama/constants.d.ts +4 -0
  68. package/dist/lib/providers/ollama/constants.js +6 -0
  69. package/dist/lib/providers/ollama/index.d.ts +3 -0
  70. package/dist/lib/providers/ollama/index.js +4 -0
  71. package/dist/lib/providers/ollama/utils.d.ts +8 -0
  72. package/dist/lib/providers/ollama/utils.js +43 -0
  73. package/dist/lib/providers/{openAI.d.ts → openAI/client.d.ts} +3 -3
  74. package/dist/lib/providers/{openAI.js → openAI/client.js} +13 -13
  75. package/dist/lib/providers/openAI/constants.d.ts +1 -0
  76. package/dist/lib/providers/openAI/constants.js +3 -0
  77. package/dist/lib/providers/openAI/index.d.ts +3 -0
  78. package/dist/lib/providers/openAI/index.js +4 -0
  79. package/dist/lib/providers/openAI/utils.d.ts +2 -0
  80. package/dist/lib/providers/openAI/utils.js +8 -0
  81. package/dist/lib/providers/{openRouter.d.ts → openRouter/client.d.ts} +3 -3
  82. package/dist/{providers/openRouter.js → lib/providers/openRouter/client.js} +13 -29
  83. package/dist/lib/providers/openRouter/constants.d.ts +1 -0
  84. package/dist/lib/providers/openRouter/constants.js +2 -0
  85. package/dist/lib/providers/openRouter/index.d.ts +3 -0
  86. package/dist/lib/providers/openRouter/index.js +4 -0
  87. package/dist/lib/providers/openRouter/utils.d.ts +3 -0
  88. package/dist/lib/providers/openRouter/utils.js +18 -0
  89. package/dist/lib/providers/{openaiCompatible.d.ts → openaiCompatible/client.d.ts} +2 -2
  90. package/dist/{providers/openaiCompatible.js → lib/providers/openaiCompatible/client.js} +8 -22
  91. package/dist/lib/providers/openaiCompatible/constants.d.ts +1 -0
  92. package/dist/lib/providers/openaiCompatible/constants.js +2 -0
  93. package/dist/lib/providers/openaiCompatible/index.d.ts +3 -0
  94. package/dist/lib/providers/openaiCompatible/index.js +4 -0
  95. package/dist/lib/providers/openaiCompatible/utils.d.ts +5 -0
  96. package/dist/lib/providers/openaiCompatible/utils.js +17 -0
  97. package/dist/lib/proxy/modelRouter.d.ts +10 -0
  98. package/dist/lib/proxy/modelRouter.js +17 -0
  99. package/dist/lib/proxy/proxyConfig.js +46 -0
  100. package/dist/lib/proxy/routingPolicy.d.ts +4 -2
  101. package/dist/lib/proxy/routingPolicy.js +8 -5
  102. package/dist/lib/proxy/runtimeConfig.js +2 -0
  103. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +52 -1
  104. package/dist/lib/server/routes/claudeProxyRoutes.js +467 -221
  105. package/dist/lib/server/routes/openaiProxyRoutes.js +1 -1
  106. package/dist/lib/types/errors.d.ts +5 -0
  107. package/dist/lib/types/proxy.d.ts +24 -0
  108. package/dist/lib/types/subscription.d.ts +4 -0
  109. package/dist/lib/utils/providerHealth.js +2 -1
  110. package/dist/lib/utils/providerUtils.js +2 -1
  111. package/dist/providers/{amazonBedrock.d.ts → amazonBedrock/client.d.ts} +4 -4
  112. package/dist/{lib/providers/amazonBedrock.js → providers/amazonBedrock/client.js} +14 -19
  113. package/dist/providers/amazonBedrock/constants.d.ts +1 -0
  114. package/dist/providers/amazonBedrock/constants.js +2 -0
  115. package/dist/providers/amazonBedrock/index.d.ts +3 -0
  116. package/dist/providers/amazonBedrock/index.js +3 -0
  117. package/dist/providers/amazonBedrock/utils.d.ts +1 -0
  118. package/dist/providers/amazonBedrock/utils.js +3 -0
  119. package/dist/{lib/providers/anthropic.d.ts → providers/anthropic/client.d.ts} +6 -21
  120. package/dist/{lib/providers/anthropic.js → providers/anthropic/client.js} +27 -46
  121. package/dist/providers/anthropic/constants.d.ts +4 -0
  122. package/dist/providers/anthropic/constants.js +8 -0
  123. package/dist/providers/anthropic/index.d.ts +3 -0
  124. package/dist/providers/anthropic/index.js +3 -0
  125. package/dist/providers/anthropic/utils.d.ts +7 -0
  126. package/dist/providers/anthropic/utils.js +149 -0
  127. package/dist/providers/{googleAiStudio.d.ts → googleAiStudio/client.d.ts} +4 -4
  128. package/dist/{lib/providers/googleAiStudio.js → providers/googleAiStudio/client.js} +15 -16
  129. package/dist/providers/googleAiStudio/index.d.ts +2 -0
  130. package/dist/providers/googleAiStudio/index.js +2 -0
  131. package/dist/providers/googleAiStudio/utils.d.ts +2 -0
  132. package/dist/providers/googleAiStudio/utils.js +18 -0
  133. package/dist/providers/googleNativeGemini3/constants.d.ts +1 -0
  134. package/dist/providers/googleNativeGemini3/constants.js +1 -0
  135. package/dist/providers/googleNativeGemini3/index.d.ts +2 -0
  136. package/dist/providers/googleNativeGemini3/index.js +2 -0
  137. package/dist/providers/{googleNativeGemini3.d.ts → googleNativeGemini3/utils.d.ts} +2 -2
  138. package/dist/providers/{googleNativeGemini3.js → googleNativeGemini3/utils.js} +7 -7
  139. package/dist/{lib/providers/googleVertex.d.ts → providers/googleVertex/client.d.ts} +4 -4
  140. package/dist/providers/{googleVertex.js → googleVertex/client.js} +31 -31
  141. package/dist/providers/googleVertex/constants.d.ts +5 -0
  142. package/dist/providers/googleVertex/constants.js +26 -0
  143. package/dist/providers/googleVertex/index.d.ts +3 -0
  144. package/dist/providers/googleVertex/index.js +3 -0
  145. package/dist/providers/googleVertex/utils.d.ts +12 -0
  146. package/dist/providers/googleVertex/utils.js +286 -0
  147. package/dist/providers/{huggingFace.d.ts → huggingFace/client.d.ts} +3 -3
  148. package/dist/providers/{huggingFace.js → huggingFace/client.js} +6 -6
  149. package/dist/providers/huggingFace/index.d.ts +2 -0
  150. package/dist/providers/huggingFace/index.js +2 -0
  151. package/dist/providers/huggingFace/utils.d.ts +2 -0
  152. package/dist/providers/huggingFace/utils.js +7 -0
  153. package/dist/providers/index.d.ts +10 -10
  154. package/dist/providers/index.js +10 -10
  155. package/dist/{lib/providers/litellm.d.ts → providers/litellm/client.d.ts} +3 -3
  156. package/dist/providers/{litellm.js → litellm/client.js} +12 -12
  157. package/dist/providers/litellm/constants.d.ts +1 -0
  158. package/dist/providers/litellm/constants.js +2 -0
  159. package/dist/providers/litellm/index.d.ts +3 -0
  160. package/dist/providers/litellm/index.js +3 -0
  161. package/dist/providers/litellm/utils.d.ts +5 -0
  162. package/dist/providers/litellm/utils.js +10 -0
  163. package/dist/{lib/providers/nvidiaNim.d.ts → providers/nvidiaNim/client.d.ts} +3 -3
  164. package/dist/providers/{nvidiaNim.js → nvidiaNim/client.js} +7 -7
  165. package/dist/providers/nvidiaNim/constants.d.ts +1 -0
  166. package/dist/providers/nvidiaNim/constants.js +1 -0
  167. package/dist/providers/nvidiaNim/index.d.ts +3 -0
  168. package/dist/providers/nvidiaNim/index.js +3 -0
  169. package/dist/providers/nvidiaNim/utils.d.ts +9 -0
  170. package/dist/providers/nvidiaNim/utils.js +100 -0
  171. package/dist/{lib/providers/ollama.d.ts → providers/ollama/client.d.ts} +3 -3
  172. package/dist/{lib/providers/ollama.js → providers/ollama/client.js} +9 -14
  173. package/dist/providers/ollama/constants.d.ts +4 -0
  174. package/dist/providers/ollama/constants.js +5 -0
  175. package/dist/providers/ollama/index.d.ts +3 -0
  176. package/dist/providers/ollama/index.js +3 -0
  177. package/dist/providers/ollama/utils.d.ts +8 -0
  178. package/dist/providers/ollama/utils.js +42 -0
  179. package/dist/providers/{openAI.d.ts → openAI/client.d.ts} +3 -3
  180. package/dist/providers/{openAI.js → openAI/client.js} +12 -12
  181. package/dist/providers/openAI/constants.d.ts +1 -0
  182. package/dist/providers/openAI/constants.js +2 -0
  183. package/dist/providers/openAI/index.d.ts +3 -0
  184. package/dist/providers/openAI/index.js +3 -0
  185. package/dist/providers/openAI/utils.d.ts +2 -0
  186. package/dist/providers/openAI/utils.js +7 -0
  187. package/dist/providers/{openRouter.d.ts → openRouter/client.d.ts} +3 -3
  188. package/dist/{lib/providers/openRouter.js → providers/openRouter/client.js} +12 -30
  189. package/dist/providers/openRouter/constants.d.ts +1 -0
  190. package/dist/providers/openRouter/constants.js +1 -0
  191. package/dist/providers/openRouter/index.d.ts +3 -0
  192. package/dist/providers/openRouter/index.js +3 -0
  193. package/dist/providers/openRouter/utils.d.ts +3 -0
  194. package/dist/providers/openRouter/utils.js +17 -0
  195. package/dist/providers/{openaiCompatible.d.ts → openaiCompatible/client.d.ts} +2 -2
  196. package/dist/{lib/providers/openaiCompatible.js → providers/openaiCompatible/client.js} +7 -23
  197. package/dist/providers/openaiCompatible/constants.d.ts +1 -0
  198. package/dist/providers/openaiCompatible/constants.js +1 -0
  199. package/dist/providers/openaiCompatible/index.d.ts +3 -0
  200. package/dist/providers/openaiCompatible/index.js +3 -0
  201. package/dist/providers/openaiCompatible/utils.d.ts +5 -0
  202. package/dist/providers/openaiCompatible/utils.js +16 -0
  203. package/dist/proxy/modelRouter.d.ts +10 -0
  204. package/dist/proxy/modelRouter.js +17 -0
  205. package/dist/proxy/proxyConfig.js +46 -0
  206. package/dist/proxy/routingPolicy.d.ts +4 -2
  207. package/dist/proxy/routingPolicy.js +8 -5
  208. package/dist/proxy/runtimeConfig.js +2 -0
  209. package/dist/server/routes/claudeProxyRoutes.d.ts +52 -1
  210. package/dist/server/routes/claudeProxyRoutes.js +467 -221
  211. package/dist/server/routes/openaiProxyRoutes.js +1 -1
  212. package/dist/types/errors.d.ts +5 -0
  213. package/dist/types/proxy.d.ts +24 -0
  214. package/dist/types/subscription.d.ts +4 -0
  215. package/dist/utils/providerHealth.js +2 -1
  216. package/dist/utils/providerUtils.js +2 -1
  217. package/package.json +3 -1
@@ -28,10 +28,12 @@ import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
28
28
  import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
29
29
  import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, refreshTokenFromLatest, } from "../../proxy/tokenRefresh.js";
30
30
  import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
31
+ import { DEFAULT_MAX_INFLIGHT_PER_ACCOUNT, MAX_MAX_INFLIGHT_PER_ACCOUNT, MIN_MAX_INFLIGHT_PER_ACCOUNT, } from "../../proxy/modelRouter.js";
31
32
  import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
32
33
  import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
34
+ import { sanitizeForLog } from "../../utils/logSanitize.js";
33
35
  import { logger } from "../../utils/logger.js";
34
- import { withTimeout } from "../../utils/async/withTimeout.js";
36
+ import { raceWithAbort, withTimeout } from "../../utils/async/withTimeout.js";
35
37
  import { ProviderHealthChecker } from "../../utils/providerHealth.js";
36
38
  // ---------------------------------------------------------------------------
37
39
  // Helpers
@@ -97,6 +99,153 @@ const transientRateLimitRetryBudgets = new Map();
97
99
  * transient cooldown. This converts a local 429/retry storm into a bounded
98
100
  * queue and avoids releasing every waiting request at the same millisecond. */
99
101
  const transientCooldownAdmissionSchedules = new Map();
102
+ /**
103
+ * Central per-account admission protects an OAuth account from a concurrent
104
+ * request herd. A lease stays held until a JSON response completes or an SSE
105
+ * response reaches a terminal state, so starting a stream cannot immediately
106
+ * make room for another stream on the same account.
107
+ */
108
+ const accountAdmissionStates = new Map();
109
+ function getAccountAdmissionState(accountKey) {
110
+ let state = accountAdmissionStates.get(accountKey);
111
+ if (!state) {
112
+ state = { active: 0, waiters: [] };
113
+ accountAdmissionStates.set(accountKey, state);
114
+ }
115
+ return state;
116
+ }
117
+ function normalizeAccountAdmissionCapacity(capacity) {
118
+ return Number.isInteger(capacity) &&
119
+ capacity >= MIN_MAX_INFLIGHT_PER_ACCOUNT &&
120
+ capacity <= MAX_MAX_INFLIGHT_PER_ACCOUNT
121
+ ? capacity
122
+ : DEFAULT_MAX_INFLIGHT_PER_ACCOUNT;
123
+ }
124
+ function drainAccountAdmissionWaiters(accountKey, state) {
125
+ while (state.waiters.length > 0 && state.active < state.waiters[0].capacity) {
126
+ const waiter = state.waiters.shift();
127
+ if (!waiter) {
128
+ return;
129
+ }
130
+ state.active += 1;
131
+ waiter.resolve(createAccountAdmissionLease(accountKey, state));
132
+ }
133
+ }
134
+ function createAccountAdmissionLease(accountKey, state) {
135
+ let released = false;
136
+ return {
137
+ release: () => {
138
+ if (released) {
139
+ return;
140
+ }
141
+ released = true;
142
+ state.active = Math.max(0, state.active - 1);
143
+ drainAccountAdmissionWaiters(accountKey, state);
144
+ discardAccountAdmissionState(accountKey, state);
145
+ },
146
+ };
147
+ }
148
+ function discardAccountAdmissionState(accountKey, state) {
149
+ if (state.active === 0 && state.waiters.length === 0) {
150
+ accountAdmissionStates.delete(accountKey);
151
+ }
152
+ }
153
+ function tryAcquireAccountAdmission(accountKey, capacity) {
154
+ const state = getAccountAdmissionState(accountKey);
155
+ const normalizedCapacity = normalizeAccountAdmissionCapacity(capacity);
156
+ if (state.waiters.length > 0 || state.active >= normalizedCapacity) {
157
+ return undefined;
158
+ }
159
+ state.active += 1;
160
+ return createAccountAdmissionLease(accountKey, state);
161
+ }
162
+ function isAccountAdmissionAvailable(accountKey, capacity) {
163
+ const state = accountAdmissionStates.get(accountKey);
164
+ const normalizedCapacity = normalizeAccountAdmissionCapacity(capacity);
165
+ return (!state || (state.waiters.length === 0 && state.active < normalizedCapacity));
166
+ }
167
+ function enqueueAccountAdmission(accountKey, capacity) {
168
+ const state = getAccountAdmissionState(accountKey);
169
+ const normalizedCapacity = normalizeAccountAdmissionCapacity(capacity);
170
+ let queued = true;
171
+ let grantedLease;
172
+ let resolveAdmission;
173
+ const promise = new Promise((resolve) => {
174
+ resolveAdmission = resolve;
175
+ });
176
+ const waiter = {
177
+ capacity: normalizedCapacity,
178
+ resolve: (lease) => {
179
+ queued = false;
180
+ grantedLease = lease;
181
+ resolveAdmission(lease);
182
+ },
183
+ };
184
+ state.waiters.push(waiter);
185
+ drainAccountAdmissionWaiters(accountKey, state);
186
+ return {
187
+ accountKey,
188
+ promise,
189
+ cancel: () => {
190
+ if (grantedLease) {
191
+ const lease = grantedLease;
192
+ grantedLease = undefined;
193
+ lease.release();
194
+ return;
195
+ }
196
+ if (!queued) {
197
+ return;
198
+ }
199
+ queued = false;
200
+ const index = state.waiters.indexOf(waiter);
201
+ if (index >= 0) {
202
+ state.waiters.splice(index, 1);
203
+ drainAccountAdmissionWaiters(accountKey, state);
204
+ }
205
+ discardAccountAdmissionState(accountKey, state);
206
+ },
207
+ };
208
+ }
209
+ async function acquireAccountAdmission(accountKey, capacity, abortSignal, timeoutMs = MAX_TRANSIENT_QUEUE_WAIT_MS) {
210
+ const queued = enqueueAccountAdmission(accountKey, capacity);
211
+ try {
212
+ return await withTimeout(raceWithAbort(queued.promise, abortSignal), timeoutMs, `Account admission for ${accountKey} timed out after ${timeoutMs}ms`);
213
+ }
214
+ catch (error) {
215
+ queued.cancel();
216
+ if (abortSignal?.aborted) {
217
+ throw error;
218
+ }
219
+ return undefined;
220
+ }
221
+ }
222
+ async function acquireFirstAvailableAccountAdmission(accountKeys, capacity, abortSignal, timeoutMs = MAX_TRANSIENT_QUEUE_WAIT_MS) {
223
+ const queuedAdmissions = [...new Set(accountKeys)].map((accountKey) => enqueueAccountAdmission(accountKey, capacity));
224
+ let winnerKey;
225
+ try {
226
+ return await withTimeout(raceWithAbort(Promise.race(queuedAdmissions.map((queued) => queued.promise.then((lease) => {
227
+ if (winnerKey) {
228
+ lease.release();
229
+ return new Promise(() => undefined);
230
+ }
231
+ winnerKey = queued.accountKey;
232
+ return { accountKey: queued.accountKey, lease };
233
+ }))), abortSignal), timeoutMs, `Account admission timed out after ${timeoutMs}ms`);
234
+ }
235
+ catch (error) {
236
+ if (abortSignal?.aborted) {
237
+ throw error;
238
+ }
239
+ return undefined;
240
+ }
241
+ finally {
242
+ for (const queued of queuedAdmissions) {
243
+ if (queued.accountKey !== winnerKey) {
244
+ queued.cancel();
245
+ }
246
+ }
247
+ }
248
+ }
100
249
  /** Track whether we've run the one-time startup prune. */
101
250
  let startupPruneDone = false;
102
251
  /** Default cooling period when retries are exhausted and upstream didn't
@@ -1124,7 +1273,7 @@ async function handleTranslatedClaudeRequest(args) {
1124
1273
  const plan = buildProxyTranslationPlan({
1125
1274
  provider: route.provider,
1126
1275
  model: route.model,
1127
- }, modelRouter?.getFallbackChain() ?? [], body.model, parsed);
1276
+ }, modelRouter?.getFallbackChain() ?? [], body.model, parsed, modelRouter?.isAutoFallbackEnabled?.() ?? false);
1128
1277
  logProxyRoutingPlan(logProxyBody, "translated_request", plan);
1129
1278
  const attempts = plan.attempts;
1130
1279
  if (body.stream) {
@@ -1960,7 +2109,7 @@ async function executeClaudeFallbackWithRetry(args) {
1960
2109
  throw error;
1961
2110
  }
1962
2111
  const delayMs = TRANSIENT_SAME_ACCOUNT_RETRY_DELAYS_MS[retry] ?? 250;
1963
- logger.always(`[proxy] retrying fallback=${args.providerLabel} after transient network error (${retry + 1}/${MAX_FALLBACK_NETWORK_RETRIES}) in ${delayMs}ms: ${describeTransportError(error)}`);
2112
+ logger.always(`[proxy] retrying fallback=${args.providerLabel} after transient network error (${retry + 1}/${MAX_FALLBACK_NETWORK_RETRIES}) in ${delayMs}ms: ${redactProviderErrorMessage(describeTransportError(error))}`);
1964
2113
  await sleep(delayMs);
1965
2114
  }
1966
2115
  }
@@ -1990,7 +2139,17 @@ async function tryConfiguredClaudeFallbackChain(args) {
1990
2139
  }
1991
2140
  const availability = await ProviderHealthChecker.checkFallbackProviderAvailability(fallback.provider, fallback.model);
1992
2141
  if (!availability.available) {
1993
- logger.always(`[proxy] fallback ${fallback.provider}/${fallback.model} health-check failed (${availability.reason ?? "provider unavailable"}), attempting anyway`);
2142
+ const reason = availability.reason ?? "provider unavailable";
2143
+ logger.always(`[proxy] fallback ${fallback.provider}/${fallback.model} health-check failed (${reason}), skipping`);
2144
+ recordFallbackAttempt({
2145
+ provider: fallback.provider,
2146
+ model: fallback.model,
2147
+ status: "failure",
2148
+ errorMessage: `[unavailable] ${reason}`,
2149
+ durationMs: 0,
2150
+ });
2151
+ lastFallbackError = `[${fallback.provider}/${fallback.model}] unavailable: ${reason}`;
2152
+ continue;
1994
2153
  }
1995
2154
  const fallbackStart = Date.now();
1996
2155
  try {
@@ -2025,9 +2184,9 @@ async function tryConfiguredClaudeFallbackChain(args) {
2025
2184
  return { response };
2026
2185
  }
2027
2186
  catch (fallbackErr) {
2028
- const errMsg = fallbackErr instanceof Error
2187
+ const errMsg = redactProviderErrorMessage(fallbackErr instanceof Error
2029
2188
  ? fallbackErr.message
2030
- : String(fallbackErr);
2189
+ : String(fallbackErr));
2031
2190
  let errorClass = "unknown";
2032
2191
  if (errMsg.includes("Rate limit") ||
2033
2192
  errMsg.includes("rate_limit") ||
@@ -2049,7 +2208,7 @@ async function tryConfiguredClaudeFallbackChain(args) {
2049
2208
  else if (errMsg.includes("Resource exhausted")) {
2050
2209
  errorClass = "provider_quota";
2051
2210
  }
2052
- logger.always(`[proxy] fallback ${fallback.provider}/${fallback.model} failed [${errorClass}]: ${describeTransportError(fallbackErr)}`);
2211
+ logger.always(`[proxy] fallback ${fallback.provider}/${fallback.model} failed [${errorClass}]: ${redactProviderErrorMessage(describeTransportError(fallbackErr))}`);
2053
2212
  recordFallbackAttempt({
2054
2213
  provider: fallback.provider,
2055
2214
  model: fallback.model,
@@ -2057,7 +2216,7 @@ async function tryConfiguredClaudeFallbackChain(args) {
2057
2216
  errorMessage: `[${errorClass}] ${errMsg}`,
2058
2217
  durationMs: Date.now() - fallbackStart,
2059
2218
  });
2060
- lastFallbackError = `[${fallback.provider}/${fallback.model}] ${describeTransportError(fallbackErr)}`;
2219
+ lastFallbackError = `[${fallback.provider}/${fallback.model}] ${redactProviderErrorMessage(describeTransportError(fallbackErr))}`;
2061
2220
  }
2062
2221
  }
2063
2222
  return { response: null, lastErrorMessage: lastFallbackError };
@@ -2067,7 +2226,7 @@ async function tryAutoClaudeFallback(args) {
2067
2226
  const fallbackStart = Date.now();
2068
2227
  try {
2069
2228
  const parsed = parseClaudeRequest(body);
2070
- const plan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, [], body.model, parsed);
2229
+ const plan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, [], body.model, parsed, true);
2071
2230
  logProxyRoutingPlan(logProxyBody, "auto_fallback", plan);
2072
2231
  const autoAttempt = plan.attempts.find((attempt) => attempt.label === "auto-provider");
2073
2232
  if (!autoAttempt) {
@@ -2101,7 +2260,7 @@ async function tryAutoClaudeFallback(args) {
2101
2260
  return { response };
2102
2261
  }
2103
2262
  catch (fallbackErr) {
2104
- const errorMessage = describeTransportError(fallbackErr);
2263
+ const errorMessage = redactProviderErrorMessage(describeTransportError(fallbackErr));
2105
2264
  logger.always(`[proxy] fallback auto-provider failed: ${errorMessage}`);
2106
2265
  recordFallbackAttempt({
2107
2266
  provider: "auto-provider",
@@ -2219,7 +2378,7 @@ function buildClaudeAnthropicFailureResponse(args) {
2219
2378
  });
2220
2379
  }
2221
2380
  async function handleAnthropicSuccessfulResponse(args) {
2222
- const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2381
+ const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, } = args;
2223
2382
  accountState.consecutiveRefreshFailures = 0;
2224
2383
  logger.always(`[proxy] ← ${response.status} account=${account.label}`);
2225
2384
  const quota = parseQuotaHeaders(response.headers);
@@ -2262,6 +2421,7 @@ async function handleAnthropicSuccessfulResponse(args) {
2262
2421
  logAttempt,
2263
2422
  logProxyBody,
2264
2423
  logFinalRequest,
2424
+ onStreamTerminal,
2265
2425
  });
2266
2426
  }
2267
2427
  return handleAnthropicJsonSuccessResponse({
@@ -2280,7 +2440,7 @@ async function handleAnthropicSuccessfulResponse(args) {
2280
2440
  });
2281
2441
  }
2282
2442
  async function handleAnthropicStreamingSuccessResponse(args) {
2283
- const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2443
+ const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, } = args;
2284
2444
  if (!response.body) {
2285
2445
  recordAttemptError(account.label, account.type, 502);
2286
2446
  logAttempt(502, "stream_error", "No response body from upstream");
@@ -2467,7 +2627,7 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2467
2627
  return reader.cancel();
2468
2628
  },
2469
2629
  });
2470
- const result = attachAnthropicSuccessStreamTelemetry({
2630
+ const { response: result, telemetryDone } = attachAnthropicSuccessStreamTelemetry({
2471
2631
  account,
2472
2632
  response,
2473
2633
  responseHeaders,
@@ -2481,7 +2641,8 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2481
2641
  logProxyBody,
2482
2642
  logFinalRequest,
2483
2643
  });
2484
- return { response: result };
2644
+ void telemetryDone.then(() => onStreamTerminal?.(), () => onStreamTerminal?.());
2645
+ return { response: result, holdsAccountAdmission: true };
2485
2646
  }
2486
2647
  function getStreamFailureDetails(outcome) {
2487
2648
  if (outcome.kind === "upstream_error") {
@@ -2509,6 +2670,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2509
2670
  const { account, response, responseHeaders, remainingStream, streamOutcome, tracer, requestStartTime, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2510
2671
  const { stream: clientCaptureStream, capture: clientCapture } = createRawStreamCapture();
2511
2672
  let streamSource = remainingStream;
2673
+ let telemetryDone;
2512
2674
  if (tracer) {
2513
2675
  try {
2514
2676
  const { stream: interceptor, telemetry } = createSSEInterceptor({
@@ -2520,7 +2682,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2520
2682
  const capturedResponse = response;
2521
2683
  const capturedRequestBytes = finalBodyStr.length;
2522
2684
  const capturedAccountLabel = account.label;
2523
- Promise.all([telemetry, clientCapture, streamOutcome])
2685
+ telemetryDone = Promise.all([telemetry, clientCapture, streamOutcome])
2524
2686
  .then(([data, clientBody, rawOutcome]) => {
2525
2687
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
2526
2688
  recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account);
@@ -2604,7 +2766,8 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2604
2766
  catch {
2605
2767
  // Interceptor attachment failed after stream setup. Preserve delivery but
2606
2768
  // still settle the request from the actual stream terminal outcome.
2607
- streamOutcome.then((outcome) => {
2769
+ telemetryDone = streamOutcome
2770
+ .then((outcome) => {
2608
2771
  recordCommittedAnthropicStreamAttemptFailure(outcome, account);
2609
2772
  const failure = getStreamFailureDetails(outcome);
2610
2773
  upstreamSpan?.end();
@@ -2617,7 +2780,8 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2617
2780
  tracer.end(response.status, Date.now() - requestStartTime);
2618
2781
  logFinalRequest(response.status, account.label, account.type);
2619
2782
  }
2620
- });
2783
+ })
2784
+ .catch(() => undefined);
2621
2785
  }
2622
2786
  }
2623
2787
  else {
@@ -2628,7 +2792,11 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2628
2792
  });
2629
2793
  streamSource = streamSource.pipeThrough(noTracerInterceptor);
2630
2794
  const capturedAccountLabel = account.label;
2631
- Promise.all([noTracerTelemetry, clientCapture, streamOutcome])
2795
+ telemetryDone = Promise.all([
2796
+ noTracerTelemetry,
2797
+ clientCapture,
2798
+ streamOutcome,
2799
+ ])
2632
2800
  .then(([data, clientBody, rawOutcome]) => {
2633
2801
  const terminalOutcome = mergeStreamTerminalOutcome(rawOutcome, data.streamErrorMessage);
2634
2802
  recordCommittedAnthropicStreamAttemptFailure(terminalOutcome, account);
@@ -2694,7 +2862,8 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2694
2862
  .catch(() => {
2695
2863
  // Non-fatal
2696
2864
  });
2697
- streamOutcome.then((outcome) => {
2865
+ telemetryDone = streamOutcome
2866
+ .then((outcome) => {
2698
2867
  recordCommittedAnthropicStreamAttemptFailure(outcome, account);
2699
2868
  const failure = getStreamFailureDetails(outcome);
2700
2869
  if (failure) {
@@ -2703,7 +2872,8 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2703
2872
  else {
2704
2873
  logFinalRequest(response.status, account.label, account.type);
2705
2874
  }
2706
- });
2875
+ })
2876
+ .catch(() => undefined);
2707
2877
  }
2708
2878
  }
2709
2879
  const clientStream = streamSource.pipeThrough(clientCaptureStream);
@@ -2724,10 +2894,13 @@ function attachAnthropicSuccessStreamTelemetry(args) {
2724
2894
  clientResponseHeaders[headerName] = value;
2725
2895
  }
2726
2896
  }
2727
- return new Response(clientStream, {
2728
- status: response.status,
2729
- headers: clientResponseHeaders,
2730
- });
2897
+ return {
2898
+ response: new Response(clientStream, {
2899
+ status: response.status,
2900
+ headers: clientResponseHeaders,
2901
+ }),
2902
+ telemetryDone,
2903
+ };
2731
2904
  }
2732
2905
  async function handleAnthropicJsonSuccessResponse(args) {
2733
2906
  const { account, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
@@ -2902,7 +3075,7 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
2902
3075
  return retryJson;
2903
3076
  }
2904
3077
  async function handleAnthropicAuthRetry(args) {
2905
- const { ctx, body, account, accountState, headers, buildUpstreamBody, url, enabledAccounts, orderedAccounts, tracer, requestStartTime, allocateAttemptNumber, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, lastError, authFailureMessage, sawRateLimit, sawTransientFailure, sawNetworkError, } = args;
3078
+ const { ctx, body, account, accountState, headers, buildUpstreamBody, url, enabledAccounts, orderedAccounts, tracer, requestStartTime, allocateAttemptNumber, upstreamSpan, logAttempt, logProxyBody, onStreamTerminal, logFinalRequest, lastError, authFailureMessage, sawRateLimit, sawTransientFailure, sawNetworkError, } = args;
2906
3079
  recordAttemptError(account.label, account.type, 401);
2907
3080
  logAttempt(401, "authentication_error", "received 401 from Anthropic", {
2908
3081
  retryable: true,
@@ -2986,6 +3159,7 @@ async function handleAnthropicAuthRetry(args) {
2986
3159
  logAttempt: retryLogAttempt,
2987
3160
  logProxyBody,
2988
3161
  logFinalRequest,
3162
+ onStreamTerminal,
2989
3163
  })
2990
3164
  : {
2991
3165
  response: await handleAnthropicSuccessfulNonStreamRetryResponse({
@@ -3018,6 +3192,7 @@ async function handleAnthropicAuthRetry(args) {
3018
3192
  }
3019
3193
  return {
3020
3194
  response: successResult.response,
3195
+ holdsAccountAdmission: successResult.holdsAccountAdmission,
3021
3196
  continueLoop: false,
3022
3197
  lastError: currentLastError,
3023
3198
  authFailureMessage: currentAuthFailureMessage,
@@ -3399,9 +3574,10 @@ async function handleAnthropicNonOkResponse(args) {
3399
3574
  };
3400
3575
  }
3401
3576
  if (isTransientHttpFailure(response.status, errBody)) {
3577
+ const upstreamOverload = isUpstreamOverload(response.status, errBody);
3402
3578
  recordAttemptError(account.label, account.type, response.status, response.status === 429 ? "transient" : undefined);
3403
3579
  currentSawTransientFailure = true;
3404
- logger.always(`[proxy] ← ${response.status} account=${account.label} (transient)`);
3580
+ logger.always(`[proxy] ← ${response.status} account=${account.label} (${upstreamOverload ? "overloaded" : "transient"})`);
3405
3581
  currentLastError = errBody;
3406
3582
  logAttempt(response.status, "api_error", summarizeErrorMessage(errBody), response.status === 429
3407
3583
  ? {
@@ -3410,11 +3586,11 @@ async function handleAnthropicNonOkResponse(args) {
3410
3586
  cooldownReason: "transient",
3411
3587
  }
3412
3588
  : undefined);
3413
- tracer?.setError("transient_error", summarizeErrorMessage(errBody));
3414
- tracer?.recordRetry(account.label, "transient");
3589
+ tracer?.setError(upstreamOverload ? "overloaded_error" : "transient_error", summarizeErrorMessage(errBody));
3590
+ tracer?.recordRetry(account.label, upstreamOverload ? "overloaded" : "transient");
3415
3591
  return {
3416
3592
  continueLoop: true,
3417
- retrySameAccount: true,
3593
+ retrySameAccount: !upstreamOverload,
3418
3594
  lastError: currentLastError,
3419
3595
  authFailureMessage: currentAuthFailureMessage,
3420
3596
  sawTransientFailure: currentSawTransientFailure,
@@ -4018,6 +4194,15 @@ async function handleAnthropicRoutedClaudeRequest(args) {
4018
4194
  loopState.authCooldownMessage = `All ${orderedAccounts.length} Anthropic accounts are temporarily unavailable while OAuth refresh is cooling. Earliest retry at ${new Date(earliestRetryAt).toISOString()}.`;
4019
4195
  }
4020
4196
  }
4197
+ const accountAdmissionCapacity = modelRouter?.getMaxInflightPerAccount?.() ??
4198
+ DEFAULT_MAX_INFLIGHT_PER_ACCOUNT;
4199
+ // When every eligible account is busy, reserve the first account that frees
4200
+ // instead of arbitrarily waiting behind the last configured account.
4201
+ let queuedAccountAdmission;
4202
+ if (effectiveAccounts.length > 0 &&
4203
+ effectiveAccounts.every((account) => !isAccountAdmissionAvailable(account.key, accountAdmissionCapacity))) {
4204
+ queuedAccountAdmission = await acquireFirstAvailableAccountAdmission(effectiveAccounts.map((account) => account.key), accountAdmissionCapacity, ctx.abortSignal);
4205
+ }
4021
4206
  accountLoop: for (const account of effectiveAccounts) {
4022
4207
  const accountState = getOrCreateRuntimeState(account.key);
4023
4208
  let transientSameAccountRetries = 0;
@@ -4066,229 +4251,264 @@ async function handleAnthropicRoutedClaudeRequest(args) {
4066
4251
  preparedAttempt.fetchStartMs === undefined) {
4067
4252
  continue accountLoop;
4068
4253
  }
4069
- const fetchResult = await fetchAnthropicAccountResponse({
4070
- url,
4071
- headers: preparedAttempt.headers,
4072
- finalBodyStr: preparedAttempt.finalBodyStr,
4073
- account,
4074
- accountState,
4075
- enabledAccounts,
4076
- orderedAccounts,
4077
- tracer,
4078
- logAttempt,
4079
- logProxyBody,
4080
- fetchStartMs: preparedAttempt.fetchStartMs,
4081
- attemptNumber: loopState.attemptNumber,
4082
- currentLastError: loopState.lastError,
4083
- currentSawRateLimit: loopState.sawRateLimit,
4084
- currentSawNetworkError: loopState.sawNetworkError,
4085
- upstreamSpan: preparedAttempt.upstreamSpan,
4086
- });
4087
- loopState.lastError = fetchResult.lastError;
4088
- loopState.sawRateLimit = fetchResult.sawRateLimit;
4089
- loopState.sawNetworkError = fetchResult.sawNetworkError;
4090
- if (fetchResult.terminalError) {
4091
- return finalizeAnthropicTerminalFetchError({
4092
- terminalError: fetchResult.terminalError,
4254
+ let admissionLease;
4255
+ if (queuedAccountAdmission?.accountKey === account.key) {
4256
+ admissionLease = queuedAccountAdmission.lease;
4257
+ queuedAccountAdmission = undefined;
4258
+ }
4259
+ else {
4260
+ admissionLease = tryAcquireAccountAdmission(account.key, accountAdmissionCapacity);
4261
+ if (admissionLease && queuedAccountAdmission) {
4262
+ // A preferred account became available while the race was being
4263
+ // established; release the lower-priority reservation.
4264
+ queuedAccountAdmission.lease.release();
4265
+ queuedAccountAdmission = undefined;
4266
+ }
4267
+ }
4268
+ if (!admissionLease) {
4269
+ // A later account may be immediately available. Preserve the routing
4270
+ // order but do not leave a request queued behind a busy first choice.
4271
+ continue accountLoop;
4272
+ }
4273
+ let admissionTransferredToStream = false;
4274
+ try {
4275
+ const fetchResult = await fetchAnthropicAccountResponse({
4276
+ url,
4277
+ headers: preparedAttempt.headers,
4278
+ finalBodyStr: preparedAttempt.finalBodyStr,
4093
4279
  account,
4280
+ accountState,
4281
+ enabledAccounts,
4282
+ orderedAccounts,
4094
4283
  tracer,
4095
- requestStartTime,
4096
- attemptNumber: loopState.attemptNumber,
4284
+ logAttempt,
4097
4285
  logProxyBody,
4098
- logFinalRequest,
4286
+ fetchStartMs: preparedAttempt.fetchStartMs,
4287
+ attemptNumber: loopState.attemptNumber,
4288
+ currentLastError: loopState.lastError,
4289
+ currentSawRateLimit: loopState.sawRateLimit,
4290
+ currentSawNetworkError: loopState.sawNetworkError,
4291
+ upstreamSpan: preparedAttempt.upstreamSpan,
4099
4292
  });
4100
- }
4101
- if (fetchResult.continueLoop || !fetchResult.response) {
4102
- // Genuine 429 (carries a cooldown plan derived from quota headers).
4103
- if (fetchResult.cooldownPlan) {
4104
- const plan = fetchResult.cooldownPlan;
4105
- // Refresh the account's quota snapshot for proactive selection.
4106
- if (fetchResult.quota) {
4107
- accountState.quota = fetchResult.quota;
4108
- saveAccountQuota(account.label, fetchResult.quota).catch(() => {
4109
- // Non-fatal: routing already has the in-memory snapshot.
4110
- });
4111
- }
4112
- // Publish the cooldown before retrying so requests arriving behind
4113
- // this one skip the throttled account instead of joining the burst.
4114
- let cooldownExtended = false;
4115
- if (!accountState.coolingUntil ||
4116
- plan.coolingUntil > accountState.coolingUntil) {
4117
- accountState.coolingUntil = plan.coolingUntil;
4118
- accountState.coolingReason = plan.reason;
4119
- cooldownExtended = true;
4120
- }
4121
- if (cooldownExtended) {
4122
- await saveAccountCooldown(account.key, accountState.coolingUntil, accountState.coolingReason ?? plan.reason).catch(() => {
4123
- // Non-fatal: routing already has the in-memory cooldown.
4124
- });
4293
+ loopState.lastError = fetchResult.lastError;
4294
+ loopState.sawRateLimit = fetchResult.sawRateLimit;
4295
+ loopState.sawNetworkError = fetchResult.sawNetworkError;
4296
+ if (fetchResult.terminalError) {
4297
+ return finalizeAnthropicTerminalFetchError({
4298
+ terminalError: fetchResult.terminalError,
4299
+ account,
4300
+ tracer,
4301
+ requestStartTime,
4302
+ attemptNumber: loopState.attemptNumber,
4303
+ logProxyBody,
4304
+ logFinalRequest,
4305
+ });
4306
+ }
4307
+ if (fetchResult.continueLoop || !fetchResult.response) {
4308
+ // Genuine 429 (carries a cooldown plan derived from quota headers).
4309
+ if (fetchResult.cooldownPlan) {
4310
+ const plan = fetchResult.cooldownPlan;
4311
+ // Refresh the account's quota snapshot for proactive selection.
4312
+ if (fetchResult.quota) {
4313
+ accountState.quota = fetchResult.quota;
4314
+ saveAccountQuota(account.label, fetchResult.quota).catch(() => {
4315
+ // Non-fatal: routing already has the in-memory snapshot.
4316
+ });
4317
+ }
4318
+ // Publish the cooldown before retrying so requests arriving behind
4319
+ // this one skip the throttled account instead of joining the burst.
4320
+ let cooldownExtended = false;
4321
+ if (!accountState.coolingUntil ||
4322
+ plan.coolingUntil > accountState.coolingUntil) {
4323
+ accountState.coolingUntil = plan.coolingUntil;
4324
+ accountState.coolingReason = plan.reason;
4325
+ cooldownExtended = true;
4326
+ }
4327
+ if (cooldownExtended) {
4328
+ await saveAccountCooldown(account.key, accountState.coolingUntil, accountState.coolingReason ?? plan.reason).catch(() => {
4329
+ // Non-fatal: routing already has the in-memory cooldown.
4330
+ });
4331
+ }
4332
+ // Transient retries are budgeted across all concurrent requests for
4333
+ // this account/window. Exhaustion plans rotate immediately and never
4334
+ // claim this budget.
4335
+ const sharedRetrySlot = fetchResult.retrySameAccount
4336
+ ? claimTransientRateLimitRetry(account.key, plan.coolingUntil)
4337
+ : undefined;
4338
+ if (fetchResult.retrySameAccount &&
4339
+ fetchResult.retryAfterMs !== undefined &&
4340
+ rateLimitSameAccountRetries <
4341
+ MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES &&
4342
+ sharedRetrySlot !== undefined) {
4343
+ rateLimitSameAccountRetries += 1;
4344
+ const base = Math.min(fetchResult.retryAfterMs || 1_000, MAX_RATE_LIMIT_RETRY_DELAY_MS);
4345
+ // Stagger the two shared slots, then cap after jitter so the final
4346
+ // sleep never exceeds the configured maximum.
4347
+ const delayMs = Math.min(MAX_RATE_LIMIT_RETRY_DELAY_MS, jitteredDelay(base * sharedRetrySlot));
4348
+ logger.always(`[proxy] retrying same account=${account.label} after transient 429 (shared slot ${sharedRetrySlot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
4349
+ await sleep(delayMs);
4350
+ continue;
4351
+ }
4352
+ // Exhaustion, or the shared transient retry budget being used up:
4353
+ // rotate while the already-published cooldown remains active.
4354
+ advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
4355
+ logger.always(`[proxy] account=${account.label} rate-limited (${plan.reason}); cooling ~${minutesUntil(plan.coolingUntil, Date.now())}m until ${new Date(plan.coolingUntil).toISOString()}, rotating`);
4356
+ continue accountLoop;
4125
4357
  }
4126
- // Transient retries are budgeted across all concurrent requests for
4127
- // this account/window. Exhaustion plans rotate immediately and never
4128
- // claim this budget.
4129
- const sharedRetrySlot = fetchResult.retrySameAccount
4130
- ? claimTransientRateLimitRetry(account.key, plan.coolingUntil)
4131
- : undefined;
4358
+ // Transient error retry (network errors, 529 overloaded)
4132
4359
  if (fetchResult.retrySameAccount &&
4133
- fetchResult.retryAfterMs !== undefined &&
4134
- rateLimitSameAccountRetries < MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES &&
4135
- sharedRetrySlot !== undefined) {
4136
- rateLimitSameAccountRetries += 1;
4137
- const base = Math.min(fetchResult.retryAfterMs || 1_000, MAX_RATE_LIMIT_RETRY_DELAY_MS);
4138
- // Stagger the two shared slots, then cap after jitter so the final
4139
- // sleep never exceeds the configured maximum.
4140
- const delayMs = Math.min(MAX_RATE_LIMIT_RETRY_DELAY_MS, jitteredDelay(base * sharedRetrySlot));
4141
- logger.always(`[proxy] retrying same account=${account.label} after transient 429 (shared slot ${sharedRetrySlot}/${MAX_RATE_LIMIT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
4360
+ transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
4361
+ transientSameAccountRetries += 1;
4362
+ const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
4363
+ logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
4142
4364
  await sleep(delayMs);
4143
4365
  continue;
4144
4366
  }
4145
- // Exhaustion, or the shared transient retry budget being used up:
4146
- // rotate while the already-published cooldown remains active.
4147
- advancePrimaryIfCurrent(account.key, enabledAccounts.length, orderedAccounts[0]?.key);
4148
- logger.always(`[proxy] account=${account.label} rate-limited (${plan.reason}); cooling ~${minutesUntil(plan.coolingUntil, Date.now())}m until ${new Date(plan.coolingUntil).toISOString()}, rotating`);
4367
+ if (fetchResult.retrySameAccount) {
4368
+ logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
4369
+ }
4149
4370
  continue accountLoop;
4150
4371
  }
4151
- // Transient error retry (network errors, 529 overloaded)
4152
- if (fetchResult.retrySameAccount &&
4153
- transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
4154
- transientSameAccountRetries += 1;
4155
- const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
4156
- logger.always(`[proxy] retrying same account=${account.label} after transient network error (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
4157
- await sleep(delayMs);
4158
- continue;
4372
+ let upstreamSpan = fetchResult.upstreamSpan;
4373
+ const response = fetchResult.response;
4374
+ if (response.status === 401 &&
4375
+ account.type === "oauth" &&
4376
+ account.refreshToken) {
4377
+ const authRetryResult = await handleAnthropicAuthRetry({
4378
+ ctx,
4379
+ body,
4380
+ account,
4381
+ accountState,
4382
+ headers: preparedAttempt.headers,
4383
+ buildUpstreamBody: preparedAttempt.buildUpstreamBody,
4384
+ url,
4385
+ enabledAccounts,
4386
+ orderedAccounts,
4387
+ tracer,
4388
+ requestStartTime,
4389
+ allocateAttemptNumber: () => {
4390
+ loopState.attemptNumber += 1;
4391
+ return loopState.attemptNumber;
4392
+ },
4393
+ upstreamSpan,
4394
+ logAttempt,
4395
+ logProxyBody,
4396
+ logFinalRequest,
4397
+ onStreamTerminal: admissionLease.release,
4398
+ lastError: loopState.lastError,
4399
+ authFailureMessage: loopState.authFailureMessage,
4400
+ sawRateLimit: loopState.sawRateLimit,
4401
+ sawTransientFailure: loopState.sawTransientFailure,
4402
+ sawNetworkError: loopState.sawNetworkError,
4403
+ });
4404
+ loopState.lastError = authRetryResult.lastError;
4405
+ loopState.authFailureMessage = authRetryResult.authFailureMessage;
4406
+ loopState.sawRateLimit = authRetryResult.sawRateLimit;
4407
+ loopState.sawTransientFailure = authRetryResult.sawTransientFailure;
4408
+ loopState.sawNetworkError = authRetryResult.sawNetworkError;
4409
+ upstreamSpan = authRetryResult.upstreamSpan;
4410
+ if (authRetryResult.response !== undefined) {
4411
+ admissionTransferredToStream =
4412
+ authRetryResult.holdsAccountAdmission === true;
4413
+ return authRetryResult.response;
4414
+ }
4415
+ if (authRetryResult.continueLoop) {
4416
+ continue accountLoop;
4417
+ }
4418
+ }
4419
+ if (!response.ok) {
4420
+ const nonOkResult = await handleAnthropicNonOkResponse({
4421
+ response,
4422
+ account,
4423
+ accountState,
4424
+ enabledAccounts,
4425
+ orderedAccounts,
4426
+ tracer,
4427
+ requestStartTime,
4428
+ fetchStartMs: preparedAttempt.fetchStartMs,
4429
+ attemptNumber: loopState.attemptNumber,
4430
+ logAttempt,
4431
+ logProxyBody,
4432
+ logFinalRequest,
4433
+ lastError: loopState.lastError,
4434
+ authFailureMessage: loopState.authFailureMessage,
4435
+ sawTransientFailure: loopState.sawTransientFailure,
4436
+ invalidRequestFailure: loopState.invalidRequestFailure,
4437
+ });
4438
+ loopState.lastError = nonOkResult.lastError;
4439
+ loopState.authFailureMessage = nonOkResult.authFailureMessage;
4440
+ loopState.sawTransientFailure = nonOkResult.sawTransientFailure;
4441
+ loopState.invalidRequestFailure = nonOkResult.invalidRequestFailure;
4442
+ if (nonOkResult.response !== undefined) {
4443
+ return nonOkResult.response;
4444
+ }
4445
+ if (nonOkResult.continueLoop) {
4446
+ if (nonOkResult.retrySameAccount &&
4447
+ transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
4448
+ transientSameAccountRetries += 1;
4449
+ const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
4450
+ logger.always(`[proxy] retrying same account=${account.label} after transient upstream ${response.status} (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
4451
+ await sleep(delayMs);
4452
+ continue;
4453
+ }
4454
+ if (nonOkResult.retrySameAccount) {
4455
+ logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
4456
+ }
4457
+ continue accountLoop;
4458
+ }
4459
+ break accountLoop;
4159
4460
  }
4160
- if (fetchResult.retrySameAccount) {
4161
- logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
4461
+ // Clear cooling on success — but only if the stored cooldown has already
4462
+ // expired, so an older in-flight success can't wipe an active exhaustion
4463
+ // cooldown just set by a concurrent 429. The success handler re-applies a
4464
+ // cooldown via maybeCoolFromQuota if the fresh quota headers report the
4465
+ // window flipped to "rejected" on this very request.
4466
+ if (accountState.coolingUntil &&
4467
+ Date.now() >= accountState.coolingUntil) {
4468
+ const expiredCooldown = accountState.coolingUntil;
4469
+ accountState.coolingUntil = undefined;
4470
+ accountState.coolingReason = undefined;
4471
+ clearAccountCooldown(account.key, expiredCooldown).catch(() => {
4472
+ // Best-effort cleanup; expired entries are ignored during seeding.
4473
+ });
4162
4474
  }
4163
- continue accountLoop;
4164
- }
4165
- let upstreamSpan = fetchResult.upstreamSpan;
4166
- const response = fetchResult.response;
4167
- if (response.status === 401 &&
4168
- account.type === "oauth" &&
4169
- account.refreshToken) {
4170
- const authRetryResult = await handleAnthropicAuthRetry({
4475
+ const successResult = await handleAnthropicSuccessfulResponse({
4171
4476
  ctx,
4172
4477
  body,
4173
4478
  account,
4174
4479
  accountState,
4175
- headers: preparedAttempt.headers,
4176
- buildUpstreamBody: preparedAttempt.buildUpstreamBody,
4177
- url,
4178
- enabledAccounts,
4179
- orderedAccounts,
4180
- tracer,
4181
- requestStartTime,
4182
- allocateAttemptNumber: () => {
4183
- loopState.attemptNumber += 1;
4184
- return loopState.attemptNumber;
4185
- },
4186
- upstreamSpan,
4187
- logAttempt,
4188
- logProxyBody,
4189
- logFinalRequest,
4190
- lastError: loopState.lastError,
4191
- authFailureMessage: loopState.authFailureMessage,
4192
- sawRateLimit: loopState.sawRateLimit,
4193
- sawTransientFailure: loopState.sawTransientFailure,
4194
- sawNetworkError: loopState.sawNetworkError,
4195
- });
4196
- loopState.lastError = authRetryResult.lastError;
4197
- loopState.authFailureMessage = authRetryResult.authFailureMessage;
4198
- loopState.sawRateLimit = authRetryResult.sawRateLimit;
4199
- loopState.sawTransientFailure = authRetryResult.sawTransientFailure;
4200
- loopState.sawNetworkError = authRetryResult.sawNetworkError;
4201
- upstreamSpan = authRetryResult.upstreamSpan;
4202
- if (authRetryResult.response !== undefined) {
4203
- return authRetryResult.response;
4204
- }
4205
- if (authRetryResult.continueLoop) {
4206
- continue accountLoop;
4207
- }
4208
- }
4209
- if (!response.ok) {
4210
- const nonOkResult = await handleAnthropicNonOkResponse({
4211
4480
  response,
4212
- account,
4213
- accountState,
4214
- enabledAccounts,
4215
- orderedAccounts,
4216
4481
  tracer,
4217
4482
  requestStartTime,
4218
4483
  fetchStartMs: preparedAttempt.fetchStartMs,
4219
4484
  attemptNumber: loopState.attemptNumber,
4485
+ finalBodyStr: preparedAttempt.finalBodyStr,
4486
+ upstreamSpan,
4220
4487
  logAttempt,
4221
4488
  logProxyBody,
4222
4489
  logFinalRequest,
4223
- lastError: loopState.lastError,
4224
- authFailureMessage: loopState.authFailureMessage,
4225
- sawTransientFailure: loopState.sawTransientFailure,
4226
- invalidRequestFailure: loopState.invalidRequestFailure,
4490
+ onStreamTerminal: admissionLease.release,
4227
4491
  });
4228
- loopState.lastError = nonOkResult.lastError;
4229
- loopState.authFailureMessage = nonOkResult.authFailureMessage;
4230
- loopState.sawTransientFailure = nonOkResult.sawTransientFailure;
4231
- loopState.invalidRequestFailure = nonOkResult.invalidRequestFailure;
4232
- if (nonOkResult.response !== undefined) {
4233
- return nonOkResult.response;
4234
- }
4235
- if (nonOkResult.continueLoop) {
4236
- if (nonOkResult.retrySameAccount &&
4237
- transientSameAccountRetries < MAX_TRANSIENT_SAME_ACCOUNT_RETRIES) {
4238
- transientSameAccountRetries += 1;
4239
- const delayMs = getTransientSameAccountRetryDelayMs(transientSameAccountRetries);
4240
- logger.always(`[proxy] retrying same account=${account.label} after transient upstream ${response.status} (${transientSameAccountRetries}/${MAX_TRANSIENT_SAME_ACCOUNT_RETRIES}) in ${delayMs}ms`);
4241
- await sleep(delayMs);
4242
- continue;
4243
- }
4244
- if (nonOkResult.retrySameAccount) {
4245
- logger.always(`[proxy] exhausted transient same-account retries for account=${account.label}; rotating`);
4492
+ if ("retryNextAccount" in successResult) {
4493
+ if (successResult.failure) {
4494
+ loopState.lastError = successResult.failure.message;
4495
+ loopState.sawRateLimit ||= successResult.failure.rateLimit;
4496
+ loopState.sawTransientFailure ||= !successResult.failure.rateLimit;
4246
4497
  }
4247
4498
  continue accountLoop;
4248
4499
  }
4249
- break accountLoop;
4250
- }
4251
- // Clear cooling on success — but only if the stored cooldown has already
4252
- // expired, so an older in-flight success can't wipe an active exhaustion
4253
- // cooldown just set by a concurrent 429. The success handler re-applies a
4254
- // cooldown via maybeCoolFromQuota if the fresh quota headers report the
4255
- // window flipped to "rejected" on this very request.
4256
- if (accountState.coolingUntil &&
4257
- Date.now() >= accountState.coolingUntil) {
4258
- const expiredCooldown = accountState.coolingUntil;
4259
- accountState.coolingUntil = undefined;
4260
- accountState.coolingReason = undefined;
4261
- clearAccountCooldown(account.key, expiredCooldown).catch(() => {
4262
- // Best-effort cleanup; expired entries are ignored during seeding.
4263
- });
4500
+ admissionTransferredToStream =
4501
+ successResult.holdsAccountAdmission === true;
4502
+ return successResult.response;
4264
4503
  }
4265
- const successResult = await handleAnthropicSuccessfulResponse({
4266
- ctx,
4267
- body,
4268
- account,
4269
- accountState,
4270
- response,
4271
- tracer,
4272
- requestStartTime,
4273
- fetchStartMs: preparedAttempt.fetchStartMs,
4274
- attemptNumber: loopState.attemptNumber,
4275
- finalBodyStr: preparedAttempt.finalBodyStr,
4276
- upstreamSpan,
4277
- logAttempt,
4278
- logProxyBody,
4279
- logFinalRequest,
4280
- });
4281
- if ("retryNextAccount" in successResult) {
4282
- if (successResult.failure) {
4283
- loopState.lastError = successResult.failure.message;
4284
- loopState.sawRateLimit ||= successResult.failure.rateLimit;
4285
- loopState.sawTransientFailure ||= !successResult.failure.rateLimit;
4504
+ finally {
4505
+ if (!admissionTransferredToStream) {
4506
+ admissionLease.release();
4286
4507
  }
4287
- continue accountLoop;
4288
4508
  }
4289
- return successResult.response;
4290
4509
  }
4291
4510
  }
4511
+ queuedAccountAdmission?.lease.release();
4292
4512
  if (loopState.attemptNumber === 0) {
4293
4513
  acctSelectionSpan?.end();
4294
4514
  }
@@ -4311,9 +4531,9 @@ async function handleAnthropicRoutedClaudeRequest(args) {
4311
4531
  return configuredFallbackResult.response;
4312
4532
  }
4313
4533
  fallbackFailureMessage = configuredFallbackResult.lastErrorMessage;
4314
- // Try auto-provider fallback when the configured chain didn't produce a
4315
- // response (either no chain configured, or all entries failed/deduped).
4316
- if (!loopState.sawRateLimit) {
4534
+ // A translation-layer-selected provider is only permitted by an explicit
4535
+ // routing setting. Empty fallback chains otherwise stay within OAuth.
4536
+ if (!loopState.sawRateLimit && modelRouter?.isAutoFallbackEnabled?.()) {
4317
4537
  const autoFallbackResult = await tryAutoClaudeFallback({
4318
4538
  ctx,
4319
4539
  body,
@@ -4872,6 +5092,19 @@ export function isTransientHttpFailure(status, errBody) {
4872
5092
  normalized.includes("cloudflare") ||
4873
5093
  normalized.includes("internal server error"));
4874
5094
  }
5095
+ /**
5096
+ * An upstream overload is already a capacity signal. Retrying it on the same
5097
+ * OAuth account only consumes its remaining concurrency and delays rotation.
5098
+ */
5099
+ export function isUpstreamOverload(status, errBody) {
5100
+ return (status === 529 ||
5101
+ parseClaudeErrorBody(errBody).errorType === "overloaded_error");
5102
+ }
5103
+ /** Remove provider credentials before they enter persistent proxy diagnostics. */
5104
+ const MAX_PROVIDER_ERROR_MESSAGE_LENGTH = 500;
5105
+ export function redactProviderErrorMessage(message) {
5106
+ return sanitizeForLog(message, MAX_PROVIDER_ERROR_MESSAGE_LENGTH);
5107
+ }
4875
5108
  // ---------------------------------------------------------------------------
4876
5109
  // Test hooks (not part of the public SDK API). Only consumed by the proxy
4877
5110
  // continuous-test-suite to drive the in-process resolver/reset logic without
@@ -4922,6 +5155,7 @@ export const __testHooks = {
4922
5155
  accountRuntimeState.clear();
4923
5156
  transientRateLimitRetryBudgets.clear();
4924
5157
  transientCooldownAdmissionSchedules.clear();
5158
+ accountAdmissionStates.clear();
4925
5159
  primaryAccountIndex = 0;
4926
5160
  lastKnownAccountCount = 0;
4927
5161
  },
@@ -4929,12 +5163,24 @@ export const __testHooks = {
4929
5163
  isAntiAbuseConstruction429,
4930
5164
  fetchAnthropicAccountResponse,
4931
5165
  finalizeAnthropicTerminalFetchError,
5166
+ handleAnthropicNonOkResponse,
4932
5167
  handleAnthropicAuthRetry,
4933
5168
  handleAnthropicStreamingSuccessResponse,
4934
5169
  claimTransientRateLimitRetry,
4935
5170
  claimTransientCooldownAdmission,
4936
5171
  waitForTransientAccountAvailability,
5172
+ acquireAccountAdmission,
5173
+ acquireFirstAvailableAccountAdmission,
5174
+ tryAcquireAccountAdmission,
5175
+ getAccountAdmissionSnapshot: (accountKey) => {
5176
+ const state = accountAdmissionStates.get(accountKey);
5177
+ return state
5178
+ ? { active: state.active, waiting: state.waiters.length }
5179
+ : { active: 0, waiting: 0 };
5180
+ },
4937
5181
  describeTransportError,
5182
+ redactProviderErrorMessage,
5183
+ isUpstreamOverload,
4938
5184
  shouldAttemptClaudeFallback,
4939
5185
  executeClaudeFallbackWithRetry,
4940
5186
  buildClaudeAnthropicFailureResponse,