@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.
- package/CHANGELOG.md +8 -0
- package/dist/browser/neurolink.min.js +497 -478
- package/dist/factories/providerRegistry.js +11 -11
- package/dist/lib/factories/providerRegistry.js +11 -11
- package/dist/lib/providers/{amazonBedrock.d.ts → amazonBedrock/client.d.ts} +4 -4
- package/dist/{providers/amazonBedrock.js → lib/providers/amazonBedrock/client.js} +15 -18
- package/dist/lib/providers/amazonBedrock/constants.d.ts +1 -0
- package/dist/lib/providers/amazonBedrock/constants.js +3 -0
- package/dist/lib/providers/amazonBedrock/index.d.ts +3 -0
- package/dist/lib/providers/amazonBedrock/index.js +4 -0
- package/dist/lib/providers/amazonBedrock/utils.d.ts +1 -0
- package/dist/lib/providers/amazonBedrock/utils.js +4 -0
- package/dist/{providers/anthropic.d.ts → lib/providers/anthropic/client.d.ts} +6 -21
- package/dist/{providers/anthropic.js → lib/providers/anthropic/client.js} +28 -45
- package/dist/lib/providers/anthropic/constants.d.ts +4 -0
- package/dist/lib/providers/anthropic/constants.js +9 -0
- package/dist/lib/providers/anthropic/index.d.ts +3 -0
- package/dist/lib/providers/anthropic/index.js +4 -0
- package/dist/lib/providers/anthropic/utils.d.ts +7 -0
- package/dist/lib/providers/anthropic/utils.js +150 -0
- package/dist/lib/providers/{googleAiStudio.d.ts → googleAiStudio/client.d.ts} +4 -4
- package/dist/{providers/googleAiStudio.js → lib/providers/googleAiStudio/client.js} +16 -15
- package/dist/lib/providers/googleAiStudio/index.d.ts +2 -0
- package/dist/lib/providers/googleAiStudio/index.js +3 -0
- package/dist/lib/providers/googleAiStudio/utils.d.ts +2 -0
- package/dist/lib/providers/googleAiStudio/utils.js +19 -0
- package/dist/lib/providers/googleNativeGemini3/constants.d.ts +1 -0
- package/dist/lib/providers/googleNativeGemini3/constants.js +2 -0
- package/dist/lib/providers/googleNativeGemini3/index.d.ts +2 -0
- package/dist/lib/providers/googleNativeGemini3/index.js +3 -0
- package/dist/lib/providers/{googleNativeGemini3.d.ts → googleNativeGemini3/utils.d.ts} +2 -2
- package/dist/lib/providers/{googleNativeGemini3.js → googleNativeGemini3/utils.js} +8 -8
- package/dist/{providers/googleVertex.d.ts → lib/providers/googleVertex/client.d.ts} +4 -4
- package/dist/lib/providers/{googleVertex.js → googleVertex/client.js} +32 -32
- package/dist/lib/providers/googleVertex/constants.d.ts +5 -0
- package/dist/lib/providers/googleVertex/constants.js +27 -0
- package/dist/lib/providers/googleVertex/index.d.ts +3 -0
- package/dist/lib/providers/googleVertex/index.js +4 -0
- package/dist/lib/providers/googleVertex/utils.d.ts +12 -0
- package/dist/lib/providers/googleVertex/utils.js +287 -0
- package/dist/lib/providers/{huggingFace.d.ts → huggingFace/client.d.ts} +3 -3
- package/dist/lib/providers/{huggingFace.js → huggingFace/client.js} +7 -7
- package/dist/lib/providers/huggingFace/index.d.ts +2 -0
- package/dist/lib/providers/huggingFace/index.js +3 -0
- package/dist/lib/providers/huggingFace/utils.d.ts +2 -0
- package/dist/lib/providers/huggingFace/utils.js +8 -0
- package/dist/lib/providers/index.d.ts +10 -10
- package/dist/lib/providers/index.js +10 -10
- package/dist/{providers/litellm.d.ts → lib/providers/litellm/client.d.ts} +3 -3
- package/dist/lib/providers/{litellm.js → litellm/client.js} +13 -13
- package/dist/lib/providers/litellm/constants.d.ts +1 -0
- package/dist/lib/providers/litellm/constants.js +3 -0
- package/dist/lib/providers/litellm/index.d.ts +3 -0
- package/dist/lib/providers/litellm/index.js +4 -0
- package/dist/lib/providers/litellm/utils.d.ts +5 -0
- package/dist/lib/providers/litellm/utils.js +11 -0
- package/dist/{providers/nvidiaNim.d.ts → lib/providers/nvidiaNim/client.d.ts} +3 -3
- package/dist/lib/providers/{nvidiaNim.js → nvidiaNim/client.js} +8 -8
- package/dist/lib/providers/nvidiaNim/constants.d.ts +1 -0
- package/dist/lib/providers/nvidiaNim/constants.js +2 -0
- package/dist/lib/providers/nvidiaNim/index.d.ts +3 -0
- package/dist/lib/providers/nvidiaNim/index.js +4 -0
- package/dist/lib/providers/nvidiaNim/utils.d.ts +9 -0
- package/dist/lib/providers/nvidiaNim/utils.js +101 -0
- package/dist/{providers/ollama.d.ts → lib/providers/ollama/client.d.ts} +3 -3
- package/dist/{providers/ollama.js → lib/providers/ollama/client.js} +10 -13
- package/dist/lib/providers/ollama/constants.d.ts +4 -0
- package/dist/lib/providers/ollama/constants.js +6 -0
- package/dist/lib/providers/ollama/index.d.ts +3 -0
- package/dist/lib/providers/ollama/index.js +4 -0
- package/dist/lib/providers/ollama/utils.d.ts +8 -0
- package/dist/lib/providers/ollama/utils.js +43 -0
- package/dist/lib/providers/{openAI.d.ts → openAI/client.d.ts} +3 -3
- package/dist/lib/providers/{openAI.js → openAI/client.js} +13 -13
- package/dist/lib/providers/openAI/constants.d.ts +1 -0
- package/dist/lib/providers/openAI/constants.js +3 -0
- package/dist/lib/providers/openAI/index.d.ts +3 -0
- package/dist/lib/providers/openAI/index.js +4 -0
- package/dist/lib/providers/openAI/utils.d.ts +2 -0
- package/dist/lib/providers/openAI/utils.js +8 -0
- package/dist/lib/providers/{openRouter.d.ts → openRouter/client.d.ts} +3 -3
- package/dist/{providers/openRouter.js → lib/providers/openRouter/client.js} +13 -29
- package/dist/lib/providers/openRouter/constants.d.ts +1 -0
- package/dist/lib/providers/openRouter/constants.js +2 -0
- package/dist/lib/providers/openRouter/index.d.ts +3 -0
- package/dist/lib/providers/openRouter/index.js +4 -0
- package/dist/lib/providers/openRouter/utils.d.ts +3 -0
- package/dist/lib/providers/openRouter/utils.js +18 -0
- package/dist/lib/providers/{openaiCompatible.d.ts → openaiCompatible/client.d.ts} +2 -2
- package/dist/{providers/openaiCompatible.js → lib/providers/openaiCompatible/client.js} +8 -22
- package/dist/lib/providers/openaiCompatible/constants.d.ts +1 -0
- package/dist/lib/providers/openaiCompatible/constants.js +2 -0
- package/dist/lib/providers/openaiCompatible/index.d.ts +3 -0
- package/dist/lib/providers/openaiCompatible/index.js +4 -0
- package/dist/lib/providers/openaiCompatible/utils.d.ts +5 -0
- package/dist/lib/providers/openaiCompatible/utils.js +17 -0
- package/dist/lib/proxy/modelRouter.d.ts +10 -0
- package/dist/lib/proxy/modelRouter.js +17 -0
- package/dist/lib/proxy/proxyConfig.js +46 -0
- package/dist/lib/proxy/routingPolicy.d.ts +4 -2
- package/dist/lib/proxy/routingPolicy.js +8 -5
- package/dist/lib/proxy/runtimeConfig.js +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +52 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +467 -221
- package/dist/lib/server/routes/openaiProxyRoutes.js +1 -1
- package/dist/lib/types/errors.d.ts +5 -0
- package/dist/lib/types/proxy.d.ts +24 -0
- package/dist/lib/types/subscription.d.ts +4 -0
- package/dist/lib/utils/providerHealth.js +2 -1
- package/dist/lib/utils/providerUtils.js +2 -1
- package/dist/providers/{amazonBedrock.d.ts → amazonBedrock/client.d.ts} +4 -4
- package/dist/{lib/providers/amazonBedrock.js → providers/amazonBedrock/client.js} +14 -19
- package/dist/providers/amazonBedrock/constants.d.ts +1 -0
- package/dist/providers/amazonBedrock/constants.js +2 -0
- package/dist/providers/amazonBedrock/index.d.ts +3 -0
- package/dist/providers/amazonBedrock/index.js +3 -0
- package/dist/providers/amazonBedrock/utils.d.ts +1 -0
- package/dist/providers/amazonBedrock/utils.js +3 -0
- package/dist/{lib/providers/anthropic.d.ts → providers/anthropic/client.d.ts} +6 -21
- package/dist/{lib/providers/anthropic.js → providers/anthropic/client.js} +27 -46
- package/dist/providers/anthropic/constants.d.ts +4 -0
- package/dist/providers/anthropic/constants.js +8 -0
- package/dist/providers/anthropic/index.d.ts +3 -0
- package/dist/providers/anthropic/index.js +3 -0
- package/dist/providers/anthropic/utils.d.ts +7 -0
- package/dist/providers/anthropic/utils.js +149 -0
- package/dist/providers/{googleAiStudio.d.ts → googleAiStudio/client.d.ts} +4 -4
- package/dist/{lib/providers/googleAiStudio.js → providers/googleAiStudio/client.js} +15 -16
- package/dist/providers/googleAiStudio/index.d.ts +2 -0
- package/dist/providers/googleAiStudio/index.js +2 -0
- package/dist/providers/googleAiStudio/utils.d.ts +2 -0
- package/dist/providers/googleAiStudio/utils.js +18 -0
- package/dist/providers/googleNativeGemini3/constants.d.ts +1 -0
- package/dist/providers/googleNativeGemini3/constants.js +1 -0
- package/dist/providers/googleNativeGemini3/index.d.ts +2 -0
- package/dist/providers/googleNativeGemini3/index.js +2 -0
- package/dist/providers/{googleNativeGemini3.d.ts → googleNativeGemini3/utils.d.ts} +2 -2
- package/dist/providers/{googleNativeGemini3.js → googleNativeGemini3/utils.js} +7 -7
- package/dist/{lib/providers/googleVertex.d.ts → providers/googleVertex/client.d.ts} +4 -4
- package/dist/providers/{googleVertex.js → googleVertex/client.js} +31 -31
- package/dist/providers/googleVertex/constants.d.ts +5 -0
- package/dist/providers/googleVertex/constants.js +26 -0
- package/dist/providers/googleVertex/index.d.ts +3 -0
- package/dist/providers/googleVertex/index.js +3 -0
- package/dist/providers/googleVertex/utils.d.ts +12 -0
- package/dist/providers/googleVertex/utils.js +286 -0
- package/dist/providers/{huggingFace.d.ts → huggingFace/client.d.ts} +3 -3
- package/dist/providers/{huggingFace.js → huggingFace/client.js} +6 -6
- package/dist/providers/huggingFace/index.d.ts +2 -0
- package/dist/providers/huggingFace/index.js +2 -0
- package/dist/providers/huggingFace/utils.d.ts +2 -0
- package/dist/providers/huggingFace/utils.js +7 -0
- package/dist/providers/index.d.ts +10 -10
- package/dist/providers/index.js +10 -10
- package/dist/{lib/providers/litellm.d.ts → providers/litellm/client.d.ts} +3 -3
- package/dist/providers/{litellm.js → litellm/client.js} +12 -12
- package/dist/providers/litellm/constants.d.ts +1 -0
- package/dist/providers/litellm/constants.js +2 -0
- package/dist/providers/litellm/index.d.ts +3 -0
- package/dist/providers/litellm/index.js +3 -0
- package/dist/providers/litellm/utils.d.ts +5 -0
- package/dist/providers/litellm/utils.js +10 -0
- package/dist/{lib/providers/nvidiaNim.d.ts → providers/nvidiaNim/client.d.ts} +3 -3
- package/dist/providers/{nvidiaNim.js → nvidiaNim/client.js} +7 -7
- package/dist/providers/nvidiaNim/constants.d.ts +1 -0
- package/dist/providers/nvidiaNim/constants.js +1 -0
- package/dist/providers/nvidiaNim/index.d.ts +3 -0
- package/dist/providers/nvidiaNim/index.js +3 -0
- package/dist/providers/nvidiaNim/utils.d.ts +9 -0
- package/dist/providers/nvidiaNim/utils.js +100 -0
- package/dist/{lib/providers/ollama.d.ts → providers/ollama/client.d.ts} +3 -3
- package/dist/{lib/providers/ollama.js → providers/ollama/client.js} +9 -14
- package/dist/providers/ollama/constants.d.ts +4 -0
- package/dist/providers/ollama/constants.js +5 -0
- package/dist/providers/ollama/index.d.ts +3 -0
- package/dist/providers/ollama/index.js +3 -0
- package/dist/providers/ollama/utils.d.ts +8 -0
- package/dist/providers/ollama/utils.js +42 -0
- package/dist/providers/{openAI.d.ts → openAI/client.d.ts} +3 -3
- package/dist/providers/{openAI.js → openAI/client.js} +12 -12
- package/dist/providers/openAI/constants.d.ts +1 -0
- package/dist/providers/openAI/constants.js +2 -0
- package/dist/providers/openAI/index.d.ts +3 -0
- package/dist/providers/openAI/index.js +3 -0
- package/dist/providers/openAI/utils.d.ts +2 -0
- package/dist/providers/openAI/utils.js +7 -0
- package/dist/providers/{openRouter.d.ts → openRouter/client.d.ts} +3 -3
- package/dist/{lib/providers/openRouter.js → providers/openRouter/client.js} +12 -30
- package/dist/providers/openRouter/constants.d.ts +1 -0
- package/dist/providers/openRouter/constants.js +1 -0
- package/dist/providers/openRouter/index.d.ts +3 -0
- package/dist/providers/openRouter/index.js +3 -0
- package/dist/providers/openRouter/utils.d.ts +3 -0
- package/dist/providers/openRouter/utils.js +17 -0
- package/dist/providers/{openaiCompatible.d.ts → openaiCompatible/client.d.ts} +2 -2
- package/dist/{lib/providers/openaiCompatible.js → providers/openaiCompatible/client.js} +7 -23
- package/dist/providers/openaiCompatible/constants.d.ts +1 -0
- package/dist/providers/openaiCompatible/constants.js +1 -0
- package/dist/providers/openaiCompatible/index.d.ts +3 -0
- package/dist/providers/openaiCompatible/index.js +3 -0
- package/dist/providers/openaiCompatible/utils.d.ts +5 -0
- package/dist/providers/openaiCompatible/utils.js +16 -0
- package/dist/proxy/modelRouter.d.ts +10 -0
- package/dist/proxy/modelRouter.js +17 -0
- package/dist/proxy/proxyConfig.js +46 -0
- package/dist/proxy/routingPolicy.d.ts +4 -2
- package/dist/proxy/routingPolicy.js +8 -5
- package/dist/proxy/runtimeConfig.js +2 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +52 -1
- package/dist/server/routes/claudeProxyRoutes.js +467 -221
- package/dist/server/routes/openaiProxyRoutes.js +1 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/proxy.d.ts +24 -0
- package/dist/types/subscription.d.ts +4 -0
- package/dist/utils/providerHealth.js +2 -1
- package/dist/utils/providerUtils.js +2 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
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
|
|
2728
|
-
|
|
2729
|
-
|
|
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:
|
|
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
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
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
|
-
|
|
4096
|
-
attemptNumber: loopState.attemptNumber,
|
|
4284
|
+
logAttempt,
|
|
4097
4285
|
logProxyBody,
|
|
4098
|
-
|
|
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
|
-
|
|
4102
|
-
|
|
4103
|
-
if (fetchResult.
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
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
|
|
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
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
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
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
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
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
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
|
|
4161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4224
|
-
authFailureMessage: loopState.authFailureMessage,
|
|
4225
|
-
sawTransientFailure: loopState.sawTransientFailure,
|
|
4226
|
-
invalidRequestFailure: loopState.invalidRequestFailure,
|
|
4490
|
+
onStreamTerminal: admissionLease.release,
|
|
4227
4491
|
});
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
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
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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
|
-
//
|
|
4315
|
-
//
|
|
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,
|