@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1
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/README.md +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1816 -594
- package/package.json +1 -1
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -174
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +31 -13
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
package/src/accounts.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { RateLimitReason } from "./backoff.js";
|
|
2
2
|
import { calculateBackoffMs } from "./backoff.js";
|
|
3
3
|
import { readCCCredentials } from "./cc-credentials.js";
|
|
4
|
+
import {
|
|
5
|
+
findByIdentity,
|
|
6
|
+
resolveIdentity,
|
|
7
|
+
resolveIdentityFromCCCredential,
|
|
8
|
+
type AccountIdentity,
|
|
9
|
+
} from "./account-identity.js";
|
|
4
10
|
import type { AnthropicAuthConfig } from "./config.js";
|
|
5
11
|
import { HealthScoreTracker, selectAccount, TokenBucketTracker } from "./rotation.js";
|
|
6
|
-
import type { AccountMetadata, AccountStorage } from "./storage.js";
|
|
12
|
+
import type { AccountMetadata, AccountStats, AccountStorage } from "./storage.js";
|
|
7
13
|
import { createDefaultStats, loadAccounts, saveAccounts } from "./storage.js";
|
|
8
14
|
|
|
9
15
|
export interface ManagedAccount {
|
|
10
16
|
id: string;
|
|
11
17
|
index: number;
|
|
12
18
|
email?: string;
|
|
19
|
+
identity?: AccountIdentity;
|
|
20
|
+
label?: string;
|
|
13
21
|
refreshToken: string;
|
|
14
22
|
access?: string;
|
|
15
23
|
expires?: number;
|
|
@@ -21,7 +29,7 @@ export interface ManagedAccount {
|
|
|
21
29
|
consecutiveFailures: number;
|
|
22
30
|
lastFailureTime: number | null;
|
|
23
31
|
lastSwitchReason?: string;
|
|
24
|
-
stats:
|
|
32
|
+
stats: AccountStats;
|
|
25
33
|
source?: "cc-keychain" | "cc-file" | "oauth";
|
|
26
34
|
}
|
|
27
35
|
|
|
@@ -40,6 +48,167 @@ export interface StatsDelta {
|
|
|
40
48
|
const MAX_ACCOUNTS = 10;
|
|
41
49
|
const RATE_LIMIT_KEY = "anthropic";
|
|
42
50
|
|
|
51
|
+
type ManagedAccountSource = ManagedAccount["source"];
|
|
52
|
+
|
|
53
|
+
type AddAccountOptions = {
|
|
54
|
+
identity?: AccountIdentity;
|
|
55
|
+
label?: string;
|
|
56
|
+
source?: ManagedAccountSource;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ManagedAccountInit = {
|
|
60
|
+
id?: string;
|
|
61
|
+
index: number;
|
|
62
|
+
email?: string;
|
|
63
|
+
identity?: AccountIdentity;
|
|
64
|
+
label?: string;
|
|
65
|
+
refreshToken: string;
|
|
66
|
+
access?: string;
|
|
67
|
+
expires?: number;
|
|
68
|
+
tokenUpdatedAt?: number;
|
|
69
|
+
addedAt?: number;
|
|
70
|
+
lastUsed?: number;
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
rateLimitResetTimes?: Record<string, number>;
|
|
73
|
+
consecutiveFailures?: number;
|
|
74
|
+
lastFailureTime?: number | null;
|
|
75
|
+
lastSwitchReason?: string;
|
|
76
|
+
stats?: AccountStats;
|
|
77
|
+
source?: ManagedAccountSource;
|
|
78
|
+
now?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function resolveAccountIdentity(params: {
|
|
82
|
+
refreshToken: string;
|
|
83
|
+
email?: string;
|
|
84
|
+
identity?: AccountIdentity;
|
|
85
|
+
label?: string;
|
|
86
|
+
source?: ManagedAccountSource;
|
|
87
|
+
}): AccountIdentity {
|
|
88
|
+
if (params.identity) {
|
|
89
|
+
return params.identity;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ((params.source === "cc-keychain" || params.source === "cc-file") && params.label) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "cc",
|
|
95
|
+
source: params.source,
|
|
96
|
+
label: params.label,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (params.email) {
|
|
101
|
+
return {
|
|
102
|
+
kind: "oauth",
|
|
103
|
+
email: params.email,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
kind: "legacy",
|
|
109
|
+
refreshToken: params.refreshToken,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createManagedAccount(init: ManagedAccountInit): ManagedAccount {
|
|
114
|
+
const now = init.now ?? Date.now();
|
|
115
|
+
const addedAt = init.addedAt ?? now;
|
|
116
|
+
const tokenUpdatedAt = init.tokenUpdatedAt ?? addedAt;
|
|
117
|
+
const identity = resolveAccountIdentity({
|
|
118
|
+
refreshToken: init.refreshToken,
|
|
119
|
+
email: init.email,
|
|
120
|
+
identity: init.identity,
|
|
121
|
+
label: init.label,
|
|
122
|
+
source: init.source,
|
|
123
|
+
});
|
|
124
|
+
const email = init.email ?? (identity.kind === "oauth" ? identity.email : undefined);
|
|
125
|
+
const label = init.label ?? (identity.kind === "cc" ? identity.label : undefined);
|
|
126
|
+
const source = init.source ?? (identity.kind === "cc" ? identity.source : "oauth");
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
id: init.id ?? `${addedAt}:${init.refreshToken.slice(0, 12)}`,
|
|
130
|
+
index: init.index,
|
|
131
|
+
email,
|
|
132
|
+
identity,
|
|
133
|
+
label,
|
|
134
|
+
refreshToken: init.refreshToken,
|
|
135
|
+
access: init.access,
|
|
136
|
+
expires: init.expires,
|
|
137
|
+
tokenUpdatedAt,
|
|
138
|
+
addedAt,
|
|
139
|
+
lastUsed: init.lastUsed ?? 0,
|
|
140
|
+
enabled: init.enabled ?? true,
|
|
141
|
+
rateLimitResetTimes: { ...(init.rateLimitResetTimes ?? {}) },
|
|
142
|
+
consecutiveFailures: init.consecutiveFailures ?? 0,
|
|
143
|
+
lastFailureTime: init.lastFailureTime ?? null,
|
|
144
|
+
lastSwitchReason: init.lastSwitchReason ?? "initial",
|
|
145
|
+
stats: init.stats ?? createDefaultStats(addedAt),
|
|
146
|
+
source,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findMatchingAccount(
|
|
151
|
+
accounts: ManagedAccount[],
|
|
152
|
+
params: {
|
|
153
|
+
id?: string;
|
|
154
|
+
identity?: AccountIdentity;
|
|
155
|
+
refreshToken?: string;
|
|
156
|
+
},
|
|
157
|
+
): ManagedAccount | null {
|
|
158
|
+
if (params.id) {
|
|
159
|
+
const byId = accounts.find((account) => account.id === params.id);
|
|
160
|
+
if (byId) return byId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (params.identity) {
|
|
164
|
+
const byIdentity = findByIdentity(accounts, params.identity);
|
|
165
|
+
if (byIdentity) return byIdentity;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (params.refreshToken) {
|
|
169
|
+
return accounts.find((account) => account.refreshToken === params.refreshToken) ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function reindexAccounts(accounts: ManagedAccount[]): void {
|
|
176
|
+
accounts.forEach((account, index) => {
|
|
177
|
+
account.index = index;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function updateManagedAccountFromStorage(existing: ManagedAccount, account: AccountMetadata, index: number): void {
|
|
182
|
+
const source = account.source || existing.source || "oauth";
|
|
183
|
+
const label = account.label ?? existing.label;
|
|
184
|
+
const email = account.email ?? existing.email;
|
|
185
|
+
|
|
186
|
+
existing.id = account.id || existing.id || `${account.addedAt}:${account.refreshToken.slice(0, 12)}`;
|
|
187
|
+
existing.index = index;
|
|
188
|
+
existing.email = email;
|
|
189
|
+
existing.label = label;
|
|
190
|
+
existing.identity = resolveAccountIdentity({
|
|
191
|
+
refreshToken: account.refreshToken,
|
|
192
|
+
email,
|
|
193
|
+
identity: account.identity ?? existing.identity,
|
|
194
|
+
label,
|
|
195
|
+
source,
|
|
196
|
+
});
|
|
197
|
+
existing.refreshToken = account.refreshToken;
|
|
198
|
+
existing.access = account.access ?? existing.access;
|
|
199
|
+
existing.expires = account.expires ?? existing.expires;
|
|
200
|
+
existing.tokenUpdatedAt = account.token_updated_at ?? existing.tokenUpdatedAt ?? account.addedAt;
|
|
201
|
+
existing.addedAt = account.addedAt;
|
|
202
|
+
existing.lastUsed = account.lastUsed;
|
|
203
|
+
existing.enabled = account.enabled;
|
|
204
|
+
existing.rateLimitResetTimes = { ...account.rateLimitResetTimes };
|
|
205
|
+
existing.consecutiveFailures = account.consecutiveFailures;
|
|
206
|
+
existing.lastFailureTime = account.lastFailureTime;
|
|
207
|
+
existing.lastSwitchReason = account.lastSwitchReason || existing.lastSwitchReason || "initial";
|
|
208
|
+
existing.stats = account.stats ?? existing.stats ?? createDefaultStats(account.addedAt);
|
|
209
|
+
existing.source = source;
|
|
210
|
+
}
|
|
211
|
+
|
|
43
212
|
export class AccountManager {
|
|
44
213
|
#accounts: ManagedAccount[] = [];
|
|
45
214
|
#cursor = 0;
|
|
@@ -49,6 +218,13 @@ export class AccountManager {
|
|
|
49
218
|
#config: AnthropicAuthConfig;
|
|
50
219
|
#saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
51
220
|
#statsDeltas = new Map<string, StatsDelta>();
|
|
221
|
+
/**
|
|
222
|
+
* Cap on pending stats deltas. When hit, a forced flush is scheduled so the
|
|
223
|
+
* map does not grow without bound between debounced saves. This is only a
|
|
224
|
+
* safety net — under normal load the 1s debounced save in `requestSaveToDisk`
|
|
225
|
+
* keeps the delta count below this cap.
|
|
226
|
+
*/
|
|
227
|
+
readonly #MAX_STATS_DELTAS = 100;
|
|
52
228
|
|
|
53
229
|
constructor(config: AnthropicAuthConfig) {
|
|
54
230
|
this.#config = config;
|
|
@@ -56,6 +232,11 @@ export class AccountManager {
|
|
|
56
232
|
this.#tokenTracker = new TokenBucketTracker(config.token_bucket);
|
|
57
233
|
}
|
|
58
234
|
|
|
235
|
+
#rebuildTrackers(): void {
|
|
236
|
+
this.#healthTracker = new HealthScoreTracker(this.#config.health_score);
|
|
237
|
+
this.#tokenTracker = new TokenBucketTracker(this.#config.token_bucket);
|
|
238
|
+
}
|
|
239
|
+
|
|
59
240
|
/**
|
|
60
241
|
* Load accounts from disk, optionally merging with an OpenCode auth fallback.
|
|
61
242
|
*/
|
|
@@ -72,30 +253,41 @@ export class AccountManager {
|
|
|
72
253
|
|
|
73
254
|
// If storage exists (even with zero accounts), treat disk as authoritative.
|
|
74
255
|
if (stored) {
|
|
75
|
-
manager.#accounts = stored.accounts.map((acc, index) =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
256
|
+
manager.#accounts = stored.accounts.map((acc, index) =>
|
|
257
|
+
createManagedAccount({
|
|
258
|
+
id: acc.id || `${acc.addedAt}:${acc.refreshToken.slice(0, 12)}`,
|
|
259
|
+
index,
|
|
260
|
+
email: acc.email,
|
|
261
|
+
identity: acc.identity,
|
|
262
|
+
label: acc.label,
|
|
263
|
+
refreshToken: acc.refreshToken,
|
|
264
|
+
access: acc.access,
|
|
265
|
+
expires: acc.expires,
|
|
266
|
+
tokenUpdatedAt: acc.token_updated_at,
|
|
267
|
+
addedAt: acc.addedAt,
|
|
268
|
+
lastUsed: acc.lastUsed,
|
|
269
|
+
enabled: acc.enabled,
|
|
270
|
+
rateLimitResetTimes: acc.rateLimitResetTimes,
|
|
271
|
+
consecutiveFailures: acc.consecutiveFailures,
|
|
272
|
+
lastFailureTime: acc.lastFailureTime,
|
|
273
|
+
lastSwitchReason: acc.lastSwitchReason,
|
|
274
|
+
stats: acc.stats,
|
|
275
|
+
source: acc.source || "oauth",
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
93
278
|
|
|
94
279
|
manager.#currentIndex =
|
|
95
280
|
manager.#accounts.length > 0 ? Math.min(stored.activeIndex, manager.#accounts.length - 1) : -1;
|
|
96
281
|
|
|
97
282
|
if (authFallback && manager.#accounts.length > 0) {
|
|
98
|
-
const
|
|
283
|
+
const fallbackIdentity = resolveAccountIdentity({
|
|
284
|
+
refreshToken: authFallback.refresh,
|
|
285
|
+
source: "oauth",
|
|
286
|
+
});
|
|
287
|
+
const match = findMatchingAccount(manager.#accounts, {
|
|
288
|
+
identity: fallbackIdentity,
|
|
289
|
+
refreshToken: authFallback.refresh,
|
|
290
|
+
});
|
|
99
291
|
if (match) {
|
|
100
292
|
const fallbackHasAccess = typeof authFallback.access === "string" && authFallback.access.length > 0;
|
|
101
293
|
const fallbackExpires = typeof authFallback.expires === "number" ? authFallback.expires : 0;
|
|
@@ -115,23 +307,17 @@ export class AccountManager {
|
|
|
115
307
|
} else if (authFallback && authFallback.refresh) {
|
|
116
308
|
const now = Date.now();
|
|
117
309
|
manager.#accounts = [
|
|
118
|
-
{
|
|
310
|
+
createManagedAccount({
|
|
119
311
|
id: `${now}:${authFallback.refresh.slice(0, 12)}`,
|
|
120
312
|
index: 0,
|
|
121
|
-
email: undefined,
|
|
122
313
|
refreshToken: authFallback.refresh,
|
|
123
314
|
access: authFallback.access,
|
|
124
315
|
expires: authFallback.expires,
|
|
125
316
|
tokenUpdatedAt: now,
|
|
126
317
|
addedAt: now,
|
|
127
|
-
lastUsed: 0,
|
|
128
|
-
enabled: true,
|
|
129
|
-
rateLimitResetTimes: {},
|
|
130
|
-
consecutiveFailures: 0,
|
|
131
|
-
lastFailureTime: null,
|
|
132
318
|
lastSwitchReason: "initial",
|
|
133
|
-
|
|
134
|
-
},
|
|
319
|
+
source: "oauth",
|
|
320
|
+
}),
|
|
135
321
|
];
|
|
136
322
|
manager.#currentIndex = 0;
|
|
137
323
|
}
|
|
@@ -147,19 +333,38 @@ export class AccountManager {
|
|
|
147
333
|
})();
|
|
148
334
|
|
|
149
335
|
for (const ccCredential of ccCredentials) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
336
|
+
const ccIdentity = resolveIdentityFromCCCredential(ccCredential);
|
|
337
|
+
let existingMatch = findMatchingAccount(manager.#accounts, {
|
|
338
|
+
identity: ccIdentity,
|
|
339
|
+
refreshToken: ccCredential.refreshToken,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (!existingMatch) {
|
|
343
|
+
const legacyUnlabeledMatches = manager.#accounts.filter(
|
|
344
|
+
(account) => account.source === ccCredential.source && !account.label && !account.email,
|
|
345
|
+
);
|
|
346
|
+
if (legacyUnlabeledMatches.length === 1) {
|
|
347
|
+
existingMatch = legacyUnlabeledMatches[0]!;
|
|
157
348
|
}
|
|
158
|
-
|
|
159
|
-
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (existingMatch) {
|
|
352
|
+
existingMatch.refreshToken = ccCredential.refreshToken;
|
|
353
|
+
existingMatch.identity = ccIdentity;
|
|
354
|
+
existingMatch.source = ccCredential.source;
|
|
355
|
+
existingMatch.label = ccCredential.label;
|
|
356
|
+
existingMatch.enabled = true;
|
|
357
|
+
if (ccCredential.accessToken) {
|
|
160
358
|
existingMatch.access = ccCredential.accessToken;
|
|
359
|
+
}
|
|
360
|
+
if (ccCredential.expiresAt >= (existingMatch.expires ?? 0)) {
|
|
161
361
|
existingMatch.expires = ccCredential.expiresAt;
|
|
162
362
|
}
|
|
363
|
+
existingMatch.tokenUpdatedAt = Math.max(existingMatch.tokenUpdatedAt || 0, ccCredential.expiresAt || 0);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (manager.#accounts.length >= MAX_ACCOUNTS) {
|
|
163
368
|
continue;
|
|
164
369
|
}
|
|
165
370
|
|
|
@@ -172,24 +377,19 @@ export class AccountManager {
|
|
|
172
377
|
}
|
|
173
378
|
|
|
174
379
|
const now = Date.now();
|
|
175
|
-
const ccAccount
|
|
380
|
+
const ccAccount = createManagedAccount({
|
|
176
381
|
id: `cc-${ccCredential.source}-${now}:${ccCredential.refreshToken.slice(0, 12)}`,
|
|
177
382
|
index: manager.#accounts.length,
|
|
178
|
-
email: undefined,
|
|
179
383
|
refreshToken: ccCredential.refreshToken,
|
|
180
384
|
access: ccCredential.accessToken,
|
|
181
385
|
expires: ccCredential.expiresAt,
|
|
182
386
|
tokenUpdatedAt: now,
|
|
183
387
|
addedAt: now,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
rateLimitResetTimes: {},
|
|
187
|
-
consecutiveFailures: 0,
|
|
188
|
-
lastFailureTime: null,
|
|
388
|
+
identity: ccIdentity,
|
|
389
|
+
label: ccCredential.label,
|
|
189
390
|
lastSwitchReason: "cc-auto-detected",
|
|
190
|
-
stats: createDefaultStats(now),
|
|
191
391
|
source: ccCredential.source,
|
|
192
|
-
};
|
|
392
|
+
});
|
|
193
393
|
|
|
194
394
|
manager.#accounts.push(ccAccount);
|
|
195
395
|
}
|
|
@@ -198,9 +398,7 @@ export class AccountManager {
|
|
|
198
398
|
manager.#accounts = [...manager.getCCAccounts(), ...manager.getOAuthAccounts()];
|
|
199
399
|
}
|
|
200
400
|
|
|
201
|
-
manager.#accounts
|
|
202
|
-
account.index = index;
|
|
203
|
-
});
|
|
401
|
+
reindexAccounts(manager.#accounts);
|
|
204
402
|
|
|
205
403
|
if (config.cc_credential_reuse.prefer_over_oauth && manager.getCCAccounts().length > 0) {
|
|
206
404
|
manager.#currentIndex = 0;
|
|
@@ -372,37 +570,56 @@ export class AccountManager {
|
|
|
372
570
|
* Add a new account to the pool.
|
|
373
571
|
* @returns The new account, or null if at capacity
|
|
374
572
|
*/
|
|
375
|
-
addAccount(
|
|
376
|
-
|
|
573
|
+
addAccount(
|
|
574
|
+
refreshToken: string,
|
|
575
|
+
accessToken: string,
|
|
576
|
+
expires: number,
|
|
577
|
+
email?: string,
|
|
578
|
+
options?: AddAccountOptions,
|
|
579
|
+
): ManagedAccount | null {
|
|
580
|
+
const identity = resolveAccountIdentity({
|
|
581
|
+
refreshToken,
|
|
582
|
+
email,
|
|
583
|
+
identity: options?.identity,
|
|
584
|
+
label: options?.label,
|
|
585
|
+
source: options?.source ?? "oauth",
|
|
586
|
+
});
|
|
587
|
+
const existing = findMatchingAccount(this.#accounts, {
|
|
588
|
+
identity,
|
|
589
|
+
refreshToken,
|
|
590
|
+
});
|
|
377
591
|
|
|
378
|
-
const existing = this.#accounts.find((acc) => acc.refreshToken === refreshToken);
|
|
379
592
|
if (existing) {
|
|
593
|
+
existing.refreshToken = refreshToken;
|
|
380
594
|
existing.access = accessToken;
|
|
381
595
|
existing.expires = expires;
|
|
382
596
|
existing.tokenUpdatedAt = Date.now();
|
|
383
|
-
|
|
597
|
+
existing.email = email ?? existing.email;
|
|
598
|
+
existing.identity = identity;
|
|
599
|
+
existing.label = options?.label ?? existing.label;
|
|
600
|
+
existing.source = options?.source ?? existing.source ?? "oauth";
|
|
384
601
|
existing.enabled = true;
|
|
602
|
+
this.requestSaveToDisk();
|
|
385
603
|
return existing;
|
|
386
604
|
}
|
|
387
605
|
|
|
606
|
+
if (this.#accounts.length >= MAX_ACCOUNTS) return null;
|
|
607
|
+
|
|
388
608
|
const now = Date.now();
|
|
389
|
-
const account
|
|
609
|
+
const account = createManagedAccount({
|
|
390
610
|
id: `${now}:${refreshToken.slice(0, 12)}`,
|
|
391
611
|
index: this.#accounts.length,
|
|
392
|
-
email,
|
|
393
612
|
refreshToken,
|
|
394
613
|
access: accessToken,
|
|
395
614
|
expires,
|
|
396
615
|
tokenUpdatedAt: now,
|
|
397
616
|
addedAt: now,
|
|
398
|
-
lastUsed: 0,
|
|
399
|
-
enabled: true,
|
|
400
|
-
rateLimitResetTimes: {},
|
|
401
|
-
consecutiveFailures: 0,
|
|
402
|
-
lastFailureTime: null,
|
|
403
617
|
lastSwitchReason: "initial",
|
|
404
|
-
|
|
405
|
-
|
|
618
|
+
email,
|
|
619
|
+
identity,
|
|
620
|
+
label: options?.label,
|
|
621
|
+
source: options?.source ?? "oauth",
|
|
622
|
+
});
|
|
406
623
|
|
|
407
624
|
this.#accounts.push(account);
|
|
408
625
|
|
|
@@ -422,9 +639,7 @@ export class AccountManager {
|
|
|
422
639
|
|
|
423
640
|
this.#accounts.splice(index, 1);
|
|
424
641
|
|
|
425
|
-
this.#accounts
|
|
426
|
-
acc.index = i;
|
|
427
|
-
});
|
|
642
|
+
reindexAccounts(this.#accounts);
|
|
428
643
|
|
|
429
644
|
if (this.#accounts.length === 0) {
|
|
430
645
|
this.#currentIndex = -1;
|
|
@@ -438,9 +653,7 @@ export class AccountManager {
|
|
|
438
653
|
}
|
|
439
654
|
}
|
|
440
655
|
|
|
441
|
-
|
|
442
|
-
this.#healthTracker.reset(i);
|
|
443
|
-
}
|
|
656
|
+
this.#rebuildTrackers();
|
|
444
657
|
this.requestSaveToDisk();
|
|
445
658
|
return true;
|
|
446
659
|
}
|
|
@@ -474,7 +687,12 @@ export class AccountManager {
|
|
|
474
687
|
if (this.#saveTimeout) clearTimeout(this.#saveTimeout);
|
|
475
688
|
this.#saveTimeout = setTimeout(() => {
|
|
476
689
|
this.#saveTimeout = null;
|
|
477
|
-
this.saveToDisk().catch(() => {
|
|
690
|
+
this.saveToDisk().catch((err) => {
|
|
691
|
+
if (this.#config.debug) {
|
|
692
|
+
// eslint-disable-next-line no-console -- debug-gated stderr logging; plugin has no dedicated logger
|
|
693
|
+
console.error("[opencode-anthropic-auth] saveToDisk failed:", (err as Error).message);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
478
696
|
}, 1000);
|
|
479
697
|
}
|
|
480
698
|
|
|
@@ -487,9 +705,11 @@ export class AccountManager {
|
|
|
487
705
|
let diskAccountsById: Map<string, AccountMetadata> | null = null;
|
|
488
706
|
let diskAccountsByAddedAt: Map<number, AccountMetadata[]> | null = null;
|
|
489
707
|
let diskAccountsByRefreshToken: Map<string, AccountMetadata> | null = null;
|
|
708
|
+
let diskAccounts: AccountMetadata[] = [];
|
|
490
709
|
try {
|
|
491
710
|
const diskData = await loadAccounts();
|
|
492
711
|
if (diskData) {
|
|
712
|
+
diskAccounts = diskData.accounts;
|
|
493
713
|
diskAccountsById = new Map(diskData.accounts.map((a) => [a.id, a]));
|
|
494
714
|
diskAccountsByAddedAt = new Map();
|
|
495
715
|
diskAccountsByRefreshToken = new Map();
|
|
@@ -508,6 +728,9 @@ export class AccountManager {
|
|
|
508
728
|
const byId = diskAccountsById?.get(account.id);
|
|
509
729
|
if (byId) return byId;
|
|
510
730
|
|
|
731
|
+
const byIdentity = findByIdentity(diskAccounts, resolveIdentity(account));
|
|
732
|
+
if (byIdentity) return byIdentity;
|
|
733
|
+
|
|
511
734
|
const byAddedAt = diskAccountsByAddedAt?.get(account.addedAt);
|
|
512
735
|
if (byAddedAt?.length === 1) return byAddedAt[0]!;
|
|
513
736
|
|
|
@@ -518,78 +741,101 @@ export class AccountManager {
|
|
|
518
741
|
return null;
|
|
519
742
|
};
|
|
520
743
|
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
744
|
+
const matchedDiskAccounts = new Set<AccountMetadata>();
|
|
745
|
+
const activeAccountId = this.#accounts[this.#currentIndex]?.id ?? null;
|
|
746
|
+
const accountsToPersist = this.#accounts.filter((account) => account.enabled || !!findDiskAccount(account));
|
|
747
|
+
|
|
748
|
+
const persistedAccounts = accountsToPersist.map((acc) => {
|
|
749
|
+
const delta = this.#statsDeltas.get(acc.id);
|
|
750
|
+
let mergedStats = acc.stats;
|
|
751
|
+
const diskAcc = findDiskAccount(acc);
|
|
752
|
+
|
|
753
|
+
if (diskAcc) {
|
|
754
|
+
matchedDiskAccounts.add(diskAcc);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (delta) {
|
|
758
|
+
const diskStats = diskAcc?.stats;
|
|
759
|
+
|
|
760
|
+
if (delta.isReset) {
|
|
761
|
+
mergedStats = {
|
|
762
|
+
requests: delta.requests,
|
|
763
|
+
inputTokens: delta.inputTokens,
|
|
764
|
+
outputTokens: delta.outputTokens,
|
|
765
|
+
cacheReadTokens: delta.cacheReadTokens,
|
|
766
|
+
cacheWriteTokens: delta.cacheWriteTokens,
|
|
767
|
+
lastReset: delta.resetTimestamp ?? acc.stats.lastReset,
|
|
768
|
+
};
|
|
769
|
+
} else if (diskStats) {
|
|
770
|
+
mergedStats = {
|
|
771
|
+
requests: diskStats.requests + delta.requests,
|
|
772
|
+
inputTokens: diskStats.inputTokens + delta.inputTokens,
|
|
773
|
+
outputTokens: diskStats.outputTokens + delta.outputTokens,
|
|
774
|
+
cacheReadTokens: diskStats.cacheReadTokens + delta.cacheReadTokens,
|
|
775
|
+
cacheWriteTokens: diskStats.cacheWriteTokens + delta.cacheWriteTokens,
|
|
776
|
+
lastReset: diskStats.lastReset,
|
|
777
|
+
};
|
|
550
778
|
}
|
|
779
|
+
}
|
|
551
780
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
acc.refreshToken = freshestAuth.refreshToken;
|
|
570
|
-
acc.access = freshestAuth.access;
|
|
571
|
-
acc.expires = freshestAuth.expires;
|
|
572
|
-
acc.tokenUpdatedAt = freshestAuth.tokenUpdatedAt;
|
|
781
|
+
const memTokenUpdatedAt = acc.tokenUpdatedAt || 0;
|
|
782
|
+
const diskTokenUpdatedAt = diskAcc?.token_updated_at || 0;
|
|
783
|
+
const freshestAuth =
|
|
784
|
+
diskAcc && diskTokenUpdatedAt > memTokenUpdatedAt
|
|
785
|
+
? {
|
|
786
|
+
refreshToken: diskAcc.refreshToken,
|
|
787
|
+
access: diskAcc.access,
|
|
788
|
+
expires: diskAcc.expires,
|
|
789
|
+
tokenUpdatedAt: diskTokenUpdatedAt,
|
|
790
|
+
}
|
|
791
|
+
: {
|
|
792
|
+
refreshToken: acc.refreshToken,
|
|
793
|
+
access: acc.access,
|
|
794
|
+
expires: acc.expires,
|
|
795
|
+
tokenUpdatedAt: memTokenUpdatedAt,
|
|
796
|
+
};
|
|
573
797
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
798
|
+
acc.refreshToken = freshestAuth.refreshToken;
|
|
799
|
+
acc.access = freshestAuth.access;
|
|
800
|
+
acc.expires = freshestAuth.expires;
|
|
801
|
+
acc.tokenUpdatedAt = freshestAuth.tokenUpdatedAt;
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
id: acc.id,
|
|
805
|
+
email: acc.email,
|
|
806
|
+
identity: acc.identity,
|
|
807
|
+
label: acc.label,
|
|
808
|
+
refreshToken: freshestAuth.refreshToken,
|
|
809
|
+
access: freshestAuth.access,
|
|
810
|
+
expires: freshestAuth.expires,
|
|
811
|
+
token_updated_at: freshestAuth.tokenUpdatedAt,
|
|
812
|
+
addedAt: acc.addedAt,
|
|
813
|
+
lastUsed: acc.lastUsed,
|
|
814
|
+
enabled: acc.enabled,
|
|
815
|
+
rateLimitResetTimes: Object.keys(acc.rateLimitResetTimes).length > 0 ? acc.rateLimitResetTimes : {},
|
|
816
|
+
consecutiveFailures: acc.consecutiveFailures,
|
|
817
|
+
lastFailureTime: acc.lastFailureTime,
|
|
818
|
+
lastSwitchReason: acc.lastSwitchReason,
|
|
819
|
+
stats: mergedStats,
|
|
820
|
+
source: acc.source,
|
|
821
|
+
};
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const diskOnlyAccounts = diskAccounts.filter((account) => !matchedDiskAccounts.has(account));
|
|
825
|
+
const allAccounts = accountsToPersist.length > 0 ? [...persistedAccounts, ...diskOnlyAccounts] : persistedAccounts;
|
|
826
|
+
const resolvedActiveIndex = activeAccountId
|
|
827
|
+
? allAccounts.findIndex((account) => account.id === activeAccountId)
|
|
828
|
+
: -1;
|
|
829
|
+
|
|
830
|
+
const storage: AccountStorage = {
|
|
831
|
+
version: 1,
|
|
832
|
+
accounts: allAccounts,
|
|
833
|
+
activeIndex:
|
|
834
|
+
resolvedActiveIndex >= 0
|
|
835
|
+
? resolvedActiveIndex
|
|
836
|
+
: allAccounts.length > 0
|
|
837
|
+
? Math.max(0, Math.min(this.#currentIndex, allAccounts.length - 1))
|
|
838
|
+
: 0,
|
|
593
839
|
};
|
|
594
840
|
|
|
595
841
|
await saveAccounts(storage);
|
|
@@ -611,68 +857,108 @@ export class AccountManager {
|
|
|
611
857
|
const stored = await loadAccounts();
|
|
612
858
|
if (!stored) return;
|
|
613
859
|
|
|
614
|
-
const
|
|
615
|
-
const
|
|
860
|
+
const matchedAccounts = new Set<ManagedAccount>();
|
|
861
|
+
const reconciledAccounts: ManagedAccount[] = [];
|
|
862
|
+
let structuralChange = false;
|
|
616
863
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
864
|
+
for (const [index, storedAccount] of stored.accounts.entries()) {
|
|
865
|
+
const existing = findMatchingAccount(this.#accounts, {
|
|
866
|
+
id: storedAccount.id,
|
|
867
|
+
identity: resolveIdentity(storedAccount),
|
|
868
|
+
refreshToken: storedAccount.refreshToken,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
if (existing) {
|
|
872
|
+
updateManagedAccountFromStorage(existing, storedAccount, index);
|
|
873
|
+
matchedAccounts.add(existing);
|
|
874
|
+
reconciledAccounts.push(existing);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const addedAccount = createManagedAccount({
|
|
879
|
+
id: storedAccount.id,
|
|
880
|
+
index,
|
|
881
|
+
email: storedAccount.email,
|
|
882
|
+
identity: storedAccount.identity,
|
|
883
|
+
label: storedAccount.label,
|
|
884
|
+
refreshToken: storedAccount.refreshToken,
|
|
885
|
+
access: storedAccount.access,
|
|
886
|
+
expires: storedAccount.expires,
|
|
887
|
+
tokenUpdatedAt: storedAccount.token_updated_at,
|
|
888
|
+
addedAt: storedAccount.addedAt,
|
|
889
|
+
lastUsed: storedAccount.lastUsed,
|
|
890
|
+
enabled: storedAccount.enabled,
|
|
891
|
+
rateLimitResetTimes: storedAccount.rateLimitResetTimes,
|
|
892
|
+
consecutiveFailures: storedAccount.consecutiveFailures,
|
|
893
|
+
lastFailureTime: storedAccount.lastFailureTime,
|
|
894
|
+
lastSwitchReason: storedAccount.lastSwitchReason,
|
|
895
|
+
stats: storedAccount.stats,
|
|
896
|
+
source: storedAccount.source || "oauth",
|
|
648
897
|
});
|
|
898
|
+
matchedAccounts.add(addedAccount);
|
|
899
|
+
reconciledAccounts.push(addedAccount);
|
|
900
|
+
structuralChange = true;
|
|
901
|
+
}
|
|
649
902
|
|
|
650
|
-
|
|
651
|
-
|
|
903
|
+
for (const account of this.#accounts) {
|
|
904
|
+
if (matchedAccounts.has(account)) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
652
907
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
908
|
+
if (account.enabled) {
|
|
909
|
+
account.enabled = false;
|
|
910
|
+
structuralChange = true;
|
|
656
911
|
}
|
|
657
912
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
913
|
+
reconciledAccounts.push(account);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const orderChanged =
|
|
917
|
+
reconciledAccounts.length !== this.#accounts.length ||
|
|
918
|
+
reconciledAccounts.some((account, index) => this.#accounts[index] !== account);
|
|
919
|
+
|
|
920
|
+
this.#accounts = reconciledAccounts;
|
|
921
|
+
reindexAccounts(this.#accounts);
|
|
922
|
+
|
|
923
|
+
if (orderChanged || structuralChange) {
|
|
924
|
+
this.#rebuildTrackers();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const currentIds = new Set(this.#accounts.map((account) => account.id));
|
|
928
|
+
for (const id of this.#statsDeltas.keys()) {
|
|
929
|
+
if (!currentIds.has(id)) {
|
|
930
|
+
this.#statsDeltas.delete(id);
|
|
662
931
|
}
|
|
663
932
|
}
|
|
664
933
|
|
|
665
|
-
const
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
934
|
+
const enabledAccounts = this.#accounts.filter((account) => account.enabled);
|
|
935
|
+
if (enabledAccounts.length === 0) {
|
|
936
|
+
this.#currentIndex = -1;
|
|
937
|
+
this.#cursor = 0;
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
669
940
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
941
|
+
const diskIndex = Math.min(stored.activeIndex, stored.accounts.length - 1);
|
|
942
|
+
const diskAccount = diskIndex >= 0 ? stored.accounts[diskIndex] : undefined;
|
|
943
|
+
if (!diskAccount || !diskAccount.enabled) {
|
|
944
|
+
if (!this.#accounts[this.#currentIndex]?.enabled) {
|
|
945
|
+
const fallback = enabledAccounts[0]!;
|
|
946
|
+
this.#currentIndex = fallback.index;
|
|
947
|
+
this.#cursor = fallback.index;
|
|
675
948
|
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const activeAccount = findMatchingAccount(this.#accounts, {
|
|
953
|
+
id: diskAccount.id,
|
|
954
|
+
identity: resolveIdentity(diskAccount),
|
|
955
|
+
refreshToken: diskAccount.refreshToken,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
if (activeAccount && activeAccount.enabled && activeAccount.index !== this.#currentIndex) {
|
|
959
|
+
this.#currentIndex = activeAccount.index;
|
|
960
|
+
this.#cursor = activeAccount.index;
|
|
961
|
+
this.#healthTracker.reset(activeAccount.index);
|
|
676
962
|
}
|
|
677
963
|
}
|
|
678
964
|
|
|
@@ -710,6 +996,14 @@ export class AccountManager {
|
|
|
710
996
|
delta.cacheReadTokens += crTok;
|
|
711
997
|
delta.cacheWriteTokens += cwTok;
|
|
712
998
|
} else {
|
|
999
|
+
if (this.#statsDeltas.size >= this.#MAX_STATS_DELTAS) {
|
|
1000
|
+
this.saveToDisk().catch((err) => {
|
|
1001
|
+
if (this.#config.debug) {
|
|
1002
|
+
// eslint-disable-next-line no-console -- debug-gated stderr logging; plugin has no dedicated logger
|
|
1003
|
+
console.error("[opencode-anthropic-auth] forced statsDeltas flush failed:", (err as Error).message);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
713
1007
|
this.#statsDeltas.set(account.id, {
|
|
714
1008
|
requests: 1,
|
|
715
1009
|
inputTokens: inTok,
|