@yansigit/opencodex 2.31.1 → 2.31.2
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/gui/dist/assets/{index-DJDp_XER.js → index-BAMgarF9.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +71 -0
- package/src/adapters/cursor.ts +90 -28
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +26 -0
- package/src/generated/compatibility-version.json +39 -15
- package/src/oauth/anthropic-routing.ts +129 -219
- package/src/oauth/antigravity-routing.ts +165 -0
- package/src/oauth/cursor-routing.ts +252 -0
- package/src/providers/cursor-pool.ts +3 -3
- package/src/routing/account-pool/affinity.ts +125 -0
- package/src/routing/account-pool/cooldown.ts +145 -0
- package/src/routing/account-pool/index.ts +45 -0
- package/src/routing/account-pool/resolve.ts +356 -0
- package/src/routing/account-pool/types.ts +32 -0
- package/src/server/management/oauth-account-routes.ts +47 -12
- package/src/server/responses/core.ts +383 -65
- package/src/types/config.ts +8 -0
- package/src/types/request.ts +7 -3
- package/src/usage/log.ts +4 -0
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* Intentionally narrower than the Codex pool: no mid-session quota rotation,
|
|
11
11
|
* soft-avoid ladders, or probe leases. Anthropic OAuth is ToS-sensitive.
|
|
12
12
|
*
|
|
13
|
-
* Affinity
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Affinity and cooldown delegate to `src/routing/account-pool/` (process-local).
|
|
14
|
+
* 401/403 credential failures should set needsReauth on the store (existing OAuth path)
|
|
15
|
+
* so the account is excluded from eligibility.
|
|
16
16
|
*/
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
17
|
import { setActiveAccount, getAccountSet, getAccountCredential } from "./store";
|
|
19
18
|
import { getCachedProviderAccountQuota } from "../providers/quota";
|
|
20
19
|
import { fallbackCodexAccountLogLabel } from "../codex/account-label";
|
|
@@ -22,25 +21,36 @@ import {
|
|
|
22
21
|
normalizeAccountPoolStickyLimit,
|
|
23
22
|
normalizeAccountPoolStrategy,
|
|
24
23
|
notePoolRotationFailure,
|
|
25
|
-
notePoolRotationSuccess,
|
|
26
24
|
pickRoundRobinAccount,
|
|
27
25
|
POOL_KEY_ANTHROPIC,
|
|
28
26
|
seedPoolRotationAccount,
|
|
29
27
|
} from "../codex/pool-rotation";
|
|
30
28
|
import type { OcxAccountPoolRotationStrategy, OcxConfig } from "../types";
|
|
31
|
-
import {
|
|
32
|
-
|
|
29
|
+
import {
|
|
30
|
+
ACCOUNT_POOL_MAX_FAILOVERS,
|
|
31
|
+
affinitySizeForTests,
|
|
32
|
+
bindSessionAffinity,
|
|
33
|
+
buildSessionKeyFromParts,
|
|
34
|
+
clearAccountPoolState,
|
|
35
|
+
clearAffinityState,
|
|
36
|
+
clearResolveState,
|
|
37
|
+
clearSessionAffinityForAccount,
|
|
38
|
+
getPoolCooldownRegistry,
|
|
39
|
+
getSessionAffinity,
|
|
40
|
+
isAccountPoolEligible,
|
|
41
|
+
isRateLimitStickWait,
|
|
42
|
+
normalizeAffinityComponent,
|
|
43
|
+
recordPoolAccountCooldown,
|
|
44
|
+
resolvePoolAccount,
|
|
45
|
+
touchSessionAffinity,
|
|
46
|
+
type AccountPoolPlugin,
|
|
47
|
+
} from "../routing/account-pool";
|
|
33
48
|
|
|
34
49
|
const PROVIDER = "anthropic";
|
|
35
|
-
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
36
|
-
const MAX_COOLDOWN_MS = 15 * 60_000;
|
|
37
|
-
const AFFINITY_IDLE_TTL_MS = 24 * 60 * 60_000;
|
|
38
|
-
const MAX_AFFINITY_ENTRIES = 2_000;
|
|
39
|
-
const MAX_AFFINITY_COMPONENT_BYTES = 512;
|
|
40
50
|
const UNKNOWN_USAGE_SCORE = 100;
|
|
41
51
|
const DEFAULT_AUTO_SWITCH_THRESHOLD = 80;
|
|
42
52
|
/** Cap same-request 429 rotations so short Retry-After cannot infinite-loop. */
|
|
43
|
-
export const ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST =
|
|
53
|
+
export const ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST = ACCOUNT_POOL_MAX_FAILOVERS;
|
|
44
54
|
|
|
45
55
|
export interface AnthropicAccountPoolConfig {
|
|
46
56
|
enabled?: boolean;
|
|
@@ -52,23 +62,24 @@ export interface AnthropicAccountPoolConfig {
|
|
|
52
62
|
stickyLimit?: number;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const anthropicPoolPlugin: AccountPoolPlugin = {
|
|
66
|
+
poolKey: POOL_KEY_ANTHROPIC,
|
|
67
|
+
sessionKeyFromRequest: buildSessionKeyFromParts,
|
|
68
|
+
listEligibleAccountIds(now) {
|
|
69
|
+
const set = getAccountSet(PROVIDER);
|
|
70
|
+
if (!set) return [];
|
|
71
|
+
return set.accounts
|
|
72
|
+
.filter(account =>
|
|
73
|
+
account.needsReauth !== true
|
|
74
|
+
&& isPoolCredentialUsable(account.id, now))
|
|
75
|
+
.map(account => account.id);
|
|
76
|
+
},
|
|
77
|
+
usageScore(accountId) {
|
|
78
|
+
return anthropicUsageScore(accountId);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
const normalized = value?.trim() ?? "";
|
|
70
|
-
return normalized && retainedUtf8Bytes(normalized) <= MAX_AFFINITY_COMPONENT_BYTES ? normalized : "";
|
|
71
|
-
}
|
|
82
|
+
const TOKEN_SKEW_MS = 60_000;
|
|
72
83
|
|
|
73
84
|
export function anthropicAccountPoolConfig(config: OcxConfig): AnthropicAccountPoolConfig {
|
|
74
85
|
const raw = config.anthropicAccountPool;
|
|
@@ -86,60 +97,34 @@ export function anthropicAutoSwitchThreshold(config: OcxConfig): number {
|
|
|
86
97
|
return DEFAULT_AUTO_SWITCH_THRESHOLD;
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
function parseRetryAfterMs(value: string | null | undefined, now: number): number | undefined {
|
|
90
|
-
const text = value?.trim();
|
|
91
|
-
if (!text) return undefined;
|
|
92
|
-
if (/^\d+(?:\.\d+)?$/.test(text)) {
|
|
93
|
-
const seconds = Number(text);
|
|
94
|
-
if (Number.isFinite(seconds) && seconds > 0) {
|
|
95
|
-
return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const timestamp = Date.parse(text);
|
|
99
|
-
if (!Number.isFinite(timestamp)) return undefined;
|
|
100
|
-
const delay = timestamp - now;
|
|
101
|
-
return delay > 0 ? Math.min(delay, MAX_COOLDOWN_MS) : undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
100
|
export function getAnthropicAccountHealthSnapshot(
|
|
105
101
|
accountId: string,
|
|
106
102
|
now = Date.now(),
|
|
107
|
-
): { cooldownUntil?: number; cooldownSource?:
|
|
108
|
-
const entry =
|
|
103
|
+
): { cooldownUntil?: number; cooldownSource?: "retry-after" | "default" } | null {
|
|
104
|
+
const entry = getPoolCooldownRegistry(POOL_KEY_ANTHROPIC).get(accountId, now);
|
|
109
105
|
if (!entry) return null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
return { cooldownUntil: entry.cooldownUntil, cooldownSource: entry.cooldownSource };
|
|
106
|
+
const source = entry.source === "retry-after" ? "retry-after" : "default";
|
|
107
|
+
return { cooldownUntil: entry.until, cooldownSource: source };
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
export function clearAnthropicAccountCooldown(accountId: string): boolean {
|
|
118
|
-
|
|
111
|
+
const registry = getPoolCooldownRegistry(POOL_KEY_ANTHROPIC);
|
|
112
|
+
const had = registry.get(accountId) !== null;
|
|
113
|
+
registry.clear(accountId);
|
|
114
|
+
return had;
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
export function sweepExpiredAnthropicRoutingHealth(now = Date.now()): number {
|
|
122
|
-
|
|
123
|
-
for (const [accountId, health] of upstreamHealth) {
|
|
124
|
-
if (health.cooldownUntil > now) continue;
|
|
125
|
-
upstreamHealth.delete(accountId);
|
|
126
|
-
removed += 1;
|
|
127
|
-
}
|
|
128
|
-
return removed;
|
|
118
|
+
return getPoolCooldownRegistry(POOL_KEY_ANTHROPIC).sweep(now);
|
|
129
119
|
}
|
|
130
120
|
|
|
131
121
|
/** Test / logout helper. */
|
|
132
122
|
export function clearAnthropicAccountPoolState(): void {
|
|
133
|
-
|
|
134
|
-
sessionAffinity.clear();
|
|
123
|
+
clearAccountPoolState(POOL_KEY_ANTHROPIC);
|
|
135
124
|
}
|
|
136
125
|
|
|
137
126
|
export function anthropicSessionAffinitySizeForTests(): number {
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function isCooled(accountId: string, now: number): boolean {
|
|
142
|
-
return getAnthropicAccountHealthSnapshot(accountId, now) !== null;
|
|
127
|
+
return affinitySizeForTests(POOL_KEY_ANTHROPIC);
|
|
143
128
|
}
|
|
144
129
|
|
|
145
130
|
function hasKnownUsage(accountId: string): boolean {
|
|
@@ -147,7 +132,7 @@ function hasKnownUsage(accountId: string): boolean {
|
|
|
147
132
|
return typeof quota?.fiveHourPercent === "number" && Number.isFinite(quota.fiveHourPercent);
|
|
148
133
|
}
|
|
149
134
|
|
|
150
|
-
function
|
|
135
|
+
function anthropicUsageScore(accountId: string): number {
|
|
151
136
|
const quota = getCachedProviderAccountQuota(PROVIDER, accountId);
|
|
152
137
|
if (!quota || typeof quota.fiveHourPercent !== "number" || !Number.isFinite(quota.fiveHourPercent)) {
|
|
153
138
|
return UNKNOWN_USAGE_SCORE;
|
|
@@ -155,8 +140,6 @@ function usageScore(accountId: string): number {
|
|
|
155
140
|
return Math.max(0, Math.min(100, quota.fiveHourPercent));
|
|
156
141
|
}
|
|
157
142
|
|
|
158
|
-
const TOKEN_SKEW_MS = 60_000;
|
|
159
|
-
|
|
160
143
|
/** Background `local-cli` slots with expired access are not pool-eligible (identity adoption risk). */
|
|
161
144
|
function isPoolCredentialUsable(accountId: string, now: number): boolean {
|
|
162
145
|
const cred = getAccountCredential(PROVIDER, accountId);
|
|
@@ -166,13 +149,19 @@ function isPoolCredentialUsable(accountId: string, now: number): boolean {
|
|
|
166
149
|
return cred.expires > now + TOKEN_SKEW_MS;
|
|
167
150
|
}
|
|
168
151
|
|
|
152
|
+
function isAnthropicAccountEligible(accountId: string, now: number): boolean {
|
|
153
|
+
return isAccountPoolEligible(POOL_KEY_ANTHROPIC, accountId, now, {
|
|
154
|
+
allowStickWait: isRateLimitStickWait(POOL_KEY_ANTHROPIC, accountId, now),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
169
158
|
export function getEligibleAnthropicAccounts(now = Date.now()): string[] {
|
|
170
159
|
const set = getAccountSet(PROVIDER);
|
|
171
160
|
if (!set) return [];
|
|
172
161
|
return set.accounts
|
|
173
162
|
.filter(account =>
|
|
174
163
|
account.needsReauth !== true
|
|
175
|
-
&&
|
|
164
|
+
&& isAnthropicAccountEligible(account.id, now)
|
|
176
165
|
&& isPoolCredentialUsable(account.id, now))
|
|
177
166
|
.map(account => account.id);
|
|
178
167
|
}
|
|
@@ -195,10 +184,10 @@ function pickLowestUsage(excludeId: string | undefined, now: number): string | n
|
|
|
195
184
|
const eligible = getEligibleAnthropicAccounts(now).filter(id => id !== excludeId);
|
|
196
185
|
if (eligible.length === 0) return null;
|
|
197
186
|
let best = eligible[0]!;
|
|
198
|
-
let bestScore =
|
|
187
|
+
let bestScore = anthropicUsageScore(best);
|
|
199
188
|
for (let i = 1; i < eligible.length; i++) {
|
|
200
189
|
const id = eligible[i]!;
|
|
201
|
-
const score =
|
|
190
|
+
const score = anthropicUsageScore(id);
|
|
202
191
|
if (score < bestScore) {
|
|
203
192
|
best = id;
|
|
204
193
|
bestScore = score;
|
|
@@ -226,7 +215,6 @@ function pickNextFillFirstAnthropicAccount(
|
|
|
226
215
|
}
|
|
227
216
|
return ordered[0] ?? null;
|
|
228
217
|
}
|
|
229
|
-
// Skip successors that are also at/above threshold (known drained usage).
|
|
230
218
|
let fallback: string | null = null;
|
|
231
219
|
for (let step = 1; step <= stableAll.length; step++) {
|
|
232
220
|
const candidate = stableAll[(startIdx + step) % stableAll.length]!;
|
|
@@ -253,16 +241,6 @@ function pickAlternateAnthropicAccount(
|
|
|
253
241
|
return pickLowestUsage(excludeId, now);
|
|
254
242
|
}
|
|
255
243
|
|
|
256
|
-
function pruneExpiredAffinity(now: number): void {
|
|
257
|
-
for (const [key, entry] of sessionAffinity) {
|
|
258
|
-
if (now - entry.lastUsedAt > AFFINITY_IDLE_TTL_MS) sessionAffinity.delete(key);
|
|
259
|
-
}
|
|
260
|
-
if (sessionAffinity.size <= MAX_AFFINITY_ENTRIES) return;
|
|
261
|
-
const sorted = [...sessionAffinity.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
|
|
262
|
-
const drop = sessionAffinity.size - MAX_AFFINITY_ENTRIES;
|
|
263
|
-
for (let i = 0; i < drop; i++) sessionAffinity.delete(sorted[i]![0]);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
244
|
export type AnthropicAccountSelectionReason =
|
|
267
245
|
| "pool-disabled"
|
|
268
246
|
| "affinity"
|
|
@@ -290,15 +268,10 @@ function anthropicPoolStrategy(config: OcxConfig): OcxAccountPoolRotationStrateg
|
|
|
290
268
|
function isActiveUnderFillFirstThreshold(config: OcxConfig, accountId: string): boolean {
|
|
291
269
|
const threshold = anthropicAutoSwitchThreshold(config);
|
|
292
270
|
if (threshold <= 0) return true;
|
|
293
|
-
// Unknown usage must not force fill-first to abandon the active account.
|
|
294
271
|
if (!hasKnownUsage(accountId)) return true;
|
|
295
|
-
return
|
|
272
|
+
return anthropicUsageScore(accountId) < threshold;
|
|
296
273
|
}
|
|
297
274
|
|
|
298
|
-
/**
|
|
299
|
-
* Fill-first: keep eligible active under threshold; otherwise advance to the next
|
|
300
|
-
* eligible id in stable sorted order after the current active (wrapping).
|
|
301
|
-
*/
|
|
302
275
|
function pickFillFirstAnthropicAccount(config: OcxConfig, now: number): string | null {
|
|
303
276
|
const eligible = getEligibleAnthropicAccounts(now);
|
|
304
277
|
if (eligible.length === 0) return null;
|
|
@@ -320,33 +293,48 @@ function pickFillFirstAnthropicAccount(config: OcxConfig, now: number): string |
|
|
|
320
293
|
return pickNextFillFirstAnthropicAccount(config, active, eligible);
|
|
321
294
|
}
|
|
322
295
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
* to the legacy quota path (or when the strategy is quota).
|
|
326
|
-
*/
|
|
327
|
-
function pickUnboundStrategyAccount(
|
|
296
|
+
function resolveAnthropicFillFirst(
|
|
297
|
+
sessionKey: string | null | undefined,
|
|
328
298
|
config: OcxConfig,
|
|
299
|
+
set: NonNullable<ReturnType<typeof getAccountSet>>,
|
|
329
300
|
now: number,
|
|
330
|
-
):
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
301
|
+
): AnthropicAccountSelection {
|
|
302
|
+
const key = normalizeAffinityComponent(sessionKey);
|
|
303
|
+
if (key) {
|
|
304
|
+
const affined = getSessionAffinity(POOL_KEY_ANTHROPIC, key, now);
|
|
305
|
+
if (affined) {
|
|
306
|
+
const stillThere = set.accounts.some(a => a.id === affined.accountId && a.needsReauth !== true);
|
|
307
|
+
if (
|
|
308
|
+
stillThere
|
|
309
|
+
&& isAnthropicAccountEligible(affined.accountId, now)
|
|
310
|
+
&& isPoolCredentialUsable(affined.accountId, now)
|
|
311
|
+
) {
|
|
312
|
+
touchSessionAffinity(POOL_KEY_ANTHROPIC, key, now);
|
|
313
|
+
return { accountId: affined.accountId, reason: "affinity" };
|
|
314
|
+
}
|
|
315
|
+
clearSessionAffinityForAccount(POOL_KEY_ANTHROPIC, affined.accountId);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
333
318
|
|
|
334
|
-
if (
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
319
|
+
if (!key) {
|
|
320
|
+
const activeOk = set.accounts.some(a => a.id === set.activeAccountId && a.needsReauth !== true)
|
|
321
|
+
&& isAnthropicAccountEligible(set.activeAccountId, now)
|
|
322
|
+
&& isPoolCredentialUsable(set.activeAccountId, now);
|
|
323
|
+
if (activeOk) {
|
|
324
|
+
return { accountId: set.activeAccountId, reason: "active" };
|
|
325
|
+
}
|
|
341
326
|
}
|
|
342
327
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return { accountId:
|
|
328
|
+
const picked = pickFillFirstAnthropicAccount(config, now);
|
|
329
|
+
if (!picked) {
|
|
330
|
+
const anyCooled = set.accounts.some(a => !isAnthropicAccountEligible(a.id, now));
|
|
331
|
+
return { accountId: null, reason: anyCooled ? "all-cooled" : "none" };
|
|
347
332
|
}
|
|
348
333
|
|
|
349
|
-
|
|
334
|
+
if (key && normalizeAffinityComponent(picked)) {
|
|
335
|
+
bindSessionAffinity(POOL_KEY_ANTHROPIC, key, picked, now);
|
|
336
|
+
}
|
|
337
|
+
return { accountId: picked, reason: "fill-first" };
|
|
350
338
|
}
|
|
351
339
|
|
|
352
340
|
/**
|
|
@@ -358,7 +346,6 @@ export function resolveAnthropicAccountForSession(
|
|
|
358
346
|
config: OcxConfig,
|
|
359
347
|
now = Date.now(),
|
|
360
348
|
): AnthropicAccountSelection {
|
|
361
|
-
pruneExpiredAffinity(now);
|
|
362
349
|
const set = getAccountSet(PROVIDER);
|
|
363
350
|
if (!set || set.accounts.length === 0) return { accountId: null, reason: "none" };
|
|
364
351
|
|
|
@@ -366,87 +353,27 @@ export function resolveAnthropicAccountForSession(
|
|
|
366
353
|
return { accountId: set.activeAccountId, reason: "pool-disabled" };
|
|
367
354
|
}
|
|
368
355
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const affined = sessionAffinity.get(key);
|
|
372
|
-
if (affined && now - affined.lastUsedAt <= AFFINITY_IDLE_TTL_MS) {
|
|
373
|
-
const stillThere = set.accounts.some(a => a.id === affined.accountId && a.needsReauth !== true);
|
|
374
|
-
if (stillThere && !isCooled(affined.accountId, now) && isPoolCredentialUsable(affined.accountId, now)) {
|
|
375
|
-
affined.lastUsedAt = now;
|
|
376
|
-
return { accountId: affined.accountId, reason: "affinity" };
|
|
377
|
-
}
|
|
378
|
-
sessionAffinity.delete(key);
|
|
379
|
-
}
|
|
356
|
+
if (anthropicPoolStrategy(config) === "fill-first") {
|
|
357
|
+
return resolveAnthropicFillFirst(sessionKey, config, set, now);
|
|
380
358
|
}
|
|
381
359
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const strategyPick = pickUnboundStrategyAccount(config, now);
|
|
396
|
-
if (strategyPick) {
|
|
397
|
-
// Do not promote active here — token validation may still fail. Callers
|
|
398
|
-
// (responses/core) promote after getAnthropicPoolAccessToken succeeds.
|
|
399
|
-
if (key && normalizeAffinityComponent(strategyPick.accountId)) {
|
|
400
|
-
sessionAffinity.set(key, { accountId: strategyPick.accountId, lastUsedAt: now });
|
|
401
|
-
pruneExpiredAffinity(now);
|
|
402
|
-
}
|
|
403
|
-
return { accountId: strategyPick.accountId, reason: strategyPick.reason };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const threshold = anthropicAutoSwitchThreshold(config);
|
|
407
|
-
const activeOk = set.accounts.some(a => a.id === set.activeAccountId && a.needsReauth !== true)
|
|
408
|
-
&& !isCooled(set.activeAccountId, now)
|
|
409
|
-
&& isPoolCredentialUsable(set.activeAccountId, now);
|
|
410
|
-
|
|
411
|
-
let accountId: string | null = null;
|
|
412
|
-
let reason: AnthropicAccountSelectionReason = "none";
|
|
413
|
-
|
|
414
|
-
if (threshold > 0) {
|
|
415
|
-
// Unknown usage must NOT force a switch away from the healthy active account.
|
|
416
|
-
if (activeOk && (!hasKnownUsage(set.activeAccountId) || usageScore(set.activeAccountId) < threshold)) {
|
|
417
|
-
accountId = set.activeAccountId;
|
|
418
|
-
reason = "active";
|
|
419
|
-
} else {
|
|
420
|
-
const picked = pickLowestUsage(undefined, now);
|
|
421
|
-
if (picked) {
|
|
422
|
-
accountId = picked;
|
|
423
|
-
reason = activeOk && picked === set.activeAccountId ? "active" : "lowest-usage";
|
|
424
|
-
} else if (activeOk) {
|
|
425
|
-
accountId = set.activeAccountId;
|
|
426
|
-
reason = "active";
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} else if (activeOk) {
|
|
430
|
-
accountId = set.activeAccountId;
|
|
431
|
-
reason = "active";
|
|
432
|
-
} else {
|
|
433
|
-
const picked = pickLowestUsage(set.activeAccountId, now);
|
|
434
|
-
if (picked) {
|
|
435
|
-
accountId = picked;
|
|
436
|
-
reason = "only-eligible";
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (!accountId) {
|
|
441
|
-
const anyCooled = set.accounts.some(a => isCooled(a.id, now));
|
|
442
|
-
return { accountId: null, reason: anyCooled ? "all-cooled" : "none" };
|
|
443
|
-
}
|
|
360
|
+
const kernelResult = resolvePoolAccount(
|
|
361
|
+
anthropicPoolPlugin,
|
|
362
|
+
sessionKey ?? null,
|
|
363
|
+
{
|
|
364
|
+
strategy: anthropicPoolStrategy(config) === "round-robin" ? "round-robin" : "quota",
|
|
365
|
+
enabled: true,
|
|
366
|
+
activeAccountId: set.activeAccountId,
|
|
367
|
+
stickyLimit: stickyLimitForPool(config),
|
|
368
|
+
autoSwitchThreshold: anthropicAutoSwitchThreshold(config),
|
|
369
|
+
},
|
|
370
|
+
now,
|
|
371
|
+
);
|
|
444
372
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
return { accountId, reason };
|
|
373
|
+
return {
|
|
374
|
+
accountId: kernelResult.accountId,
|
|
375
|
+
reason: kernelResult.reason as AnthropicAccountSelectionReason,
|
|
376
|
+
};
|
|
450
377
|
}
|
|
451
378
|
|
|
452
379
|
export function bindAnthropicSessionAffinity(
|
|
@@ -454,16 +381,11 @@ export function bindAnthropicSessionAffinity(
|
|
|
454
381
|
accountId: string,
|
|
455
382
|
now = Date.now(),
|
|
456
383
|
): void {
|
|
457
|
-
|
|
458
|
-
if (!key || !normalizeAffinityComponent(accountId)) return;
|
|
459
|
-
sessionAffinity.set(key, { accountId, lastUsedAt: now });
|
|
460
|
-
pruneExpiredAffinity(now);
|
|
384
|
+
bindSessionAffinity(POOL_KEY_ANTHROPIC, sessionKey, accountId, now);
|
|
461
385
|
}
|
|
462
386
|
|
|
463
387
|
export function clearAnthropicSessionAffinityForAccount(accountId: string): void {
|
|
464
|
-
|
|
465
|
-
if (entry.accountId === accountId) sessionAffinity.delete(key);
|
|
466
|
-
}
|
|
388
|
+
clearSessionAffinityForAccount(POOL_KEY_ANTHROPIC, accountId);
|
|
467
389
|
}
|
|
468
390
|
|
|
469
391
|
/**
|
|
@@ -480,14 +402,14 @@ export function rotateAnthropicAccountOn429(
|
|
|
480
402
|
): string | null {
|
|
481
403
|
if (!isAnthropicAccountPoolEnabled(config)) return null;
|
|
482
404
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
405
|
+
recordPoolAccountCooldown(
|
|
406
|
+
POOL_KEY_ANTHROPIC,
|
|
407
|
+
failedAccountId,
|
|
408
|
+
"rate_limit",
|
|
409
|
+
retryAfterHeader,
|
|
410
|
+
now,
|
|
411
|
+
);
|
|
412
|
+
clearSessionAffinityForAccount(POOL_KEY_ANTHROPIC, failedAccountId);
|
|
491
413
|
notePoolRotationFailure(POOL_KEY_ANTHROPIC, failedAccountId);
|
|
492
414
|
|
|
493
415
|
const next = pickAlternateAnthropicAccount(config, failedAccountId, now);
|
|
@@ -498,8 +420,7 @@ export function rotateAnthropicAccountOn429(
|
|
|
498
420
|
|
|
499
421
|
const affinityKey = normalizeAffinityComponent(sessionKey);
|
|
500
422
|
if (affinityKey && normalizeAffinityComponent(next)) {
|
|
501
|
-
|
|
502
|
-
pruneExpiredAffinity(now);
|
|
423
|
+
bindSessionAffinity(POOL_KEY_ANTHROPIC, affinityKey, next, now);
|
|
503
424
|
}
|
|
504
425
|
console.warn(
|
|
505
426
|
`[anthropic-pool] 429 on ${formatAnthropicAccountOrdinal(failedAccountId)}; failing over to ${formatAnthropicAccountOrdinal(next)}`,
|
|
@@ -517,7 +438,8 @@ export function promoteAnthropicActiveAccount(accountId: string): void {
|
|
|
517
438
|
* unbound new session honors the operator-chosen account (Codex parity).
|
|
518
439
|
*/
|
|
519
440
|
export function resetAnthropicRoutingForManualSelection(accountId: string): void {
|
|
520
|
-
|
|
441
|
+
clearAffinityState(POOL_KEY_ANTHROPIC);
|
|
442
|
+
clearResolveState(POOL_KEY_ANTHROPIC);
|
|
521
443
|
seedPoolRotationAccount(POOL_KEY_ANTHROPIC, accountId);
|
|
522
444
|
}
|
|
523
445
|
|
|
@@ -578,17 +500,5 @@ export function anthropicSessionKeyFromParts(input: {
|
|
|
578
500
|
/** When true, prompt_cache_key is a shared Desktop cohort — ignore it for affinity. */
|
|
579
501
|
promptCacheKeyIsSharedCohort?: boolean;
|
|
580
502
|
}): string | null {
|
|
581
|
-
|
|
582
|
-
input.clientThreadId
|
|
583
|
-
?? input.sessionIdHeader
|
|
584
|
-
?? input.threadIdHeader
|
|
585
|
-
?? ""
|
|
586
|
-
).trim();
|
|
587
|
-
if (preferred) {
|
|
588
|
-
return preferred.length <= 128 ? preferred : createHash("sha256").update(preferred).digest("hex");
|
|
589
|
-
}
|
|
590
|
-
if (input.promptCacheKeyIsSharedCohort) return null;
|
|
591
|
-
const cacheKey = input.promptCacheKey?.trim() ?? "";
|
|
592
|
-
if (!cacheKey) return null;
|
|
593
|
-
return cacheKey.length <= 128 ? cacheKey : createHash("sha256").update(cacheKey).digest("hex");
|
|
503
|
+
return buildSessionKeyFromParts(input);
|
|
594
504
|
}
|