opencode-anthropic-multi-account 0.2.7 → 0.2.10
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/dist/index.d.ts +5 -2
- package/dist/index.js +451 -2294
- package/dist/index.js.map +1 -0
- package/package.json +3 -3
- package/dist/account-manager.d.ts +0 -2
- package/dist/account-store.d.ts +0 -2
- package/dist/anthropic-prompt.d.ts +0 -1
- package/dist/auth-handler.d.ts +0 -20
- package/dist/claims.d.ts +0 -1
- package/dist/config.d.ts +0 -2
- package/dist/constants.d.ts +0 -23
- package/dist/executor.d.ts +0 -8
- package/dist/pi-ai-adapter.d.ts +0 -18
- package/dist/pool-chain-executor.d.ts +0 -11
- package/dist/proactive-refresh.d.ts +0 -2
- package/dist/rate-limit.d.ts +0 -2
- package/dist/request-transform.d.ts +0 -6
- package/dist/runtime-factory.d.ts +0 -20
- package/dist/storage.d.ts +0 -1
- package/dist/token-node-request.d.ts +0 -10
- package/dist/token.d.ts +0 -4
- package/dist/types.d.ts +0 -214
- package/dist/ui/ansi.d.ts +0 -1
- package/dist/ui/auth-menu.d.ts +0 -26
- package/dist/ui/confirm.d.ts +0 -1
- package/dist/ui/select.d.ts +0 -1
- package/dist/usage.d.ts +0 -23
- package/dist/utils.d.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -1,2188 +1,17 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import {
|
|
4
|
+
CascadeStateManager,
|
|
5
|
+
loadPoolChainConfig,
|
|
6
|
+
migrateFromAuthJson,
|
|
7
|
+
PoolManager
|
|
8
|
+
} from "opencode-multi-account-core";
|
|
3
9
|
|
|
4
|
-
//
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// ../multi-account-core/src/claims.ts
|
|
8
|
-
import { promises as fs2 } from "node:fs";
|
|
9
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
10
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
11
|
-
|
|
12
|
-
// ../multi-account-core/src/utils.ts
|
|
13
|
-
import { join as join2 } from "node:path";
|
|
14
|
-
import { homedir as homedir2 } from "node:os";
|
|
15
|
-
|
|
16
|
-
// ../multi-account-core/src/config.ts
|
|
17
|
-
import { promises as fs } from "node:fs";
|
|
18
|
-
import { randomBytes } from "node:crypto";
|
|
19
|
-
import { dirname, join } from "node:path";
|
|
20
|
-
import { homedir } from "node:os";
|
|
21
|
-
import * as v2 from "valibot";
|
|
22
|
-
|
|
23
|
-
// ../multi-account-core/src/types.ts
|
|
24
|
-
import * as v from "valibot";
|
|
25
|
-
var OAuthCredentialsSchema = v.object({
|
|
26
|
-
type: v.literal("oauth"),
|
|
27
|
-
refresh: v.string(),
|
|
28
|
-
access: v.string(),
|
|
29
|
-
expires: v.number()
|
|
30
|
-
});
|
|
31
|
-
var UsageLimitEntrySchema = v.object({
|
|
32
|
-
utilization: v.number(),
|
|
33
|
-
resets_at: v.nullable(v.string())
|
|
34
|
-
});
|
|
35
|
-
var UsageLimitsSchema = v.object({
|
|
36
|
-
five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
37
|
-
seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
38
|
-
seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
|
|
39
|
-
});
|
|
40
|
-
var CredentialRefreshPatchSchema = v.object({
|
|
41
|
-
accessToken: v.string(),
|
|
42
|
-
expiresAt: v.number(),
|
|
43
|
-
refreshToken: v.optional(v.string()),
|
|
44
|
-
uuid: v.optional(v.string()),
|
|
45
|
-
accountId: v.optional(v.string()),
|
|
46
|
-
email: v.optional(v.string())
|
|
47
|
-
});
|
|
48
|
-
var StoredAccountSchema = v.object({
|
|
49
|
-
uuid: v.optional(v.string()),
|
|
50
|
-
accountId: v.optional(v.string()),
|
|
51
|
-
label: v.optional(v.string()),
|
|
52
|
-
email: v.optional(v.string()),
|
|
53
|
-
planTier: v.optional(v.string(), ""),
|
|
54
|
-
refreshToken: v.string(),
|
|
55
|
-
accessToken: v.optional(v.string()),
|
|
56
|
-
expiresAt: v.optional(v.number()),
|
|
57
|
-
addedAt: v.number(),
|
|
58
|
-
lastUsed: v.number(),
|
|
59
|
-
enabled: v.optional(v.boolean(), true),
|
|
60
|
-
rateLimitResetAt: v.optional(v.number()),
|
|
61
|
-
cachedUsage: v.optional(UsageLimitsSchema),
|
|
62
|
-
cachedUsageAt: v.optional(v.number()),
|
|
63
|
-
consecutiveAuthFailures: v.optional(v.number(), 0),
|
|
64
|
-
isAuthDisabled: v.optional(v.boolean(), false),
|
|
65
|
-
authDisabledReason: v.optional(v.string())
|
|
66
|
-
});
|
|
67
|
-
var AccountStorageSchema = v.object({
|
|
68
|
-
version: v.literal(1),
|
|
69
|
-
accounts: v.optional(v.array(StoredAccountSchema), []),
|
|
70
|
-
activeAccountUuid: v.optional(v.string())
|
|
71
|
-
});
|
|
72
|
-
var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
|
|
73
|
-
var PluginConfigSchema = v.object({
|
|
74
|
-
account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
|
|
75
|
-
cross_process_claims: v.optional(v.boolean(), true),
|
|
76
|
-
soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
|
|
77
|
-
rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
78
|
-
default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
|
|
79
|
-
max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
|
|
80
|
-
token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
81
|
-
proactive_refresh: v.optional(v.boolean(), true),
|
|
82
|
-
proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
|
|
83
|
-
proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
|
|
84
|
-
quiet_mode: v.optional(v.boolean(), false),
|
|
85
|
-
debug: v.optional(v.boolean(), false)
|
|
86
|
-
});
|
|
87
|
-
var TokenRefreshError = class _TokenRefreshError extends Error {
|
|
88
|
-
status;
|
|
89
|
-
permanent;
|
|
90
|
-
constructor(permanent, status) {
|
|
91
|
-
super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
|
|
92
|
-
this.name = "TokenRefreshError";
|
|
93
|
-
this.status = status;
|
|
94
|
-
this.permanent = permanent;
|
|
95
|
-
Object.setPrototypeOf(this, _TokenRefreshError.prototype);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
function isTokenRefreshError(error) {
|
|
99
|
-
if (error instanceof TokenRefreshError) return true;
|
|
100
|
-
if (!(error instanceof Error)) return false;
|
|
101
|
-
const candidate = error;
|
|
102
|
-
return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ../multi-account-core/src/config.ts
|
|
106
|
-
var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
|
|
107
|
-
var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
|
|
108
|
-
var configFilename = DEFAULT_CONFIG_FILENAME;
|
|
109
|
-
var cachedConfig = null;
|
|
110
|
-
var externalConfigGetter = null;
|
|
111
|
-
function getConfigDir() {
|
|
112
|
-
return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
113
|
-
}
|
|
114
|
-
function getConfigPath() {
|
|
115
|
-
return join(getConfigDir(), configFilename);
|
|
116
|
-
}
|
|
117
|
-
function parseConfig(raw) {
|
|
118
|
-
const result = v2.safeParse(PluginConfigSchema, raw);
|
|
119
|
-
return result.success ? result.output : DEFAULT_CONFIG;
|
|
120
|
-
}
|
|
121
|
-
function initCoreConfig(filename) {
|
|
122
|
-
configFilename = filename || DEFAULT_CONFIG_FILENAME;
|
|
123
|
-
cachedConfig = null;
|
|
124
|
-
}
|
|
125
|
-
async function loadConfig() {
|
|
126
|
-
if (cachedConfig) return cachedConfig;
|
|
127
|
-
const path = getConfigPath();
|
|
128
|
-
try {
|
|
129
|
-
const content = await fs.readFile(path, "utf-8");
|
|
130
|
-
cachedConfig = parseConfig(JSON.parse(content));
|
|
131
|
-
} catch {
|
|
132
|
-
cachedConfig = DEFAULT_CONFIG;
|
|
133
|
-
}
|
|
134
|
-
return cachedConfig;
|
|
135
|
-
}
|
|
136
|
-
function getConfig() {
|
|
137
|
-
if (cachedConfig) return cachedConfig;
|
|
138
|
-
if (externalConfigGetter && externalConfigGetter !== getConfig) {
|
|
139
|
-
try {
|
|
140
|
-
return parseConfig(externalConfigGetter());
|
|
141
|
-
} catch {
|
|
142
|
-
return DEFAULT_CONFIG;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return DEFAULT_CONFIG;
|
|
146
|
-
}
|
|
147
|
-
function setConfigGetter(getter) {
|
|
148
|
-
if (getter === getConfig) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
externalConfigGetter = getter;
|
|
152
|
-
}
|
|
153
|
-
async function updateConfigField(key, value) {
|
|
154
|
-
const path = getConfigPath();
|
|
155
|
-
let existing = {};
|
|
156
|
-
try {
|
|
157
|
-
const content2 = await fs.readFile(path, "utf-8");
|
|
158
|
-
existing = JSON.parse(content2);
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
existing[key] = value;
|
|
162
|
-
await fs.mkdir(dirname(path), { recursive: true });
|
|
163
|
-
const content = `${JSON.stringify(existing, null, 2)}
|
|
164
|
-
`;
|
|
165
|
-
const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
166
|
-
try {
|
|
167
|
-
await fs.writeFile(tempPath, content, "utf-8");
|
|
168
|
-
await fs.rename(tempPath, path);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
try {
|
|
171
|
-
await fs.unlink(tempPath);
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
cachedConfig = null;
|
|
177
|
-
await loadConfig();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ../multi-account-core/src/utils.ts
|
|
181
|
-
function getConfigDir2() {
|
|
182
|
-
return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
|
|
183
|
-
}
|
|
184
|
-
function getErrorCode(error) {
|
|
185
|
-
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
186
|
-
return void 0;
|
|
187
|
-
}
|
|
188
|
-
const code = error.code;
|
|
189
|
-
return typeof code === "string" ? code : void 0;
|
|
190
|
-
}
|
|
191
|
-
function formatWaitTime(ms) {
|
|
192
|
-
const totalSeconds = Math.ceil(ms / 1e3);
|
|
193
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
194
|
-
const days = Math.floor(totalSeconds / 86400);
|
|
195
|
-
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
196
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
197
|
-
const seconds = totalSeconds % 60;
|
|
198
|
-
const parts = [];
|
|
199
|
-
if (days > 0) parts.push(`${days}d`);
|
|
200
|
-
if (hours > 0) parts.push(`${hours}h`);
|
|
201
|
-
if (minutes > 0) parts.push(`${minutes}m`);
|
|
202
|
-
if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
|
|
203
|
-
return parts.join(" ") || "0s";
|
|
204
|
-
}
|
|
205
|
-
function getAccountLabel(account) {
|
|
206
|
-
if (account.label) return account.label;
|
|
207
|
-
if (account.email) return account.email;
|
|
208
|
-
if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
|
|
209
|
-
return `Account ${account.index + 1}`;
|
|
210
|
-
}
|
|
211
|
-
function sleep(ms) {
|
|
212
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
213
|
-
}
|
|
214
|
-
async function showToast(client, message, variant) {
|
|
215
|
-
if (getConfig().quiet_mode) return;
|
|
216
|
-
try {
|
|
217
|
-
await client.tui.showToast({ body: { message, variant } });
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function debugLog(client, message, extra) {
|
|
222
|
-
if (!getConfig().debug) return;
|
|
223
|
-
client.app.log({
|
|
224
|
-
body: { service: "claude-multiauth", level: "debug", message, extra }
|
|
225
|
-
}).catch(() => {
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
function createMinimalClient() {
|
|
229
|
-
return {
|
|
230
|
-
auth: {
|
|
231
|
-
set: async () => {
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
tui: {
|
|
235
|
-
showToast: async () => {
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
app: {
|
|
239
|
-
log: async () => {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function getClearedOAuthBody() {
|
|
245
|
-
return {
|
|
246
|
-
type: "oauth",
|
|
247
|
-
refresh: "",
|
|
248
|
-
access: "",
|
|
249
|
-
expires: 0
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ../multi-account-core/src/claims.ts
|
|
254
|
-
var CLAIMS_FILENAME = "multiauth-claims.json";
|
|
255
|
-
var CLAIM_EXPIRY_MS = 6e4;
|
|
256
|
-
function getClaimsPath() {
|
|
257
|
-
return join3(getConfigDir2(), CLAIMS_FILENAME);
|
|
258
|
-
}
|
|
259
|
-
function isClaimShape(value) {
|
|
260
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
261
|
-
const claim = value;
|
|
262
|
-
return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
|
|
263
|
-
}
|
|
264
|
-
function parseClaims(raw) {
|
|
265
|
-
let parsed;
|
|
266
|
-
try {
|
|
267
|
-
parsed = JSON.parse(raw);
|
|
268
|
-
} catch {
|
|
269
|
-
return {};
|
|
270
|
-
}
|
|
271
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
272
|
-
return {};
|
|
273
|
-
}
|
|
274
|
-
const claims = {};
|
|
275
|
-
for (const [accountId, claim] of Object.entries(parsed)) {
|
|
276
|
-
if (isClaimShape(claim)) {
|
|
277
|
-
claims[accountId] = claim;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return claims;
|
|
281
|
-
}
|
|
282
|
-
function isProcessAlive(pid) {
|
|
283
|
-
try {
|
|
284
|
-
process.kill(pid, 0);
|
|
285
|
-
return true;
|
|
286
|
-
} catch {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function cleanClaims(claims, now) {
|
|
291
|
-
const cleaned = {};
|
|
292
|
-
let changed = false;
|
|
293
|
-
for (const [accountId, claim] of Object.entries(claims)) {
|
|
294
|
-
const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
|
|
295
|
-
const zombieClaim = !isProcessAlive(claim.pid);
|
|
296
|
-
if (expiredByTime || zombieClaim) {
|
|
297
|
-
changed = true;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
cleaned[accountId] = claim;
|
|
301
|
-
}
|
|
302
|
-
return { cleaned, changed };
|
|
303
|
-
}
|
|
304
|
-
async function writeClaimsFile(claims) {
|
|
305
|
-
const path = getClaimsPath();
|
|
306
|
-
const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
307
|
-
await fs2.mkdir(dirname2(path), { recursive: true });
|
|
308
|
-
try {
|
|
309
|
-
await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
|
|
310
|
-
await fs2.rename(tempPath, path);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
try {
|
|
313
|
-
await fs2.unlink(tempPath);
|
|
314
|
-
} catch {
|
|
315
|
-
}
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async function readClaims() {
|
|
320
|
-
try {
|
|
321
|
-
const data = await fs2.readFile(getClaimsPath(), "utf-8");
|
|
322
|
-
const parsed = parseClaims(data);
|
|
323
|
-
const now = Date.now();
|
|
324
|
-
const { cleaned, changed } = cleanClaims(parsed, now);
|
|
325
|
-
if (changed) {
|
|
326
|
-
try {
|
|
327
|
-
await writeClaimsFile(cleaned);
|
|
328
|
-
} catch {
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return cleaned;
|
|
332
|
-
} catch {
|
|
333
|
-
return {};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function writeClaim(accountId) {
|
|
337
|
-
const now = Date.now();
|
|
338
|
-
const claims = await readClaims();
|
|
339
|
-
const { cleaned } = cleanClaims(claims, now);
|
|
340
|
-
cleaned[accountId] = { pid: process.pid, at: now };
|
|
341
|
-
try {
|
|
342
|
-
await writeClaimsFile(cleaned);
|
|
343
|
-
} catch {
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function isClaimedByOther(claims, accountId) {
|
|
347
|
-
if (!accountId) return false;
|
|
348
|
-
const claim = claims[accountId];
|
|
349
|
-
if (!claim) return false;
|
|
350
|
-
if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
|
|
351
|
-
if (!isProcessAlive(claim.pid)) return false;
|
|
352
|
-
return claim.pid !== process.pid;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ../multi-account-core/src/account-manager.ts
|
|
356
|
-
var STARTUP_REFRESH_CONCURRENCY = 3;
|
|
357
|
-
var RECENT_429_COOLDOWN_MS = 3e4;
|
|
358
|
-
var HYBRID_SWITCH_MARGIN = 40;
|
|
359
|
-
function createAccountManagerForProvider(dependencies) {
|
|
360
|
-
const {
|
|
361
|
-
providerAuthId,
|
|
362
|
-
isTokenExpired: isTokenExpired2,
|
|
363
|
-
refreshToken: refreshToken2
|
|
364
|
-
} = dependencies;
|
|
365
|
-
return class AccountManager2 {
|
|
366
|
-
constructor(store) {
|
|
367
|
-
this.store = store;
|
|
368
|
-
}
|
|
369
|
-
cached = [];
|
|
370
|
-
activeAccountUuid;
|
|
371
|
-
client = null;
|
|
372
|
-
runtimeFactory = null;
|
|
373
|
-
roundRobinCursor = 0;
|
|
374
|
-
last429Map = /* @__PURE__ */ new Map();
|
|
375
|
-
static async create(store, currentAuth, client) {
|
|
376
|
-
const manager = new AccountManager2(store);
|
|
377
|
-
await manager.initialize(currentAuth, client);
|
|
378
|
-
return manager;
|
|
379
|
-
}
|
|
380
|
-
async initialize(currentAuth, client) {
|
|
381
|
-
if (client) this.client = client;
|
|
382
|
-
const storage = await this.store.load();
|
|
383
|
-
if (storage.accounts.length > 0) {
|
|
384
|
-
this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
|
|
385
|
-
this.activeAccountUuid = storage.activeAccountUuid;
|
|
386
|
-
if (!this.getActiveAccount() && this.cached.length > 0) {
|
|
387
|
-
this.activeAccountUuid = this.cached[0].uuid;
|
|
388
|
-
}
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (currentAuth.refresh) {
|
|
392
|
-
const newAccount = this.createNewAccount(currentAuth, Date.now());
|
|
393
|
-
await this.store.addAccount(newAccount);
|
|
394
|
-
await this.store.setActiveUuid(newAccount.uuid);
|
|
395
|
-
this.cached = [this.toManagedAccount(newAccount, 0)];
|
|
396
|
-
this.activeAccountUuid = newAccount.uuid;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
async refresh() {
|
|
400
|
-
const storage = await this.store.load();
|
|
401
|
-
this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
|
|
402
|
-
if (storage.activeAccountUuid) {
|
|
403
|
-
this.activeAccountUuid = storage.activeAccountUuid;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
toManagedAccount(storedAccount, index) {
|
|
407
|
-
return {
|
|
408
|
-
index,
|
|
409
|
-
uuid: storedAccount.uuid,
|
|
410
|
-
accountId: storedAccount.accountId,
|
|
411
|
-
label: storedAccount.label,
|
|
412
|
-
email: storedAccount.email,
|
|
413
|
-
planTier: storedAccount.planTier,
|
|
414
|
-
refreshToken: storedAccount.refreshToken,
|
|
415
|
-
accessToken: storedAccount.accessToken,
|
|
416
|
-
expiresAt: storedAccount.expiresAt,
|
|
417
|
-
addedAt: storedAccount.addedAt,
|
|
418
|
-
lastUsed: storedAccount.lastUsed,
|
|
419
|
-
enabled: storedAccount.enabled,
|
|
420
|
-
rateLimitResetAt: storedAccount.rateLimitResetAt,
|
|
421
|
-
cachedUsage: storedAccount.cachedUsage,
|
|
422
|
-
cachedUsageAt: storedAccount.cachedUsageAt,
|
|
423
|
-
consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
|
|
424
|
-
isAuthDisabled: storedAccount.isAuthDisabled,
|
|
425
|
-
authDisabledReason: storedAccount.authDisabledReason,
|
|
426
|
-
last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
createNewAccount(auth, now) {
|
|
430
|
-
return {
|
|
431
|
-
uuid: randomUUID(),
|
|
432
|
-
refreshToken: auth.refresh,
|
|
433
|
-
accessToken: auth.access,
|
|
434
|
-
expiresAt: auth.expires,
|
|
435
|
-
addedAt: now,
|
|
436
|
-
lastUsed: now,
|
|
437
|
-
enabled: true,
|
|
438
|
-
planTier: "",
|
|
439
|
-
consecutiveAuthFailures: 0,
|
|
440
|
-
isAuthDisabled: false
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
getAccountCount() {
|
|
444
|
-
return this.getEligibleAccounts().length;
|
|
445
|
-
}
|
|
446
|
-
getAccounts() {
|
|
447
|
-
return [...this.cached];
|
|
448
|
-
}
|
|
449
|
-
getActiveAccount() {
|
|
450
|
-
if (this.activeAccountUuid) {
|
|
451
|
-
return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
|
|
452
|
-
}
|
|
453
|
-
return this.cached[0] ?? null;
|
|
454
|
-
}
|
|
455
|
-
setClient(client) {
|
|
456
|
-
this.client = client;
|
|
457
|
-
}
|
|
458
|
-
setRuntimeFactory(factory) {
|
|
459
|
-
this.runtimeFactory = factory;
|
|
460
|
-
}
|
|
461
|
-
getEligibleAccounts() {
|
|
462
|
-
return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
|
|
463
|
-
}
|
|
464
|
-
exceedsSoftQuota(account) {
|
|
465
|
-
const threshold = getConfig().soft_quota_threshold_percent;
|
|
466
|
-
if (threshold >= 100) return false;
|
|
467
|
-
const usage = account.cachedUsage;
|
|
468
|
-
if (!usage) return false;
|
|
469
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
470
|
-
return tiers.some((tier) => tier != null && tier.utilization >= threshold);
|
|
471
|
-
}
|
|
472
|
-
hasAnyUsableAccount() {
|
|
473
|
-
return this.getEligibleAccounts().length > 0;
|
|
474
|
-
}
|
|
475
|
-
isRateLimited(account) {
|
|
476
|
-
if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
return this.isUsageExhausted(account);
|
|
480
|
-
}
|
|
481
|
-
isUsageExhausted(account) {
|
|
482
|
-
const usage = account.cachedUsage;
|
|
483
|
-
if (!usage) return false;
|
|
484
|
-
const now = Date.now();
|
|
485
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
486
|
-
return tiers.some(
|
|
487
|
-
(tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
clearExpiredRateLimits() {
|
|
491
|
-
const now = Date.now();
|
|
492
|
-
for (const account of this.cached) {
|
|
493
|
-
if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
|
|
494
|
-
account.rateLimitResetAt = void 0;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
getMinWaitTime() {
|
|
499
|
-
const eligible = this.getEligibleAccounts();
|
|
500
|
-
const available = eligible.filter((account) => !this.isRateLimited(account));
|
|
501
|
-
if (available.length > 0) return 0;
|
|
502
|
-
const now = Date.now();
|
|
503
|
-
const waits = [];
|
|
504
|
-
for (const account of eligible) {
|
|
505
|
-
if (account.rateLimitResetAt) {
|
|
506
|
-
const ms = account.rateLimitResetAt - now;
|
|
507
|
-
if (ms > 0) waits.push(ms);
|
|
508
|
-
}
|
|
509
|
-
const usageResetMs = this.getUsageResetMs(account);
|
|
510
|
-
if (usageResetMs !== null && usageResetMs > 0) {
|
|
511
|
-
waits.push(usageResetMs);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return waits.length > 0 ? Math.min(...waits) : 0;
|
|
515
|
-
}
|
|
516
|
-
getUsageResetMs(account) {
|
|
517
|
-
const usage = account.cachedUsage;
|
|
518
|
-
if (!usage) return null;
|
|
519
|
-
const now = Date.now();
|
|
520
|
-
const candidates = [];
|
|
521
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
522
|
-
for (const tier of tiers) {
|
|
523
|
-
if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
|
|
524
|
-
const ms = Date.parse(tier.resets_at) - now;
|
|
525
|
-
if (ms > 0) candidates.push(ms);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
529
|
-
}
|
|
530
|
-
async selectAccount() {
|
|
531
|
-
await this.refresh();
|
|
532
|
-
this.clearExpiredRateLimits();
|
|
533
|
-
const eligible = this.getEligibleAccounts();
|
|
534
|
-
if (eligible.length === 0) return null;
|
|
535
|
-
const config = getConfig();
|
|
536
|
-
const claims = config.cross_process_claims ? await readClaims() : {};
|
|
537
|
-
const strategy = config.account_selection_strategy;
|
|
538
|
-
let selected;
|
|
539
|
-
switch (strategy) {
|
|
540
|
-
case "round-robin":
|
|
541
|
-
selected = this.selectRoundRobin(eligible, claims);
|
|
542
|
-
break;
|
|
543
|
-
case "hybrid":
|
|
544
|
-
selected = this.selectHybrid(eligible, claims);
|
|
545
|
-
break;
|
|
546
|
-
case "sticky":
|
|
547
|
-
default:
|
|
548
|
-
selected = this.selectSticky(eligible, claims);
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
if (selected?.uuid) {
|
|
552
|
-
this.activeAccountUuid = selected.uuid;
|
|
553
|
-
this.store.setActiveUuid(selected.uuid).catch(() => {
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
if (config.cross_process_claims && selected?.uuid) {
|
|
557
|
-
writeClaim(selected.uuid).catch(() => {
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
return selected;
|
|
561
|
-
}
|
|
562
|
-
isUsable(account) {
|
|
563
|
-
return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
|
|
564
|
-
}
|
|
565
|
-
isInRecentCooldown(account) {
|
|
566
|
-
if (!account.last429At) return false;
|
|
567
|
-
return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
|
|
568
|
-
}
|
|
569
|
-
fallbackNotRateLimited(eligible) {
|
|
570
|
-
const account = eligible.find((candidate) => !this.isRateLimited(candidate));
|
|
571
|
-
if (account) {
|
|
572
|
-
this.activateAccount(account);
|
|
573
|
-
return account;
|
|
574
|
-
}
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
selectSticky(eligible, claims) {
|
|
578
|
-
const current = this.getActiveAccount();
|
|
579
|
-
if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
|
|
580
|
-
this.activateAccount(current);
|
|
581
|
-
return current;
|
|
582
|
-
}
|
|
583
|
-
const unclaimed = eligible.find(
|
|
584
|
-
(account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
|
|
585
|
-
);
|
|
586
|
-
if (unclaimed) {
|
|
587
|
-
this.activateAccount(unclaimed);
|
|
588
|
-
return unclaimed;
|
|
589
|
-
}
|
|
590
|
-
const available = eligible.find((account) => this.isUsable(account));
|
|
591
|
-
if (available) {
|
|
592
|
-
this.activateAccount(available);
|
|
593
|
-
return available;
|
|
594
|
-
}
|
|
595
|
-
return this.fallbackNotRateLimited(eligible);
|
|
596
|
-
}
|
|
597
|
-
selectRoundRobin(eligible, claims) {
|
|
598
|
-
for (let i = 0; i < eligible.length; i++) {
|
|
599
|
-
const index = (this.roundRobinCursor + i) % eligible.length;
|
|
600
|
-
const account = eligible[index];
|
|
601
|
-
if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
|
|
602
|
-
this.roundRobinCursor = (index + 1) % eligible.length;
|
|
603
|
-
this.activateAccount(account);
|
|
604
|
-
return account;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
for (let i = 0; i < eligible.length; i++) {
|
|
608
|
-
const index = (this.roundRobinCursor + i) % eligible.length;
|
|
609
|
-
const account = eligible[index];
|
|
610
|
-
if (this.isUsable(account)) {
|
|
611
|
-
this.roundRobinCursor = (index + 1) % eligible.length;
|
|
612
|
-
this.activateAccount(account);
|
|
613
|
-
return account;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return this.fallbackNotRateLimited(eligible);
|
|
617
|
-
}
|
|
618
|
-
selectHybrid(eligible, claims) {
|
|
619
|
-
const usable = eligible.filter((account) => this.isUsable(account));
|
|
620
|
-
const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
|
|
621
|
-
if (pool.length === 0) return null;
|
|
622
|
-
const activeUuid = this.activeAccountUuid;
|
|
623
|
-
let best = pool[0];
|
|
624
|
-
let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
|
|
625
|
-
for (let i = 1; i < pool.length; i++) {
|
|
626
|
-
const account = pool[i];
|
|
627
|
-
const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
|
|
628
|
-
if (score > bestScore) {
|
|
629
|
-
best = account;
|
|
630
|
-
bestScore = score;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
const current = pool.find((account) => account.uuid === activeUuid);
|
|
634
|
-
if (current && current !== best) {
|
|
635
|
-
const currentScore = this.calculateHybridScore(current, true, claims);
|
|
636
|
-
const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
|
|
637
|
-
if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
|
|
638
|
-
this.activateAccount(current);
|
|
639
|
-
return current;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
this.activateAccount(best);
|
|
643
|
-
return best;
|
|
644
|
-
}
|
|
645
|
-
calculateHybridScore(account, isActive, claims) {
|
|
646
|
-
const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
|
|
647
|
-
const usageScore = (100 - maxUtilization) / 100 * 450;
|
|
648
|
-
const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
|
|
649
|
-
const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
|
|
650
|
-
const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
|
|
651
|
-
const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
|
|
652
|
-
const stickinessBonus = isActive ? 120 : 0;
|
|
653
|
-
const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
|
|
654
|
-
return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
|
|
655
|
-
}
|
|
656
|
-
getMaxUtilization(account) {
|
|
657
|
-
const usage = account.cachedUsage;
|
|
658
|
-
if (!usage) return 65;
|
|
659
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
660
|
-
const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
|
|
661
|
-
return utilizations.length > 0 ? Math.max(...utilizations) : 65;
|
|
662
|
-
}
|
|
663
|
-
activateAccount(account) {
|
|
664
|
-
this.activeAccountUuid = account.uuid;
|
|
665
|
-
account.lastUsed = Date.now();
|
|
666
|
-
}
|
|
667
|
-
async markRateLimited(uuid, backoffMs) {
|
|
668
|
-
const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
|
|
669
|
-
this.last429Map.set(uuid, Date.now());
|
|
670
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
671
|
-
account.rateLimitResetAt = Date.now() + effectiveBackoff;
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
async markRevoked(uuid) {
|
|
675
|
-
await this.removeAccountByUuid(uuid);
|
|
676
|
-
}
|
|
677
|
-
async markSuccess(uuid) {
|
|
678
|
-
this.last429Map.delete(uuid);
|
|
679
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
680
|
-
account.rateLimitResetAt = void 0;
|
|
681
|
-
account.consecutiveAuthFailures = 0;
|
|
682
|
-
account.lastUsed = Date.now();
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
syncToOpenCode(account) {
|
|
686
|
-
if (!this.client || !account.accessToken || !account.expiresAt) return;
|
|
687
|
-
this.client.auth.set({
|
|
688
|
-
path: { id: providerAuthId },
|
|
689
|
-
body: {
|
|
690
|
-
type: "oauth",
|
|
691
|
-
refresh: account.refreshToken,
|
|
692
|
-
access: account.accessToken,
|
|
693
|
-
expires: account.expiresAt
|
|
694
|
-
}
|
|
695
|
-
}).catch(() => {
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
699
|
-
if (!this.client) return;
|
|
700
|
-
const storage = await this.store.load();
|
|
701
|
-
if (storage.accounts.length > 0) return;
|
|
702
|
-
await this.client.auth.set({
|
|
703
|
-
path: { id: providerAuthId },
|
|
704
|
-
body: getClearedOAuthBody()
|
|
705
|
-
}).catch(() => {
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
async removeAccountByUuid(uuid) {
|
|
709
|
-
const removed = await this.store.removeAccount(uuid);
|
|
710
|
-
if (!removed) return;
|
|
711
|
-
this.last429Map.delete(uuid);
|
|
712
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
713
|
-
await this.refresh();
|
|
714
|
-
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
715
|
-
}
|
|
716
|
-
async markAuthFailure(uuid, result) {
|
|
717
|
-
if (!result.ok && result.permanent) {
|
|
718
|
-
await this.removeAccountByUuid(uuid);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
await this.store.mutateStorage((storage) => {
|
|
722
|
-
const account = storage.accounts.find((entry) => entry.uuid === uuid);
|
|
723
|
-
if (!account) return;
|
|
724
|
-
account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
|
|
725
|
-
const maxFailures = getConfig().max_consecutive_auth_failures;
|
|
726
|
-
const usableCount = storage.accounts.filter(
|
|
727
|
-
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
|
|
728
|
-
).length;
|
|
729
|
-
if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
730
|
-
account.isAuthDisabled = true;
|
|
731
|
-
account.authDisabledReason = `${maxFailures} consecutive auth failures`;
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
async applyUsageCache(uuid, usage) {
|
|
736
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
737
|
-
const now = Date.now();
|
|
738
|
-
const exhaustedTierResetTimes = [usage.five_hour, usage.seven_day].flatMap((tier) => {
|
|
739
|
-
if (tier == null || tier.utilization < 100 || tier.resets_at == null) {
|
|
740
|
-
return [];
|
|
741
|
-
}
|
|
742
|
-
return [Date.parse(tier.resets_at)];
|
|
743
|
-
}).filter((resetAt) => Number.isFinite(resetAt) && resetAt > now);
|
|
744
|
-
account.cachedUsage = usage;
|
|
745
|
-
account.cachedUsageAt = Date.now();
|
|
746
|
-
account.rateLimitResetAt = exhaustedTierResetTimes.length > 0 ? Math.min(...exhaustedTierResetTimes) : void 0;
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
async applyProfileCache(uuid, profile) {
|
|
750
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
751
|
-
account.email = profile.email ?? account.email;
|
|
752
|
-
account.planTier = profile.planTier;
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
async ensureValidToken(uuid, client) {
|
|
756
|
-
const credentials = await this.store.readCredentials(uuid);
|
|
757
|
-
if (!credentials) return { ok: false, permanent: true };
|
|
758
|
-
if (credentials.accessToken && credentials.expiresAt && !isTokenExpired2(credentials)) {
|
|
759
|
-
return {
|
|
760
|
-
ok: true,
|
|
761
|
-
patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
const result = await refreshToken2(credentials.refreshToken, uuid, client);
|
|
765
|
-
if (!result.ok) return result;
|
|
766
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
767
|
-
account.accessToken = result.patch.accessToken;
|
|
768
|
-
account.expiresAt = result.patch.expiresAt;
|
|
769
|
-
if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
|
|
770
|
-
if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
|
|
771
|
-
if (result.patch.accountId) account.accountId = result.patch.accountId;
|
|
772
|
-
if (result.patch.email) account.email = result.patch.email;
|
|
773
|
-
account.consecutiveAuthFailures = 0;
|
|
774
|
-
account.isAuthDisabled = false;
|
|
775
|
-
account.authDisabledReason = void 0;
|
|
776
|
-
});
|
|
777
|
-
if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
|
|
778
|
-
this.activeAccountUuid = result.patch.uuid;
|
|
779
|
-
this.store.setActiveUuid(result.patch.uuid).catch(() => {
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
|
|
783
|
-
this.syncToOpenCode(updated);
|
|
784
|
-
}
|
|
785
|
-
return result;
|
|
786
|
-
}
|
|
787
|
-
async validateNonActiveTokens(client) {
|
|
788
|
-
await this.refresh();
|
|
789
|
-
const activeUuid = this.activeAccountUuid;
|
|
790
|
-
const eligible = this.cached.filter(
|
|
791
|
-
(account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
|
|
792
|
-
);
|
|
793
|
-
for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
|
|
794
|
-
const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
|
|
795
|
-
await Promise.all(
|
|
796
|
-
batch.map(async (account) => {
|
|
797
|
-
if (!account.uuid || !isTokenExpired2(account)) return;
|
|
798
|
-
const result = await this.ensureValidToken(account.uuid, client);
|
|
799
|
-
if (!result.ok) {
|
|
800
|
-
await this.markAuthFailure(account.uuid, result);
|
|
801
|
-
}
|
|
802
|
-
})
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
async removeAccount(index) {
|
|
807
|
-
const account = this.cached[index];
|
|
808
|
-
if (!account?.uuid) return false;
|
|
809
|
-
const removed = await this.store.removeAccount(account.uuid);
|
|
810
|
-
if (removed) {
|
|
811
|
-
await this.refresh();
|
|
812
|
-
}
|
|
813
|
-
return removed;
|
|
814
|
-
}
|
|
815
|
-
async clearAllAccounts() {
|
|
816
|
-
await this.store.clear();
|
|
817
|
-
this.cached = [];
|
|
818
|
-
this.activeAccountUuid = void 0;
|
|
819
|
-
}
|
|
820
|
-
async addAccount(auth, email) {
|
|
821
|
-
if (!auth.refresh) return;
|
|
822
|
-
const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
|
|
823
|
-
if (existingByToken) return;
|
|
824
|
-
if (email) {
|
|
825
|
-
const existingByEmail = this.cached.find(
|
|
826
|
-
(account) => account.email && account.email === email
|
|
827
|
-
);
|
|
828
|
-
if (existingByEmail?.uuid) {
|
|
829
|
-
await this.replaceAccountCredentials(existingByEmail.uuid, auth);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
const newAccount = this.createNewAccount(auth, Date.now());
|
|
834
|
-
if (email) newAccount.email = email;
|
|
835
|
-
await this.store.addAccount(newAccount);
|
|
836
|
-
this.activeAccountUuid = newAccount.uuid;
|
|
837
|
-
await this.store.setActiveUuid(newAccount.uuid);
|
|
838
|
-
await this.refresh();
|
|
839
|
-
}
|
|
840
|
-
async toggleEnabled(uuid) {
|
|
841
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
842
|
-
account.enabled = !(account.enabled ?? true);
|
|
843
|
-
if (account.enabled) {
|
|
844
|
-
account.isAuthDisabled = false;
|
|
845
|
-
account.authDisabledReason = void 0;
|
|
846
|
-
account.consecutiveAuthFailures = 0;
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
async replaceAccountCredentials(uuid, auth) {
|
|
851
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
852
|
-
account.refreshToken = auth.refresh;
|
|
853
|
-
account.accessToken = auth.access;
|
|
854
|
-
account.expiresAt = auth.expires;
|
|
855
|
-
account.lastUsed = Date.now();
|
|
856
|
-
account.enabled = true;
|
|
857
|
-
account.isAuthDisabled = false;
|
|
858
|
-
account.authDisabledReason = void 0;
|
|
859
|
-
account.consecutiveAuthFailures = 0;
|
|
860
|
-
account.rateLimitResetAt = void 0;
|
|
861
|
-
});
|
|
862
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
863
|
-
if (updated && uuid === this.activeAccountUuid) {
|
|
864
|
-
this.syncToOpenCode(updated);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async retryAuth(uuid, client) {
|
|
868
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
869
|
-
account.consecutiveAuthFailures = 0;
|
|
870
|
-
account.isAuthDisabled = false;
|
|
871
|
-
account.authDisabledReason = void 0;
|
|
872
|
-
});
|
|
873
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
874
|
-
const credentials = await this.store.readCredentials(uuid);
|
|
875
|
-
if (!credentials) return { ok: false, permanent: true };
|
|
876
|
-
const result = await refreshToken2(credentials.refreshToken, uuid, client);
|
|
877
|
-
if (result.ok) {
|
|
878
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
879
|
-
account.accessToken = result.patch.accessToken;
|
|
880
|
-
account.expiresAt = result.patch.expiresAt;
|
|
881
|
-
if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
|
|
882
|
-
if (result.patch.uuid) account.uuid = result.patch.uuid;
|
|
883
|
-
if (result.patch.accountId) account.accountId = result.patch.accountId;
|
|
884
|
-
if (result.patch.email) account.email = result.patch.email;
|
|
885
|
-
account.enabled = true;
|
|
886
|
-
account.consecutiveAuthFailures = 0;
|
|
887
|
-
});
|
|
888
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
889
|
-
if (result.patch.uuid) {
|
|
890
|
-
this.runtimeFactory?.invalidate(result.patch.uuid);
|
|
891
|
-
}
|
|
892
|
-
const nextUuid = result.patch.uuid ?? uuid;
|
|
893
|
-
if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
|
|
894
|
-
this.activeAccountUuid = result.patch.uuid;
|
|
895
|
-
await this.store.setActiveUuid(result.patch.uuid);
|
|
896
|
-
}
|
|
897
|
-
if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
|
|
898
|
-
const freshCredentials = await this.store.readCredentials(nextUuid);
|
|
899
|
-
if (freshCredentials) {
|
|
900
|
-
this.syncToOpenCode({
|
|
901
|
-
refreshToken: freshCredentials.refreshToken,
|
|
902
|
-
accessToken: freshCredentials.accessToken,
|
|
903
|
-
expiresAt: freshCredentials.expiresAt
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
} else {
|
|
908
|
-
await this.markAuthFailure(uuid, result);
|
|
909
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
910
|
-
}
|
|
911
|
-
return result;
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// ../multi-account-core/src/account-store.ts
|
|
917
|
-
import { promises as fs4 } from "node:fs";
|
|
918
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
919
|
-
import { dirname as dirname4, join as join5 } from "node:path";
|
|
920
|
-
import lockfile from "proper-lockfile";
|
|
921
|
-
import * as v4 from "valibot";
|
|
922
|
-
|
|
923
|
-
// ../multi-account-core/src/storage.ts
|
|
924
|
-
import { promises as fs3 } from "node:fs";
|
|
925
|
-
import { dirname as dirname3, join as join4 } from "node:path";
|
|
926
|
-
import * as v3 from "valibot";
|
|
927
|
-
|
|
928
|
-
// ../multi-account-core/src/constants.ts
|
|
929
|
-
var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
|
|
930
|
-
var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
|
|
931
|
-
function setAccountsFilename(filename) {
|
|
932
|
-
if (!filename) {
|
|
933
|
-
ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
ACCOUNTS_FILENAME = filename;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ../multi-account-core/src/storage.ts
|
|
940
|
-
function getStoragePath() {
|
|
941
|
-
return join4(getConfigDir2(), ACCOUNTS_FILENAME);
|
|
942
|
-
}
|
|
943
|
-
async function backupCorruptFile(targetPath, content) {
|
|
944
|
-
const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
|
|
945
|
-
await fs3.mkdir(dirname3(backupPath), { recursive: true });
|
|
946
|
-
await fs3.writeFile(backupPath, content, "utf-8");
|
|
947
|
-
}
|
|
948
|
-
async function readStorageFromDisk(targetPath, backupOnCorrupt) {
|
|
949
|
-
let content;
|
|
950
|
-
try {
|
|
951
|
-
content = await fs3.readFile(targetPath, "utf-8");
|
|
952
|
-
} catch (error) {
|
|
953
|
-
if (getErrorCode(error) === "ENOENT") {
|
|
954
|
-
return null;
|
|
955
|
-
}
|
|
956
|
-
throw error;
|
|
957
|
-
}
|
|
958
|
-
let parsed;
|
|
959
|
-
try {
|
|
960
|
-
parsed = JSON.parse(content);
|
|
961
|
-
} catch {
|
|
962
|
-
if (backupOnCorrupt) {
|
|
963
|
-
try {
|
|
964
|
-
await backupCorruptFile(targetPath, content);
|
|
965
|
-
} catch {
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
const validation = v3.safeParse(AccountStorageSchema, parsed);
|
|
971
|
-
if (!validation.success) {
|
|
972
|
-
if (backupOnCorrupt) {
|
|
973
|
-
try {
|
|
974
|
-
await backupCorruptFile(targetPath, content);
|
|
975
|
-
} catch {
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
return validation.output;
|
|
981
|
-
}
|
|
982
|
-
function deduplicateAccounts(accounts) {
|
|
983
|
-
const deduplicated = [];
|
|
984
|
-
const indexByUuid = /* @__PURE__ */ new Map();
|
|
985
|
-
for (const account of accounts) {
|
|
986
|
-
if (!account.uuid) {
|
|
987
|
-
deduplicated.push(account);
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
const existingIndex = indexByUuid.get(account.uuid);
|
|
991
|
-
if (existingIndex === void 0) {
|
|
992
|
-
indexByUuid.set(account.uuid, deduplicated.length);
|
|
993
|
-
deduplicated.push(account);
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
const existingAccount = deduplicated[existingIndex];
|
|
997
|
-
if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
|
|
998
|
-
deduplicated[existingIndex] = account;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return deduplicated;
|
|
1002
|
-
}
|
|
1003
|
-
async function loadAccounts() {
|
|
1004
|
-
const storagePath = getStoragePath();
|
|
1005
|
-
const storage = await readStorageFromDisk(storagePath, true);
|
|
1006
|
-
if (!storage) {
|
|
1007
|
-
return null;
|
|
1008
|
-
}
|
|
1009
|
-
return {
|
|
1010
|
-
...storage,
|
|
1011
|
-
accounts: deduplicateAccounts(storage.accounts || [])
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// ../multi-account-core/src/account-store.ts
|
|
1016
|
-
var FILE_MODE = 384;
|
|
1017
|
-
var LOCK_OPTIONS = {
|
|
1018
|
-
stale: 1e4,
|
|
1019
|
-
retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
|
|
1020
|
-
};
|
|
1021
|
-
function getStoragePath2() {
|
|
1022
|
-
return join5(getConfigDir2(), ACCOUNTS_FILENAME);
|
|
1023
|
-
}
|
|
1024
|
-
function createEmptyStorage() {
|
|
1025
|
-
return { version: 1, accounts: [] };
|
|
1026
|
-
}
|
|
1027
|
-
function buildTempPath(targetPath) {
|
|
1028
|
-
return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
|
|
1029
|
-
}
|
|
1030
|
-
async function writeAtomicText(targetPath, content) {
|
|
1031
|
-
await fs4.mkdir(dirname4(targetPath), { recursive: true });
|
|
1032
|
-
const tempPath = buildTempPath(targetPath);
|
|
1033
|
-
try {
|
|
1034
|
-
await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
|
|
1035
|
-
await fs4.chmod(tempPath, FILE_MODE);
|
|
1036
|
-
await fs4.rename(tempPath, targetPath);
|
|
1037
|
-
await fs4.chmod(targetPath, FILE_MODE);
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
try {
|
|
1040
|
-
await fs4.unlink(tempPath);
|
|
1041
|
-
} catch {
|
|
1042
|
-
}
|
|
1043
|
-
throw error;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
async function writeStorageAtomic(targetPath, storage) {
|
|
1047
|
-
const validation = v4.safeParse(AccountStorageSchema, storage);
|
|
1048
|
-
if (!validation.success) {
|
|
1049
|
-
throw new Error("Invalid account storage payload");
|
|
1050
|
-
}
|
|
1051
|
-
await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
|
|
1052
|
-
`);
|
|
1053
|
-
}
|
|
1054
|
-
async function ensureStorageFileExists(targetPath) {
|
|
1055
|
-
await fs4.mkdir(dirname4(targetPath), { recursive: true });
|
|
1056
|
-
const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
|
|
1057
|
-
`;
|
|
1058
|
-
try {
|
|
1059
|
-
await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
if (getErrorCode(error) !== "EEXIST") throw error;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
async function withFileLock(fn) {
|
|
1065
|
-
const storagePath = getStoragePath2();
|
|
1066
|
-
await ensureStorageFileExists(storagePath);
|
|
1067
|
-
let release = null;
|
|
1068
|
-
try {
|
|
1069
|
-
release = await lockfile.lock(storagePath, LOCK_OPTIONS);
|
|
1070
|
-
return await fn(storagePath);
|
|
1071
|
-
} finally {
|
|
1072
|
-
if (release) {
|
|
1073
|
-
try {
|
|
1074
|
-
await release();
|
|
1075
|
-
} catch {
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
var AccountStore = class {
|
|
1081
|
-
async load() {
|
|
1082
|
-
const storage = await loadAccounts();
|
|
1083
|
-
return storage ?? createEmptyStorage();
|
|
1084
|
-
}
|
|
1085
|
-
async readCredentials(uuid) {
|
|
1086
|
-
const storagePath = getStoragePath2();
|
|
1087
|
-
const storage = await readStorageFromDisk(storagePath, false);
|
|
1088
|
-
if (!storage) return null;
|
|
1089
|
-
const account = storage.accounts.find((a) => a.uuid === uuid);
|
|
1090
|
-
if (!account) return null;
|
|
1091
|
-
return {
|
|
1092
|
-
refreshToken: account.refreshToken,
|
|
1093
|
-
accessToken: account.accessToken,
|
|
1094
|
-
expiresAt: account.expiresAt,
|
|
1095
|
-
accountId: account.accountId
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
async mutateAccount(uuid, fn) {
|
|
1099
|
-
return await withFileLock(async (storagePath) => {
|
|
1100
|
-
const current = await readStorageFromDisk(storagePath, false);
|
|
1101
|
-
if (!current) return null;
|
|
1102
|
-
const account = current.accounts.find((a) => a.uuid === uuid);
|
|
1103
|
-
if (!account) return null;
|
|
1104
|
-
fn(account);
|
|
1105
|
-
await writeStorageAtomic(storagePath, current);
|
|
1106
|
-
return { ...account };
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
async mutateStorage(fn) {
|
|
1110
|
-
await withFileLock(async (storagePath) => {
|
|
1111
|
-
const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
|
|
1112
|
-
fn(current);
|
|
1113
|
-
await writeStorageAtomic(storagePath, current);
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
async addAccount(account) {
|
|
1117
|
-
await withFileLock(async (storagePath) => {
|
|
1118
|
-
const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
|
|
1119
|
-
const exists = current.accounts.some(
|
|
1120
|
-
(a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
|
|
1121
|
-
);
|
|
1122
|
-
if (exists) return;
|
|
1123
|
-
current.accounts.push(account);
|
|
1124
|
-
await writeStorageAtomic(storagePath, current);
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
async removeAccount(uuid) {
|
|
1128
|
-
return await withFileLock(async (storagePath) => {
|
|
1129
|
-
const current = await readStorageFromDisk(storagePath, false);
|
|
1130
|
-
if (!current) return false;
|
|
1131
|
-
const initialLength = current.accounts.length;
|
|
1132
|
-
current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
|
|
1133
|
-
if (current.accounts.length === initialLength) return false;
|
|
1134
|
-
if (current.activeAccountUuid === uuid) {
|
|
1135
|
-
current.activeAccountUuid = current.accounts[0]?.uuid;
|
|
1136
|
-
}
|
|
1137
|
-
await writeStorageAtomic(storagePath, current);
|
|
1138
|
-
return true;
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
async setActiveUuid(uuid) {
|
|
1142
|
-
await this.mutateStorage((storage) => {
|
|
1143
|
-
storage.activeAccountUuid = uuid;
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
async clear() {
|
|
1147
|
-
await withFileLock(async (storagePath) => {
|
|
1148
|
-
await writeStorageAtomic(storagePath, createEmptyStorage());
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
// ../multi-account-core/src/executor.ts
|
|
1154
|
-
var MIN_MAX_RETRIES = 6;
|
|
1155
|
-
var RETRIES_PER_ACCOUNT = 3;
|
|
1156
|
-
var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
|
|
1157
|
-
var MAX_RESOLVE_ATTEMPTS = 10;
|
|
1158
|
-
var SERVER_RETRY_BASE_MS = 1e3;
|
|
1159
|
-
var SERVER_RETRY_MAX_MS = 4e3;
|
|
1160
|
-
function isAbortError(error) {
|
|
1161
|
-
return error instanceof Error && error.name === "AbortError";
|
|
1162
|
-
}
|
|
1163
|
-
function createExecutorForProvider(providerName, dependencies) {
|
|
1164
|
-
const {
|
|
1165
|
-
handleRateLimitResponse: handleRateLimitResponse2,
|
|
1166
|
-
formatWaitTime: formatWaitTime2,
|
|
1167
|
-
sleep: sleep2,
|
|
1168
|
-
showToast: showToast2,
|
|
1169
|
-
getAccountLabel: getAccountLabel2
|
|
1170
|
-
} = dependencies;
|
|
1171
|
-
async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
|
|
1172
|
-
const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
|
|
1173
|
-
let previousAccountUuid;
|
|
1174
|
-
async function retryServerErrors(account, runtime) {
|
|
1175
|
-
for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
|
|
1176
|
-
const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
|
|
1177
|
-
const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
|
|
1178
|
-
await sleep2(jitteredBackoff);
|
|
1179
|
-
let retryResponse;
|
|
1180
|
-
try {
|
|
1181
|
-
retryResponse = await runtime.fetch(input, init);
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
if (isAbortError(error)) throw error;
|
|
1184
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1188
|
-
return null;
|
|
1189
|
-
}
|
|
1190
|
-
if (retryResponse.status < 500) return retryResponse;
|
|
1191
|
-
}
|
|
1192
|
-
return null;
|
|
1193
|
-
}
|
|
1194
|
-
const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
|
|
1195
|
-
if (response.status >= 500) {
|
|
1196
|
-
const recovered = await retryServerErrors(account, runtime);
|
|
1197
|
-
if (recovered === null) {
|
|
1198
|
-
return { type: "retryOuter" };
|
|
1199
|
-
}
|
|
1200
|
-
response = recovered;
|
|
1201
|
-
}
|
|
1202
|
-
if (response.status === 401) {
|
|
1203
|
-
if (allow401Retry) {
|
|
1204
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1205
|
-
try {
|
|
1206
|
-
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1207
|
-
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1208
|
-
return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
if (isAbortError(error)) throw error;
|
|
1211
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1212
|
-
return { type: "retryOuter" };
|
|
1213
|
-
}
|
|
1214
|
-
return { type: "retryOuter" };
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
|
|
1218
|
-
await manager.refresh();
|
|
1219
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1220
|
-
void showToast2(client, "All accounts have auth failures.", "error");
|
|
1221
|
-
throw new Error(
|
|
1222
|
-
`All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1225
|
-
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1226
|
-
return { type: "retryOuter" };
|
|
1227
|
-
}
|
|
1228
|
-
if (response.status === 403) {
|
|
1229
|
-
const revoked = await isRevokedTokenResponse(response);
|
|
1230
|
-
if (revoked) {
|
|
1231
|
-
await manager.markRevoked(accountUuid);
|
|
1232
|
-
await manager.refresh();
|
|
1233
|
-
void showToast2(
|
|
1234
|
-
client,
|
|
1235
|
-
`${getAccountLabel2(account)} disabled: OAuth token revoked.`,
|
|
1236
|
-
"error"
|
|
1237
|
-
);
|
|
1238
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1239
|
-
throw new Error(
|
|
1240
|
-
`All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
return { type: "retryOuter" };
|
|
1244
|
-
}
|
|
1245
|
-
if (from401RefreshRetry) {
|
|
1246
|
-
return { type: "handled", response };
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
if (response.status === 429) {
|
|
1250
|
-
await handleRateLimitResponse2(manager, client, account, response);
|
|
1251
|
-
return { type: "handled" };
|
|
1252
|
-
}
|
|
1253
|
-
return { type: "success", response };
|
|
1254
|
-
};
|
|
1255
|
-
for (let retries = 1; retries <= maxRetries; retries++) {
|
|
1256
|
-
await manager.refresh();
|
|
1257
|
-
const account = await resolveAccount(manager, client);
|
|
1258
|
-
const accountUuid = account.uuid;
|
|
1259
|
-
if (!accountUuid) continue;
|
|
1260
|
-
if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
|
|
1261
|
-
void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
|
|
1262
|
-
}
|
|
1263
|
-
previousAccountUuid = accountUuid;
|
|
1264
|
-
let runtime;
|
|
1265
|
-
let response;
|
|
1266
|
-
try {
|
|
1267
|
-
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1268
|
-
response = await runtime.fetch(input, init);
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
if (isAbortError(error)) throw error;
|
|
1271
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
|
|
1278
|
-
if (transition.type === "retryOuter" || transition.type === "handled") {
|
|
1279
|
-
if (transition.type === "handled" && transition.response) {
|
|
1280
|
-
return transition.response;
|
|
1281
|
-
}
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
await manager.markSuccess(accountUuid);
|
|
1285
|
-
return transition.response;
|
|
1286
|
-
}
|
|
1287
|
-
throw new Error(
|
|
1288
|
-
`Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
|
|
1292
|
-
if (!isTokenRefreshError(error)) return false;
|
|
1293
|
-
if (!account.uuid) return false;
|
|
1294
|
-
const accountUuid = account.uuid;
|
|
1295
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1296
|
-
await manager.markAuthFailure(accountUuid, {
|
|
1297
|
-
ok: false,
|
|
1298
|
-
permanent: error.permanent
|
|
1299
|
-
});
|
|
1300
|
-
await manager.refresh();
|
|
1301
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1302
|
-
void showToast2(client, "All accounts have auth failures.", "error");
|
|
1303
|
-
throw new Error(
|
|
1304
|
-
`All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1308
|
-
return true;
|
|
1309
|
-
}
|
|
1310
|
-
async function resolveAccount(manager, client) {
|
|
1311
|
-
let attempts = 0;
|
|
1312
|
-
while (true) {
|
|
1313
|
-
if (++attempts > MAX_RESOLVE_ATTEMPTS) {
|
|
1314
|
-
throw new Error(
|
|
1315
|
-
`Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
const account = await manager.selectAccount();
|
|
1319
|
-
if (account) return account;
|
|
1320
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1321
|
-
throw new Error(
|
|
1322
|
-
`All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
const waitMs = manager.getMinWaitTime();
|
|
1326
|
-
if (waitMs <= 0) {
|
|
1327
|
-
throw new Error(
|
|
1328
|
-
`All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
await showToast2(
|
|
1332
|
-
client,
|
|
1333
|
-
`All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
|
|
1334
|
-
"warning"
|
|
1335
|
-
);
|
|
1336
|
-
await sleep2(waitMs);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return {
|
|
1340
|
-
executeWithAccountRotation: executeWithAccountRotation2
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
async function isRevokedTokenResponse(response) {
|
|
1344
|
-
try {
|
|
1345
|
-
const cloned = response.clone();
|
|
1346
|
-
const body = await cloned.text();
|
|
1347
|
-
return body.includes("revoked");
|
|
1348
|
-
} catch {
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// ../multi-account-core/src/proactive-refresh.ts
|
|
1354
|
-
var INITIAL_DELAY_MS = 5e3;
|
|
1355
|
-
function createProactiveRefreshQueueForProvider(dependencies) {
|
|
1356
|
-
const {
|
|
1357
|
-
providerAuthId,
|
|
1358
|
-
getConfig: getConfig2,
|
|
1359
|
-
refreshToken: refreshToken2,
|
|
1360
|
-
isTokenExpired: isTokenExpired2,
|
|
1361
|
-
debugLog: debugLog2
|
|
1362
|
-
} = dependencies;
|
|
1363
|
-
return class ProactiveRefreshQueue {
|
|
1364
|
-
constructor(client, store, onInvalidate) {
|
|
1365
|
-
this.client = client;
|
|
1366
|
-
this.store = store;
|
|
1367
|
-
this.onInvalidate = onInvalidate;
|
|
1368
|
-
}
|
|
1369
|
-
timeoutHandle = null;
|
|
1370
|
-
runToken = 0;
|
|
1371
|
-
inFlight = null;
|
|
1372
|
-
start() {
|
|
1373
|
-
const config = getConfig2();
|
|
1374
|
-
if (!config.proactive_refresh) return;
|
|
1375
|
-
this.runToken++;
|
|
1376
|
-
if (this.timeoutHandle) {
|
|
1377
|
-
clearTimeout(this.timeoutHandle);
|
|
1378
|
-
this.timeoutHandle = null;
|
|
1379
|
-
}
|
|
1380
|
-
this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
|
|
1381
|
-
debugLog2(this.client, "Proactive refresh started", {
|
|
1382
|
-
intervalSeconds: config.proactive_refresh_interval_seconds,
|
|
1383
|
-
bufferSeconds: config.proactive_refresh_buffer_seconds
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
async stop() {
|
|
1387
|
-
this.runToken++;
|
|
1388
|
-
if (this.timeoutHandle) {
|
|
1389
|
-
clearTimeout(this.timeoutHandle);
|
|
1390
|
-
this.timeoutHandle = null;
|
|
1391
|
-
}
|
|
1392
|
-
if (this.inFlight) {
|
|
1393
|
-
await this.inFlight;
|
|
1394
|
-
this.inFlight = null;
|
|
1395
|
-
}
|
|
1396
|
-
debugLog2(this.client, "Proactive refresh stopped");
|
|
1397
|
-
}
|
|
1398
|
-
scheduleNext(token, delayMs) {
|
|
1399
|
-
this.timeoutHandle = setTimeout(() => {
|
|
1400
|
-
if (token !== this.runToken) return;
|
|
1401
|
-
this.inFlight = this.runCheck(token).finally(() => {
|
|
1402
|
-
this.inFlight = null;
|
|
1403
|
-
});
|
|
1404
|
-
}, delayMs);
|
|
1405
|
-
}
|
|
1406
|
-
needsProactiveRefresh(account) {
|
|
1407
|
-
if (!account.accessToken || !account.expiresAt) return false;
|
|
1408
|
-
if (isTokenExpired2(account)) return false;
|
|
1409
|
-
const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
|
|
1410
|
-
return account.expiresAt <= Date.now() + bufferMs;
|
|
1411
|
-
}
|
|
1412
|
-
async runCheck(token) {
|
|
1413
|
-
try {
|
|
1414
|
-
const stored = await this.store.load();
|
|
1415
|
-
if (token !== this.runToken) return;
|
|
1416
|
-
const candidates = stored.accounts.filter(
|
|
1417
|
-
(a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
|
|
1418
|
-
);
|
|
1419
|
-
if (candidates.length === 0) return;
|
|
1420
|
-
debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
|
|
1421
|
-
for (const account of candidates) {
|
|
1422
|
-
if (token !== this.runToken) return;
|
|
1423
|
-
const credentials = await this.store.readCredentials(account.uuid);
|
|
1424
|
-
if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
|
|
1425
|
-
const result = await refreshToken2(credentials.refreshToken, account.uuid, this.client);
|
|
1426
|
-
if (result.ok) {
|
|
1427
|
-
await this.store.mutateAccount(account.uuid, (target) => {
|
|
1428
|
-
target.accessToken = result.patch.accessToken;
|
|
1429
|
-
target.expiresAt = result.patch.expiresAt;
|
|
1430
|
-
if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
|
|
1431
|
-
if (result.patch.uuid) target.uuid = result.patch.uuid;
|
|
1432
|
-
if (result.patch.email) target.email = result.patch.email;
|
|
1433
|
-
if (result.patch.accountId) target.accountId = result.patch.accountId;
|
|
1434
|
-
target.consecutiveAuthFailures = 0;
|
|
1435
|
-
target.isAuthDisabled = false;
|
|
1436
|
-
target.authDisabledReason = void 0;
|
|
1437
|
-
});
|
|
1438
|
-
this.onInvalidate?.(account.uuid);
|
|
1439
|
-
} else {
|
|
1440
|
-
await this.persistFailure(account, result.permanent);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
debugLog2(this.client, `Proactive refresh check error: ${error}`);
|
|
1445
|
-
} finally {
|
|
1446
|
-
if (token === this.runToken) {
|
|
1447
|
-
const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
|
|
1448
|
-
this.scheduleNext(token, intervalMs);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
async persistFailure(account, permanent) {
|
|
1453
|
-
try {
|
|
1454
|
-
const accountUuid = account.uuid;
|
|
1455
|
-
if (!accountUuid) return;
|
|
1456
|
-
if (permanent) {
|
|
1457
|
-
const removed = await this.store.removeAccount(accountUuid);
|
|
1458
|
-
if (!removed) return;
|
|
1459
|
-
this.onInvalidate?.(accountUuid);
|
|
1460
|
-
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
await this.store.mutateStorage((storage) => {
|
|
1464
|
-
const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
|
|
1465
|
-
if (!target) return;
|
|
1466
|
-
target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
|
|
1467
|
-
const maxFailures = getConfig2().max_consecutive_auth_failures;
|
|
1468
|
-
const usableCount = storage.accounts.filter(
|
|
1469
|
-
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
|
|
1470
|
-
).length;
|
|
1471
|
-
if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
1472
|
-
target.isAuthDisabled = true;
|
|
1473
|
-
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1474
|
-
}
|
|
1475
|
-
});
|
|
1476
|
-
} catch {
|
|
1477
|
-
debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
1481
|
-
const storage = await this.store.load();
|
|
1482
|
-
if (storage.accounts.length > 0) return;
|
|
1483
|
-
await this.client.auth.set({
|
|
1484
|
-
path: { id: providerAuthId },
|
|
1485
|
-
body: getClearedOAuthBody()
|
|
1486
|
-
}).catch(() => {
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// ../multi-account-core/src/rate-limit.ts
|
|
1493
|
-
var USAGE_FETCH_COOLDOWN_MS = 3e4;
|
|
1494
|
-
function createRateLimitHandlers(dependencies) {
|
|
1495
|
-
const {
|
|
1496
|
-
fetchUsage: fetchUsage2,
|
|
1497
|
-
getConfig: getConfig2,
|
|
1498
|
-
formatWaitTime: formatWaitTime2,
|
|
1499
|
-
getAccountLabel: getAccountLabel2,
|
|
1500
|
-
showToast: showToast2
|
|
1501
|
-
} = dependencies;
|
|
1502
|
-
function retryAfterMsFromResponse2(response) {
|
|
1503
|
-
const retryAfterMs = response.headers.get("retry-after-ms");
|
|
1504
|
-
if (retryAfterMs) {
|
|
1505
|
-
const parsed = parseInt(retryAfterMs, 10);
|
|
1506
|
-
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
1507
|
-
}
|
|
1508
|
-
const retryAfter = response.headers.get("retry-after");
|
|
1509
|
-
if (retryAfter) {
|
|
1510
|
-
const parsed = parseInt(retryAfter, 10);
|
|
1511
|
-
if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
|
|
1512
|
-
}
|
|
1513
|
-
return getConfig2().default_retry_after_ms;
|
|
1514
|
-
}
|
|
1515
|
-
function getResetMsFromUsage2(account) {
|
|
1516
|
-
const usage = account.cachedUsage;
|
|
1517
|
-
if (!usage) return null;
|
|
1518
|
-
const now = Date.now();
|
|
1519
|
-
const candidates = [];
|
|
1520
|
-
if (usage.five_hour?.resets_at) {
|
|
1521
|
-
const ms = Date.parse(usage.five_hour.resets_at) - now;
|
|
1522
|
-
if (ms > 0) candidates.push(ms);
|
|
1523
|
-
}
|
|
1524
|
-
if (usage.seven_day?.resets_at) {
|
|
1525
|
-
const ms = Date.parse(usage.seven_day.resets_at) - now;
|
|
1526
|
-
if (ms > 0) candidates.push(ms);
|
|
1527
|
-
}
|
|
1528
|
-
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
1529
|
-
}
|
|
1530
|
-
async function fetchUsageLimits2(accessToken, accountId) {
|
|
1531
|
-
if (!accessToken) return null;
|
|
1532
|
-
try {
|
|
1533
|
-
const result = await fetchUsage2(accessToken, accountId);
|
|
1534
|
-
return result.ok ? result.data : null;
|
|
1535
|
-
} catch {
|
|
1536
|
-
return null;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
async function handleRateLimitResponse2(manager, client, account, response) {
|
|
1540
|
-
if (!account.uuid) return;
|
|
1541
|
-
const resetMs = getResetMsFromUsage2(account) ?? retryAfterMsFromResponse2(response);
|
|
1542
|
-
await manager.markRateLimited(account.uuid, resetMs);
|
|
1543
|
-
const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
|
|
1544
|
-
if (shouldFetchUsage) {
|
|
1545
|
-
const usage = await fetchUsageLimits2(account.accessToken, account.accountId);
|
|
1546
|
-
if (usage) {
|
|
1547
|
-
await manager.applyUsageCache(account.uuid, usage);
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
if (manager.getAccountCount() > 1) {
|
|
1551
|
-
void showToast2(
|
|
1552
|
-
client,
|
|
1553
|
-
`${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
|
|
1554
|
-
"warning"
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return {
|
|
1559
|
-
retryAfterMsFromResponse: retryAfterMsFromResponse2,
|
|
1560
|
-
getResetMsFromUsage: getResetMsFromUsage2,
|
|
1561
|
-
fetchUsageLimits: fetchUsageLimits2,
|
|
1562
|
-
handleRateLimitResponse: handleRateLimitResponse2
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// ../multi-account-core/src/auth-migration.ts
|
|
1567
|
-
import { promises as fs5 } from "node:fs";
|
|
1568
|
-
import { join as join6 } from "node:path";
|
|
1569
|
-
var AUTH_JSON_FILENAME = "auth.json";
|
|
1570
|
-
function isValidOAuthCredential(value) {
|
|
1571
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1572
|
-
const candidate = value;
|
|
1573
|
-
return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
|
|
1574
|
-
}
|
|
1575
|
-
function resolveAuthJsonPath() {
|
|
1576
|
-
return join6(getConfigDir2(), AUTH_JSON_FILENAME);
|
|
1577
|
-
}
|
|
1578
|
-
async function readAuthJson() {
|
|
1579
|
-
const authPath = resolveAuthJsonPath();
|
|
1580
|
-
let content;
|
|
1581
|
-
try {
|
|
1582
|
-
content = await fs5.readFile(authPath, "utf-8");
|
|
1583
|
-
} catch {
|
|
1584
|
-
return null;
|
|
1585
|
-
}
|
|
1586
|
-
try {
|
|
1587
|
-
const parsed = JSON.parse(content);
|
|
1588
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
return parsed;
|
|
1592
|
-
} catch {
|
|
1593
|
-
return null;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
async function migrateFromAuthJson(providerKey, store) {
|
|
1597
|
-
const storage = await store.load();
|
|
1598
|
-
const hasExistingAccounts = storage.accounts.length > 0;
|
|
1599
|
-
if (hasExistingAccounts) return false;
|
|
1600
|
-
const authData = await readAuthJson();
|
|
1601
|
-
if (!authData) return false;
|
|
1602
|
-
const providerCredential = authData[providerKey];
|
|
1603
|
-
if (!isValidOAuthCredential(providerCredential)) return false;
|
|
1604
|
-
const now = Date.now();
|
|
1605
|
-
const newAccount = {
|
|
1606
|
-
uuid: crypto.randomUUID(),
|
|
1607
|
-
refreshToken: providerCredential.refresh,
|
|
1608
|
-
accessToken: providerCredential.access,
|
|
1609
|
-
expiresAt: providerCredential.expires,
|
|
1610
|
-
addedAt: now,
|
|
1611
|
-
lastUsed: now,
|
|
1612
|
-
enabled: true,
|
|
1613
|
-
planTier: "",
|
|
1614
|
-
consecutiveAuthFailures: 0,
|
|
1615
|
-
isAuthDisabled: false
|
|
1616
|
-
};
|
|
1617
|
-
await store.addAccount(newAccount);
|
|
1618
|
-
await store.setActiveUuid(newAccount.uuid);
|
|
1619
|
-
return true;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
// ../multi-account-core/src/ui/ansi.ts
|
|
1623
|
-
var ANSI = {
|
|
1624
|
-
hide: "\x1B[?25l",
|
|
1625
|
-
show: "\x1B[?25h",
|
|
1626
|
-
up: (n = 1) => `\x1B[${n}A`,
|
|
1627
|
-
down: (n = 1) => `\x1B[${n}B`,
|
|
1628
|
-
clearLine: "\x1B[2K",
|
|
1629
|
-
cyan: "\x1B[36m",
|
|
1630
|
-
green: "\x1B[32m",
|
|
1631
|
-
red: "\x1B[31m",
|
|
1632
|
-
yellow: "\x1B[33m",
|
|
1633
|
-
dim: "\x1B[2m",
|
|
1634
|
-
bold: "\x1B[1m",
|
|
1635
|
-
reset: "\x1B[0m"
|
|
1636
|
-
};
|
|
1637
|
-
function parseKey(data) {
|
|
1638
|
-
const s = data.toString();
|
|
1639
|
-
if (s === "\x1B[A" || s === "\x1BOA") return "up";
|
|
1640
|
-
if (s === "\x1B[B" || s === "\x1BOB") return "down";
|
|
1641
|
-
if (s === "\r" || s === "\n") return "enter";
|
|
1642
|
-
if (s === "") return "escape";
|
|
1643
|
-
if (s === "\x1B") return "escape-start";
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
function isTTY() {
|
|
1647
|
-
return Boolean(process.stdin.isTTY);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// ../multi-account-core/src/ui/select.ts
|
|
1651
|
-
var ESCAPE_TIMEOUT_MS = 50;
|
|
1652
|
-
var COLOR_MAP = {
|
|
1653
|
-
red: ANSI.red,
|
|
1654
|
-
green: ANSI.green,
|
|
1655
|
-
yellow: ANSI.yellow,
|
|
1656
|
-
cyan: ANSI.cyan
|
|
1657
|
-
};
|
|
1658
|
-
async function select(items, options) {
|
|
1659
|
-
if (!isTTY()) {
|
|
1660
|
-
throw new Error("Interactive select requires a TTY terminal");
|
|
1661
|
-
}
|
|
1662
|
-
const enabledItems = items.filter((i) => !i.disabled && !i.separator);
|
|
1663
|
-
if (enabledItems.length === 0) {
|
|
1664
|
-
throw new Error("All items disabled");
|
|
1665
|
-
}
|
|
1666
|
-
if (enabledItems.length === 1) {
|
|
1667
|
-
return enabledItems[0].value;
|
|
1668
|
-
}
|
|
1669
|
-
const { message, subtitle } = options;
|
|
1670
|
-
const { stdin, stdout } = process;
|
|
1671
|
-
let cursor = items.findIndex((i) => !i.disabled && !i.separator);
|
|
1672
|
-
if (cursor === -1) cursor = 0;
|
|
1673
|
-
let escapeTimeout = null;
|
|
1674
|
-
let isCleanedUp = false;
|
|
1675
|
-
let isFirstRender = true;
|
|
1676
|
-
const getTotalLines = () => {
|
|
1677
|
-
const subtitleLines = subtitle ? 3 : 0;
|
|
1678
|
-
return 1 + subtitleLines + items.length + 1 + 1;
|
|
1679
|
-
};
|
|
1680
|
-
const renderItemLabel = (item, isSelected) => {
|
|
1681
|
-
const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
|
|
1682
|
-
if (item.disabled) {
|
|
1683
|
-
return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
|
|
1684
|
-
}
|
|
1685
|
-
const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
|
|
1686
|
-
if (isSelected) {
|
|
1687
|
-
const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
|
|
1688
|
-
return `${label}${hintSuffix}`;
|
|
1689
|
-
}
|
|
1690
|
-
const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
|
|
1691
|
-
return `${dimLabel}${hintSuffix}`;
|
|
1692
|
-
};
|
|
1693
|
-
const render = () => {
|
|
1694
|
-
const totalLines = getTotalLines();
|
|
1695
|
-
if (!isFirstRender) {
|
|
1696
|
-
stdout.write(ANSI.up(totalLines) + "\r");
|
|
1697
|
-
}
|
|
1698
|
-
isFirstRender = false;
|
|
1699
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
|
|
1700
|
-
`);
|
|
1701
|
-
if (subtitle) {
|
|
1702
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
|
|
1703
|
-
`);
|
|
1704
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
|
|
1705
|
-
`);
|
|
1706
|
-
stdout.write(`${ANSI.clearLine}
|
|
1707
|
-
`);
|
|
1708
|
-
}
|
|
1709
|
-
for (let i = 0; i < items.length; i++) {
|
|
1710
|
-
const item = items[i];
|
|
1711
|
-
if (!item) continue;
|
|
1712
|
-
if (item.separator) {
|
|
1713
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
|
|
1714
|
-
`);
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
const isSelected = i === cursor;
|
|
1718
|
-
const labelText = renderItemLabel(item, isSelected);
|
|
1719
|
-
const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
|
|
1720
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
|
|
1721
|
-
`);
|
|
1722
|
-
}
|
|
1723
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
|
|
1724
|
-
`);
|
|
1725
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
|
|
1726
|
-
`);
|
|
1727
|
-
};
|
|
1728
|
-
return new Promise((resolve) => {
|
|
1729
|
-
const wasRaw = stdin.isRaw ?? false;
|
|
1730
|
-
const cleanup = () => {
|
|
1731
|
-
if (isCleanedUp) return;
|
|
1732
|
-
isCleanedUp = true;
|
|
1733
|
-
if (escapeTimeout) {
|
|
1734
|
-
clearTimeout(escapeTimeout);
|
|
1735
|
-
escapeTimeout = null;
|
|
1736
|
-
}
|
|
1737
|
-
try {
|
|
1738
|
-
stdin.removeListener("data", onKey);
|
|
1739
|
-
stdin.setRawMode(wasRaw);
|
|
1740
|
-
stdin.pause();
|
|
1741
|
-
stdout.write(ANSI.show);
|
|
1742
|
-
} catch {
|
|
1743
|
-
}
|
|
1744
|
-
process.removeListener("SIGINT", onSignal);
|
|
1745
|
-
process.removeListener("SIGTERM", onSignal);
|
|
1746
|
-
};
|
|
1747
|
-
const onSignal = () => {
|
|
1748
|
-
cleanup();
|
|
1749
|
-
resolve(null);
|
|
1750
|
-
};
|
|
1751
|
-
const finishWithValue = (value) => {
|
|
1752
|
-
cleanup();
|
|
1753
|
-
resolve(value);
|
|
1754
|
-
};
|
|
1755
|
-
const findNextSelectable = (from, direction) => {
|
|
1756
|
-
if (items.length === 0) return from;
|
|
1757
|
-
let next = from;
|
|
1758
|
-
do {
|
|
1759
|
-
next = (next + direction + items.length) % items.length;
|
|
1760
|
-
} while (items[next]?.disabled || items[next]?.separator);
|
|
1761
|
-
return next;
|
|
1762
|
-
};
|
|
1763
|
-
const onKey = (data) => {
|
|
1764
|
-
if (escapeTimeout) {
|
|
1765
|
-
clearTimeout(escapeTimeout);
|
|
1766
|
-
escapeTimeout = null;
|
|
1767
|
-
}
|
|
1768
|
-
const action = parseKey(data);
|
|
1769
|
-
switch (action) {
|
|
1770
|
-
case "up":
|
|
1771
|
-
cursor = findNextSelectable(cursor, -1);
|
|
1772
|
-
render();
|
|
1773
|
-
return;
|
|
1774
|
-
case "down":
|
|
1775
|
-
cursor = findNextSelectable(cursor, 1);
|
|
1776
|
-
render();
|
|
1777
|
-
return;
|
|
1778
|
-
case "enter":
|
|
1779
|
-
finishWithValue(items[cursor]?.value ?? null);
|
|
1780
|
-
return;
|
|
1781
|
-
case "escape":
|
|
1782
|
-
finishWithValue(null);
|
|
1783
|
-
return;
|
|
1784
|
-
case "escape-start":
|
|
1785
|
-
escapeTimeout = setTimeout(() => {
|
|
1786
|
-
finishWithValue(null);
|
|
1787
|
-
}, ESCAPE_TIMEOUT_MS);
|
|
1788
|
-
return;
|
|
1789
|
-
default:
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
process.once("SIGINT", onSignal);
|
|
1794
|
-
process.once("SIGTERM", onSignal);
|
|
1795
|
-
try {
|
|
1796
|
-
stdin.setRawMode(true);
|
|
1797
|
-
} catch {
|
|
1798
|
-
cleanup();
|
|
1799
|
-
resolve(null);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
stdin.resume();
|
|
1803
|
-
stdout.write(ANSI.hide);
|
|
1804
|
-
render();
|
|
1805
|
-
stdin.on("data", onKey);
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// ../multi-account-core/src/ui/confirm.ts
|
|
1810
|
-
async function confirm(message, defaultYes = false) {
|
|
1811
|
-
const items = defaultYes ? [
|
|
1812
|
-
{ label: "Yes", value: true },
|
|
1813
|
-
{ label: "No", value: false }
|
|
1814
|
-
] : [
|
|
1815
|
-
{ label: "No", value: false },
|
|
1816
|
-
{ label: "Yes", value: true }
|
|
1817
|
-
];
|
|
1818
|
-
const result = await select(items, { message });
|
|
1819
|
-
return result ?? false;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// ../multi-account-core/src/adapters/anthropic.ts
|
|
1823
|
-
var anthropicOAuthAdapter = {
|
|
1824
|
-
id: "anthropic",
|
|
1825
|
-
authProviderId: "anthropic",
|
|
1826
|
-
modelDisplayName: "Claude",
|
|
1827
|
-
statusToolName: "claude_multiauth_status",
|
|
1828
|
-
authMethodLabel: "Claude Pro/Max (Multi-Auth)",
|
|
1829
|
-
serviceLogName: "claude-multiauth",
|
|
1830
|
-
oauthClientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
1831
|
-
tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
|
|
1832
|
-
usageEndpoint: "https://api.anthropic.com/api/oauth/usage",
|
|
1833
|
-
profileEndpoint: "https://api.anthropic.com/api/oauth/profile",
|
|
1834
|
-
oauthBetaHeader: "oauth-2025-04-20",
|
|
1835
|
-
requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
|
1836
|
-
cliUserAgent: "claude-cli/2.1.2 (external, cli)",
|
|
1837
|
-
cliVersion: "2.1.80",
|
|
1838
|
-
billingSalt: "59cf53e54c78",
|
|
1839
|
-
toolPrefix: "mcp_",
|
|
1840
|
-
accountStorageFilename: "anthropic-multi-account-accounts.json",
|
|
1841
|
-
transform: {
|
|
1842
|
-
rewriteOpenCodeBranding: true,
|
|
1843
|
-
addToolPrefix: true,
|
|
1844
|
-
stripToolPrefixInResponse: true,
|
|
1845
|
-
enableMessagesBetaQuery: true
|
|
1846
|
-
},
|
|
1847
|
-
planLabels: {
|
|
1848
|
-
max: "Claude Max",
|
|
1849
|
-
pro: "Claude Pro",
|
|
1850
|
-
free: "Free"
|
|
1851
|
-
},
|
|
1852
|
-
supported: true
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
// ../multi-account-core/src/pool-types.ts
|
|
1856
|
-
import * as v5 from "valibot";
|
|
1857
|
-
var PoolConfigSchema = v5.object({
|
|
1858
|
-
name: v5.string(),
|
|
1859
|
-
baseProvider: v5.string(),
|
|
1860
|
-
members: v5.array(v5.string()),
|
|
1861
|
-
enabled: v5.boolean()
|
|
1862
|
-
});
|
|
1863
|
-
var ChainEntryConfigSchema = v5.object({
|
|
1864
|
-
pool: v5.string(),
|
|
1865
|
-
model: v5.optional(v5.string()),
|
|
1866
|
-
enabled: v5.boolean()
|
|
1867
|
-
});
|
|
1868
|
-
var ChainConfigSchema = v5.object({
|
|
1869
|
-
name: v5.string(),
|
|
1870
|
-
entries: v5.array(ChainEntryConfigSchema),
|
|
1871
|
-
enabled: v5.boolean()
|
|
1872
|
-
});
|
|
1873
|
-
var PoolChainConfigSchema = v5.object({
|
|
1874
|
-
pools: v5.optional(v5.array(PoolConfigSchema), []),
|
|
1875
|
-
chains: v5.optional(v5.array(ChainConfigSchema), [])
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
// ../multi-account-core/src/pool-config-store.ts
|
|
1879
|
-
import { promises as fs6 } from "node:fs";
|
|
1880
|
-
import { dirname as dirname5, join as join7 } from "node:path";
|
|
1881
|
-
import lockfile2 from "proper-lockfile";
|
|
1882
|
-
import * as v6 from "valibot";
|
|
1883
|
-
var POOL_CONFIG_FILENAME = "multiauth-pools.json";
|
|
1884
|
-
function createEmptyConfig() {
|
|
1885
|
-
return { pools: [], chains: [] };
|
|
1886
|
-
}
|
|
1887
|
-
function getGlobalConfigPath() {
|
|
1888
|
-
return join7(getConfigDir2(), POOL_CONFIG_FILENAME);
|
|
1889
|
-
}
|
|
1890
|
-
async function resolveConfigPath() {
|
|
1891
|
-
const projectPath = join7(process.cwd(), ".opencode", POOL_CONFIG_FILENAME);
|
|
1892
|
-
try {
|
|
1893
|
-
await fs6.access(projectPath);
|
|
1894
|
-
return projectPath;
|
|
1895
|
-
} catch {
|
|
1896
|
-
}
|
|
1897
|
-
return getGlobalConfigPath();
|
|
1898
|
-
}
|
|
1899
|
-
function parsePoolChainConfig(content) {
|
|
1900
|
-
let parsed;
|
|
1901
|
-
try {
|
|
1902
|
-
parsed = JSON.parse(content);
|
|
1903
|
-
} catch {
|
|
1904
|
-
return null;
|
|
1905
|
-
}
|
|
1906
|
-
const validation = v6.safeParse(PoolChainConfigSchema, parsed);
|
|
1907
|
-
return validation.success ? validation.output : null;
|
|
1908
|
-
}
|
|
1909
|
-
async function loadPoolChainConfig() {
|
|
1910
|
-
const path = await resolveConfigPath();
|
|
1911
|
-
try {
|
|
1912
|
-
const content = await fs6.readFile(path, "utf-8");
|
|
1913
|
-
return parsePoolChainConfig(content) ?? createEmptyConfig();
|
|
1914
|
-
} catch {
|
|
1915
|
-
return createEmptyConfig();
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
// ../multi-account-core/src/pool-manager.ts
|
|
1920
|
-
var DEFAULT_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
1921
|
-
var PoolManager = class {
|
|
1922
|
-
poolsByName = /* @__PURE__ */ new Map();
|
|
1923
|
-
exhaustedUntilByAccount = /* @__PURE__ */ new Map();
|
|
1924
|
-
exhaustedCooldownMs;
|
|
1925
|
-
constructor(options) {
|
|
1926
|
-
this.exhaustedCooldownMs = options?.exhaustedCooldownMs ?? DEFAULT_EXHAUSTED_COOLDOWN_MS;
|
|
1927
|
-
}
|
|
1928
|
-
loadPools(configs) {
|
|
1929
|
-
this.poolsByName.clear();
|
|
1930
|
-
for (const pool of configs) {
|
|
1931
|
-
this.poolsByName.set(pool.name, pool);
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
getPoolForAccount(accountUuid) {
|
|
1935
|
-
for (const pool of this.poolsByName.values()) {
|
|
1936
|
-
if (!pool.enabled) continue;
|
|
1937
|
-
if (pool.members.includes(accountUuid)) return pool;
|
|
1938
|
-
}
|
|
1939
|
-
return null;
|
|
1940
|
-
}
|
|
1941
|
-
getAvailableMembers(pool, accountManager) {
|
|
1942
|
-
if (!pool.enabled) return [];
|
|
1943
|
-
this.clearExpiredExhausted();
|
|
1944
|
-
const accountsByUuid = /* @__PURE__ */ new Map();
|
|
1945
|
-
for (const account of accountManager.getAccounts()) {
|
|
1946
|
-
if (!account.uuid) continue;
|
|
1947
|
-
accountsByUuid.set(account.uuid, account);
|
|
1948
|
-
}
|
|
1949
|
-
return pool.members.filter((accountUuid) => {
|
|
1950
|
-
const account = accountsByUuid.get(accountUuid);
|
|
1951
|
-
if (!account) return false;
|
|
1952
|
-
if (!account.enabled || account.isAuthDisabled) return false;
|
|
1953
|
-
if (this.isExhausted(accountUuid)) return false;
|
|
1954
|
-
if (accountManager.isRateLimited(account)) return false;
|
|
1955
|
-
return true;
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
markExhausted(accountUuid) {
|
|
1959
|
-
this.exhaustedUntilByAccount.set(accountUuid, Date.now() + this.exhaustedCooldownMs);
|
|
1960
|
-
}
|
|
1961
|
-
async getNextMember(pool, currentUuid, accountManager) {
|
|
1962
|
-
const availableMembers = this.getAvailableMembers(pool, accountManager);
|
|
1963
|
-
if (availableMembers.length === 0) return null;
|
|
1964
|
-
const excluded = /* @__PURE__ */ new Set();
|
|
1965
|
-
if (currentUuid) excluded.add(currentUuid);
|
|
1966
|
-
const preferred = await this.selectPreferredMember(availableMembers, excluded, accountManager);
|
|
1967
|
-
if (preferred) return preferred;
|
|
1968
|
-
for (const candidate of availableMembers) {
|
|
1969
|
-
if (candidate !== currentUuid) return candidate;
|
|
1970
|
-
}
|
|
1971
|
-
return null;
|
|
1972
|
-
}
|
|
1973
|
-
async buildFailoverPlan(currentAccount, config, accountManager, options) {
|
|
1974
|
-
this.loadPools(config.pools ?? []);
|
|
1975
|
-
if ((config.pools?.length ?? 0) === 0 && (config.chains?.length ?? 0) === 0) {
|
|
1976
|
-
return { candidates: [], skips: [] };
|
|
1977
|
-
}
|
|
1978
|
-
const attemptedAccounts = options?.attemptedAccounts ?? /* @__PURE__ */ new Set();
|
|
1979
|
-
const visitedChainIndexes = options?.visitedChainIndexes ?? /* @__PURE__ */ new Set();
|
|
1980
|
-
const currentUuid = currentAccount?.uuid;
|
|
1981
|
-
const candidates = [];
|
|
1982
|
-
const skips = [];
|
|
1983
|
-
const addedCandidateUuids = /* @__PURE__ */ new Set();
|
|
1984
|
-
const appendPoolCandidates = async (poolName, source, chainIndex) => {
|
|
1985
|
-
const pool = this.poolsByName.get(poolName);
|
|
1986
|
-
if (!pool || !pool.enabled) {
|
|
1987
|
-
skips.push({
|
|
1988
|
-
type: "chain_disabled",
|
|
1989
|
-
poolName,
|
|
1990
|
-
reason: "Pool is missing or disabled"
|
|
1991
|
-
});
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
const available = this.getAvailableMembers(pool, accountManager);
|
|
1995
|
-
if (available.length === 0) {
|
|
1996
|
-
skips.push({
|
|
1997
|
-
type: "pool_exhausted",
|
|
1998
|
-
poolName,
|
|
1999
|
-
reason: "No available members"
|
|
2000
|
-
});
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
const poolExclusions = /* @__PURE__ */ new Set();
|
|
2004
|
-
if (currentUuid) poolExclusions.add(currentUuid);
|
|
2005
|
-
while (poolExclusions.size < available.length + (currentUuid ? 1 : 0)) {
|
|
2006
|
-
const nextMember = await this.selectPreferredMember(available, poolExclusions, accountManager);
|
|
2007
|
-
if (!nextMember) break;
|
|
2008
|
-
poolExclusions.add(nextMember);
|
|
2009
|
-
if (attemptedAccounts.has(nextMember)) {
|
|
2010
|
-
skips.push({
|
|
2011
|
-
type: "account_attempted",
|
|
2012
|
-
poolName,
|
|
2013
|
-
reason: "Already attempted in this cascade",
|
|
2014
|
-
detail: nextMember
|
|
2015
|
-
});
|
|
2016
|
-
continue;
|
|
2017
|
-
}
|
|
2018
|
-
if (addedCandidateUuids.has(nextMember)) continue;
|
|
2019
|
-
candidates.push({
|
|
2020
|
-
poolName,
|
|
2021
|
-
accountUuid: nextMember,
|
|
2022
|
-
source,
|
|
2023
|
-
chainIndex
|
|
2024
|
-
});
|
|
2025
|
-
addedCandidateUuids.add(nextMember);
|
|
2026
|
-
}
|
|
2027
|
-
for (const memberUuid of available) {
|
|
2028
|
-
if (poolExclusions.has(memberUuid)) continue;
|
|
2029
|
-
if (attemptedAccounts.has(memberUuid)) {
|
|
2030
|
-
skips.push({
|
|
2031
|
-
type: "account_attempted",
|
|
2032
|
-
poolName,
|
|
2033
|
-
reason: "Already attempted in this cascade",
|
|
2034
|
-
detail: memberUuid
|
|
2035
|
-
});
|
|
2036
|
-
continue;
|
|
2037
|
-
}
|
|
2038
|
-
if (addedCandidateUuids.has(memberUuid)) continue;
|
|
2039
|
-
candidates.push({
|
|
2040
|
-
poolName,
|
|
2041
|
-
accountUuid: memberUuid,
|
|
2042
|
-
source,
|
|
2043
|
-
chainIndex
|
|
2044
|
-
});
|
|
2045
|
-
addedCandidateUuids.add(memberUuid);
|
|
2046
|
-
}
|
|
2047
|
-
};
|
|
2048
|
-
if (currentUuid) {
|
|
2049
|
-
const currentPool = this.getPoolForAccount(currentUuid);
|
|
2050
|
-
if (currentPool) {
|
|
2051
|
-
await appendPoolCandidates(currentPool.name, "pool");
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
let flattenedChainIndex = 0;
|
|
2055
|
-
for (const chain of config.chains ?? []) {
|
|
2056
|
-
if (!chain.enabled) {
|
|
2057
|
-
for (let i = 0; i < chain.entries.length; i++) {
|
|
2058
|
-
skips.push({
|
|
2059
|
-
type: "chain_disabled",
|
|
2060
|
-
poolName: chain.entries[i]?.pool ?? chain.name,
|
|
2061
|
-
reason: `Chain '${chain.name}' is disabled`
|
|
2062
|
-
});
|
|
2063
|
-
flattenedChainIndex += 1;
|
|
2064
|
-
}
|
|
2065
|
-
continue;
|
|
2066
|
-
}
|
|
2067
|
-
for (const entry of chain.entries) {
|
|
2068
|
-
if (visitedChainIndexes.has(flattenedChainIndex)) {
|
|
2069
|
-
skips.push({
|
|
2070
|
-
type: "chain_disabled",
|
|
2071
|
-
poolName: entry.pool,
|
|
2072
|
-
reason: "Chain entry already visited in this cascade",
|
|
2073
|
-
detail: `${flattenedChainIndex}`
|
|
2074
|
-
});
|
|
2075
|
-
flattenedChainIndex += 1;
|
|
2076
|
-
continue;
|
|
2077
|
-
}
|
|
2078
|
-
if (!entry.enabled) {
|
|
2079
|
-
skips.push({
|
|
2080
|
-
type: "chain_disabled",
|
|
2081
|
-
poolName: entry.pool,
|
|
2082
|
-
reason: "Chain entry is disabled",
|
|
2083
|
-
detail: `${flattenedChainIndex}`
|
|
2084
|
-
});
|
|
2085
|
-
flattenedChainIndex += 1;
|
|
2086
|
-
continue;
|
|
2087
|
-
}
|
|
2088
|
-
await appendPoolCandidates(entry.pool, "chain", flattenedChainIndex);
|
|
2089
|
-
flattenedChainIndex += 1;
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
return { candidates, skips };
|
|
2093
|
-
}
|
|
2094
|
-
isExhausted(accountUuid) {
|
|
2095
|
-
const exhaustedUntil = this.exhaustedUntilByAccount.get(accountUuid);
|
|
2096
|
-
if (!exhaustedUntil) return false;
|
|
2097
|
-
if (Date.now() >= exhaustedUntil) {
|
|
2098
|
-
this.exhaustedUntilByAccount.delete(accountUuid);
|
|
2099
|
-
return false;
|
|
2100
|
-
}
|
|
2101
|
-
return true;
|
|
2102
|
-
}
|
|
2103
|
-
clearExpiredExhausted() {
|
|
2104
|
-
const now = Date.now();
|
|
2105
|
-
for (const [accountUuid, exhaustedUntil] of this.exhaustedUntilByAccount.entries()) {
|
|
2106
|
-
if (now >= exhaustedUntil) this.exhaustedUntilByAccount.delete(accountUuid);
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
async selectPreferredMember(availableMembers, excludedMembers, accountManager) {
|
|
2110
|
-
const availableSet = new Set(availableMembers);
|
|
2111
|
-
const maxAttempts = Math.max(availableMembers.length * 2, 6);
|
|
2112
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2113
|
-
const selected = await accountManager.selectAccount();
|
|
2114
|
-
if (!selected?.uuid) continue;
|
|
2115
|
-
if (!availableSet.has(selected.uuid)) continue;
|
|
2116
|
-
if (excludedMembers.has(selected.uuid)) continue;
|
|
2117
|
-
return selected.uuid;
|
|
2118
|
-
}
|
|
2119
|
-
for (const memberUuid of availableMembers) {
|
|
2120
|
-
if (!excludedMembers.has(memberUuid)) return memberUuid;
|
|
2121
|
-
}
|
|
2122
|
-
return null;
|
|
2123
|
-
}
|
|
2124
|
-
};
|
|
2125
|
-
|
|
2126
|
-
// ../multi-account-core/src/cascade-state.ts
|
|
2127
|
-
function createCascadeState(prompt, currentAccountUuid) {
|
|
2128
|
-
const attemptedAccounts = /* @__PURE__ */ new Set();
|
|
2129
|
-
if (currentAccountUuid) {
|
|
2130
|
-
attemptedAccounts.add(currentAccountUuid);
|
|
2131
|
-
}
|
|
2132
|
-
return {
|
|
2133
|
-
prompt,
|
|
2134
|
-
attemptedAccounts,
|
|
2135
|
-
visitedChainIndexes: /* @__PURE__ */ new Set()
|
|
2136
|
-
};
|
|
2137
|
-
}
|
|
2138
|
-
var CascadeStateManager = class {
|
|
2139
|
-
suppressNextStartTurn = false;
|
|
2140
|
-
cascadeState = null;
|
|
2141
|
-
startTurn(prompt, currentAccountUuid) {
|
|
2142
|
-
if (this.suppressNextStartTurn) {
|
|
2143
|
-
this.suppressNextStartTurn = false;
|
|
2144
|
-
return this.ensureCascadeState(prompt, currentAccountUuid);
|
|
2145
|
-
}
|
|
2146
|
-
const shouldReset = !this.cascadeState || this.cascadeState.prompt !== prompt;
|
|
2147
|
-
if (shouldReset) {
|
|
2148
|
-
this.cascadeState = createCascadeState(prompt, currentAccountUuid);
|
|
2149
|
-
return this.cascadeState;
|
|
2150
|
-
}
|
|
2151
|
-
return this.ensureCascadeState(prompt, currentAccountUuid);
|
|
2152
|
-
}
|
|
2153
|
-
ensureCascadeState(prompt, currentAccountUuid) {
|
|
2154
|
-
if (!this.cascadeState || this.cascadeState.prompt !== prompt) {
|
|
2155
|
-
this.cascadeState = createCascadeState(prompt, currentAccountUuid);
|
|
2156
|
-
return this.cascadeState;
|
|
2157
|
-
}
|
|
2158
|
-
if (currentAccountUuid) {
|
|
2159
|
-
this.cascadeState.attemptedAccounts.add(currentAccountUuid);
|
|
2160
|
-
}
|
|
2161
|
-
return this.cascadeState;
|
|
2162
|
-
}
|
|
2163
|
-
markAttempted(accountUuid) {
|
|
2164
|
-
if (!this.cascadeState) return;
|
|
2165
|
-
this.cascadeState.attemptedAccounts.add(accountUuid);
|
|
2166
|
-
}
|
|
2167
|
-
markVisitedChainIndex(index) {
|
|
2168
|
-
if (!this.cascadeState) return;
|
|
2169
|
-
this.cascadeState.visitedChainIndexes.add(index);
|
|
2170
|
-
}
|
|
2171
|
-
clearCascadeState() {
|
|
2172
|
-
this.cascadeState = null;
|
|
2173
|
-
this.suppressNextStartTurn = false;
|
|
2174
|
-
}
|
|
2175
|
-
getSnapshot() {
|
|
2176
|
-
if (!this.cascadeState) return null;
|
|
2177
|
-
return {
|
|
2178
|
-
prompt: this.cascadeState.prompt,
|
|
2179
|
-
attemptedAccounts: new Set(this.cascadeState.attemptedAccounts),
|
|
2180
|
-
visitedChainIndexes: new Set(this.cascadeState.visitedChainIndexes)
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
};
|
|
10
|
+
// src/account-manager.ts
|
|
11
|
+
import { createAccountManagerForProvider } from "opencode-multi-account-core";
|
|
2184
12
|
|
|
2185
13
|
// src/constants.ts
|
|
14
|
+
import { anthropicOAuthAdapter } from "opencode-multi-account-core";
|
|
2186
15
|
var ANTHROPIC_OAUTH_ADAPTER = anthropicOAuthAdapter;
|
|
2187
16
|
var ANTHROPIC_CLIENT_ID = ANTHROPIC_OAUTH_ADAPTER.oauthClientId;
|
|
2188
17
|
var ANTHROPIC_TOKEN_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.tokenEndpoint;
|
|
@@ -2191,17 +20,17 @@ var ANTHROPIC_PROFILE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.profileEndpoint;
|
|
|
2191
20
|
var ANTHROPIC_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.requestBetaHeader;
|
|
2192
21
|
var CLAUDE_CLI_USER_AGENT = ANTHROPIC_OAUTH_ADAPTER.cliUserAgent;
|
|
2193
22
|
var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
|
|
2194
|
-
var
|
|
23
|
+
var ACCOUNTS_FILENAME = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
|
|
2195
24
|
var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
|
|
2196
25
|
var TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
2197
26
|
var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
|
|
2198
27
|
|
|
2199
28
|
// src/pi-ai-adapter.ts
|
|
2200
|
-
import { AsyncLocalStorage } from "
|
|
29
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2201
30
|
import * as piAiOauth from "@mariozechner/pi-ai/oauth";
|
|
2202
31
|
|
|
2203
32
|
// src/token-node-request.ts
|
|
2204
|
-
import * as childProcess from "
|
|
33
|
+
import * as childProcess from "child_process";
|
|
2205
34
|
function buildNodeTokenRequestScript() {
|
|
2206
35
|
return `
|
|
2207
36
|
const https = require("node:https");
|
|
@@ -2289,106 +118,126 @@ async function runNodeTokenRequest(options) {
|
|
|
2289
118
|
return await nodeTokenRequestRunner(options);
|
|
2290
119
|
}
|
|
2291
120
|
|
|
121
|
+
// src/utils.ts
|
|
122
|
+
import { setConfigGetter } from "opencode-multi-account-core";
|
|
123
|
+
|
|
2292
124
|
// src/config.ts
|
|
125
|
+
import {
|
|
126
|
+
getConfig,
|
|
127
|
+
initCoreConfig,
|
|
128
|
+
loadConfig,
|
|
129
|
+
resetConfigCache,
|
|
130
|
+
updateConfigField
|
|
131
|
+
} from "opencode-multi-account-core";
|
|
2293
132
|
initCoreConfig("claude-multiauth.json");
|
|
2294
133
|
|
|
2295
134
|
// src/utils.ts
|
|
135
|
+
import {
|
|
136
|
+
createMinimalClient,
|
|
137
|
+
debugLog,
|
|
138
|
+
formatWaitTime,
|
|
139
|
+
getAccountLabel,
|
|
140
|
+
getConfigDir,
|
|
141
|
+
getErrorCode,
|
|
142
|
+
showToast,
|
|
143
|
+
sleep
|
|
144
|
+
} from "opencode-multi-account-core";
|
|
2296
145
|
setConfigGetter(getConfig);
|
|
2297
146
|
|
|
2298
147
|
// src/usage.ts
|
|
2299
|
-
import * as
|
|
148
|
+
import * as v2 from "valibot";
|
|
2300
149
|
|
|
2301
150
|
// src/types.ts
|
|
2302
|
-
import * as
|
|
2303
|
-
var
|
|
2304
|
-
type:
|
|
2305
|
-
refresh:
|
|
2306
|
-
access:
|
|
2307
|
-
expires:
|
|
151
|
+
import * as v from "valibot";
|
|
152
|
+
var OAuthCredentialsSchema = v.object({
|
|
153
|
+
type: v.literal("oauth"),
|
|
154
|
+
refresh: v.string(),
|
|
155
|
+
access: v.string(),
|
|
156
|
+
expires: v.number()
|
|
2308
157
|
});
|
|
2309
|
-
var
|
|
2310
|
-
utilization:
|
|
2311
|
-
resets_at:
|
|
158
|
+
var UsageLimitEntrySchema = v.object({
|
|
159
|
+
utilization: v.number(),
|
|
160
|
+
resets_at: v.nullable(v.string())
|
|
2312
161
|
});
|
|
2313
|
-
var
|
|
2314
|
-
five_hour:
|
|
2315
|
-
seven_day:
|
|
2316
|
-
seven_day_sonnet:
|
|
162
|
+
var UsageLimitsSchema = v.object({
|
|
163
|
+
five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
164
|
+
seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
165
|
+
seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
|
|
2317
166
|
});
|
|
2318
|
-
var
|
|
2319
|
-
accessToken:
|
|
2320
|
-
expiresAt:
|
|
2321
|
-
refreshToken:
|
|
2322
|
-
uuid:
|
|
2323
|
-
email:
|
|
167
|
+
var CredentialRefreshPatchSchema = v.object({
|
|
168
|
+
accessToken: v.string(),
|
|
169
|
+
expiresAt: v.number(),
|
|
170
|
+
refreshToken: v.optional(v.string()),
|
|
171
|
+
uuid: v.optional(v.string()),
|
|
172
|
+
email: v.optional(v.string())
|
|
2324
173
|
});
|
|
2325
|
-
var
|
|
2326
|
-
uuid:
|
|
2327
|
-
label:
|
|
2328
|
-
email:
|
|
2329
|
-
planTier:
|
|
2330
|
-
refreshToken:
|
|
2331
|
-
accessToken:
|
|
2332
|
-
expiresAt:
|
|
2333
|
-
addedAt:
|
|
2334
|
-
lastUsed:
|
|
2335
|
-
enabled:
|
|
2336
|
-
rateLimitResetAt:
|
|
2337
|
-
cachedUsage:
|
|
2338
|
-
cachedUsageAt:
|
|
2339
|
-
consecutiveAuthFailures:
|
|
2340
|
-
isAuthDisabled:
|
|
2341
|
-
authDisabledReason:
|
|
174
|
+
var StoredAccountSchema = v.object({
|
|
175
|
+
uuid: v.optional(v.string()),
|
|
176
|
+
label: v.optional(v.string()),
|
|
177
|
+
email: v.optional(v.string()),
|
|
178
|
+
planTier: v.optional(v.string(), ""),
|
|
179
|
+
refreshToken: v.string(),
|
|
180
|
+
accessToken: v.optional(v.string()),
|
|
181
|
+
expiresAt: v.optional(v.number()),
|
|
182
|
+
addedAt: v.number(),
|
|
183
|
+
lastUsed: v.number(),
|
|
184
|
+
enabled: v.optional(v.boolean(), true),
|
|
185
|
+
rateLimitResetAt: v.optional(v.number()),
|
|
186
|
+
cachedUsage: v.optional(UsageLimitsSchema),
|
|
187
|
+
cachedUsageAt: v.optional(v.number()),
|
|
188
|
+
consecutiveAuthFailures: v.optional(v.number(), 0),
|
|
189
|
+
isAuthDisabled: v.optional(v.boolean(), false),
|
|
190
|
+
authDisabledReason: v.optional(v.string())
|
|
2342
191
|
});
|
|
2343
|
-
var
|
|
2344
|
-
version:
|
|
2345
|
-
accounts:
|
|
2346
|
-
activeAccountUuid:
|
|
192
|
+
var AccountStorageSchema = v.object({
|
|
193
|
+
version: v.literal(1),
|
|
194
|
+
accounts: v.optional(v.array(StoredAccountSchema), []),
|
|
195
|
+
activeAccountUuid: v.optional(v.string())
|
|
2347
196
|
});
|
|
2348
|
-
var TokenResponseSchema =
|
|
2349
|
-
access_token:
|
|
2350
|
-
refresh_token:
|
|
2351
|
-
expires_in:
|
|
2352
|
-
account:
|
|
2353
|
-
uuid:
|
|
2354
|
-
email_address:
|
|
197
|
+
var TokenResponseSchema = v.object({
|
|
198
|
+
access_token: v.string(),
|
|
199
|
+
refresh_token: v.optional(v.string()),
|
|
200
|
+
expires_in: v.number(),
|
|
201
|
+
account: v.optional(v.object({
|
|
202
|
+
uuid: v.optional(v.string()),
|
|
203
|
+
email_address: v.optional(v.string())
|
|
2355
204
|
}))
|
|
2356
205
|
});
|
|
2357
|
-
var
|
|
2358
|
-
var
|
|
206
|
+
var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
|
|
207
|
+
var PluginConfigSchema = v.object({
|
|
2359
208
|
/** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
|
|
2360
|
-
account_selection_strategy:
|
|
209
|
+
account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
|
|
2361
210
|
/** Use cross-process claim file to distribute parallel sessions across accounts */
|
|
2362
|
-
cross_process_claims:
|
|
211
|
+
cross_process_claims: v.optional(v.boolean(), true),
|
|
2363
212
|
/** Skip account when any usage tier utilization >= this % (100 = disabled) */
|
|
2364
|
-
soft_quota_threshold_percent:
|
|
213
|
+
soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
|
|
2365
214
|
/** Minimum backoff after rate limit (ms) */
|
|
2366
|
-
rate_limit_min_backoff_ms:
|
|
215
|
+
rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
2367
216
|
/** Default retry-after when header is missing (ms) */
|
|
2368
|
-
default_retry_after_ms:
|
|
217
|
+
default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
|
|
2369
218
|
/** Consecutive auth failures before disabling account */
|
|
2370
|
-
max_consecutive_auth_failures:
|
|
219
|
+
max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
|
|
2371
220
|
/** Backoff after token refresh failure (ms) */
|
|
2372
|
-
token_failure_backoff_ms:
|
|
221
|
+
token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
2373
222
|
/** Enable proactive background token refresh */
|
|
2374
|
-
proactive_refresh:
|
|
223
|
+
proactive_refresh: v.optional(v.boolean(), true),
|
|
2375
224
|
/** Seconds before expiry to trigger proactive refresh (default 30 min) */
|
|
2376
|
-
proactive_refresh_buffer_seconds:
|
|
225
|
+
proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
|
|
2377
226
|
/** Interval between background refresh checks in seconds (default 5 min) */
|
|
2378
|
-
proactive_refresh_interval_seconds:
|
|
227
|
+
proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
|
|
2379
228
|
/** Suppress toast notifications */
|
|
2380
|
-
quiet_mode:
|
|
229
|
+
quiet_mode: v.optional(v.boolean(), false),
|
|
2381
230
|
/** Enable debug logging */
|
|
2382
|
-
debug:
|
|
231
|
+
debug: v.optional(v.boolean(), false)
|
|
2383
232
|
});
|
|
2384
233
|
|
|
2385
234
|
// src/usage.ts
|
|
2386
235
|
var OAUTH_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.oauthBetaHeader;
|
|
2387
|
-
var ProfileResponseSchema =
|
|
2388
|
-
account:
|
|
2389
|
-
email:
|
|
2390
|
-
has_claude_pro:
|
|
2391
|
-
has_claude_max:
|
|
236
|
+
var ProfileResponseSchema = v2.object({
|
|
237
|
+
account: v2.object({
|
|
238
|
+
email: v2.optional(v2.string()),
|
|
239
|
+
has_claude_pro: v2.optional(v2.boolean(), false),
|
|
240
|
+
has_claude_max: v2.optional(v2.boolean(), false)
|
|
2392
241
|
})
|
|
2393
242
|
});
|
|
2394
243
|
async function fetchUsage(accessToken) {
|
|
@@ -2403,7 +252,7 @@ async function fetchUsage(accessToken) {
|
|
|
2403
252
|
if (!response.ok) {
|
|
2404
253
|
return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
|
|
2405
254
|
}
|
|
2406
|
-
const result =
|
|
255
|
+
const result = v2.safeParse(UsageLimitsSchema, await response.json());
|
|
2407
256
|
if (!result.success) {
|
|
2408
257
|
return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
|
|
2409
258
|
}
|
|
@@ -2425,7 +274,7 @@ async function fetchProfile(accessToken) {
|
|
|
2425
274
|
if (!response.ok) {
|
|
2426
275
|
return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
|
|
2427
276
|
}
|
|
2428
|
-
const result =
|
|
277
|
+
const result = v2.safeParse(ProfileResponseSchema, await response.json());
|
|
2429
278
|
if (!result.success) {
|
|
2430
279
|
return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
|
|
2431
280
|
}
|
|
@@ -2675,7 +524,11 @@ var AccountManager = createAccountManagerForProvider({
|
|
|
2675
524
|
refreshToken
|
|
2676
525
|
});
|
|
2677
526
|
|
|
527
|
+
// src/executor.ts
|
|
528
|
+
import { createExecutorForProvider as createExecutorForProvider2, getClearedOAuthBody } from "opencode-multi-account-core";
|
|
529
|
+
|
|
2678
530
|
// src/rate-limit.ts
|
|
531
|
+
import { createRateLimitHandlers } from "opencode-multi-account-core";
|
|
2679
532
|
var {
|
|
2680
533
|
fetchUsageLimits,
|
|
2681
534
|
getResetMsFromUsage,
|
|
@@ -2690,6 +543,7 @@ var {
|
|
|
2690
543
|
});
|
|
2691
544
|
|
|
2692
545
|
// src/pool-chain-executor.ts
|
|
546
|
+
import { createExecutorForProvider } from "opencode-multi-account-core";
|
|
2693
547
|
function buildCascadePrompt(input, init) {
|
|
2694
548
|
if (typeof init?.body === "string" && init.body.length > 0) {
|
|
2695
549
|
return init.body;
|
|
@@ -2774,7 +628,7 @@ async function executeWithPoolChainRotation(manager, runtimeFactory, poolManager
|
|
|
2774
628
|
}
|
|
2775
629
|
|
|
2776
630
|
// src/executor.ts
|
|
2777
|
-
var { executeWithAccountRotation: executeWithCoreAccountRotation } =
|
|
631
|
+
var { executeWithAccountRotation: executeWithCoreAccountRotation } = createExecutorForProvider2("Anthropic", {
|
|
2778
632
|
handleRateLimitResponse: async (manager, client, account, response) => handleRateLimitResponse(
|
|
2779
633
|
manager,
|
|
2780
634
|
client,
|
|
@@ -2799,8 +653,8 @@ async function clearAuthIfNoUsableAccount(manager, client) {
|
|
|
2799
653
|
}).catch(() => {
|
|
2800
654
|
});
|
|
2801
655
|
}
|
|
2802
|
-
function hasPoolChainEntries(
|
|
2803
|
-
return (
|
|
656
|
+
function hasPoolChainEntries(config2) {
|
|
657
|
+
return (config2.pools?.length ?? 0) > 0 || (config2.chains?.length ?? 0) > 0;
|
|
2804
658
|
}
|
|
2805
659
|
async function executeWithAccountRotation(manager, runtimeFactory, client, input, init, options) {
|
|
2806
660
|
try {
|
|
@@ -2825,6 +679,21 @@ async function executeWithAccountRotation(manager, runtimeFactory, client, input
|
|
|
2825
679
|
}
|
|
2826
680
|
}
|
|
2827
681
|
|
|
682
|
+
// src/ui/ansi.ts
|
|
683
|
+
import {
|
|
684
|
+
ANSI,
|
|
685
|
+
isTTY,
|
|
686
|
+
parseKey
|
|
687
|
+
} from "opencode-multi-account-core";
|
|
688
|
+
|
|
689
|
+
// src/ui/select.ts
|
|
690
|
+
import {
|
|
691
|
+
select
|
|
692
|
+
} from "opencode-multi-account-core";
|
|
693
|
+
|
|
694
|
+
// src/ui/confirm.ts
|
|
695
|
+
import { confirm } from "opencode-multi-account-core";
|
|
696
|
+
|
|
2828
697
|
// src/ui/auth-menu.ts
|
|
2829
698
|
function formatRelativeTime(timestamp) {
|
|
2830
699
|
if (!timestamp) return "never";
|
|
@@ -3030,12 +899,16 @@ function printQuotaError(account, error) {
|
|
|
3030
899
|
}
|
|
3031
900
|
|
|
3032
901
|
// src/account-store.ts
|
|
3033
|
-
|
|
902
|
+
import {
|
|
903
|
+
AccountStore,
|
|
904
|
+
setAccountsFilename
|
|
905
|
+
} from "opencode-multi-account-core";
|
|
906
|
+
setAccountsFilename(ACCOUNTS_FILENAME);
|
|
3034
907
|
|
|
3035
908
|
// src/auth-handler.ts
|
|
3036
|
-
import { randomUUID
|
|
3037
|
-
import { createInterface } from "
|
|
3038
|
-
import { exec } from "
|
|
909
|
+
import { randomUUID } from "crypto";
|
|
910
|
+
import { createInterface } from "readline";
|
|
911
|
+
import { exec } from "child_process";
|
|
3039
912
|
function makeFailedFlowResult(message) {
|
|
3040
913
|
return {
|
|
3041
914
|
url: "",
|
|
@@ -3175,7 +1048,7 @@ async function persistFallback(auth) {
|
|
|
3175
1048
|
const store = new AccountStore();
|
|
3176
1049
|
const now = Date.now();
|
|
3177
1050
|
const account = {
|
|
3178
|
-
uuid:
|
|
1051
|
+
uuid: randomUUID(),
|
|
3179
1052
|
refreshToken: auth.refresh,
|
|
3180
1053
|
accessToken: auth.access,
|
|
3181
1054
|
expiresAt: auth.expires,
|
|
@@ -3352,7 +1225,122 @@ Retrying authentication for ${label}...
|
|
|
3352
1225
|
}
|
|
3353
1226
|
|
|
3354
1227
|
// src/request-transform.ts
|
|
3355
|
-
import { createHash } from "
|
|
1228
|
+
import { createHash } from "crypto";
|
|
1229
|
+
|
|
1230
|
+
// src/model-config.ts
|
|
1231
|
+
function splitBetaFlags(value) {
|
|
1232
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1233
|
+
}
|
|
1234
|
+
var config = {
|
|
1235
|
+
ccVersion: ANTHROPIC_OAUTH_ADAPTER.cliVersion,
|
|
1236
|
+
baseBetas: splitBetaFlags(ANTHROPIC_OAUTH_ADAPTER.requestBetaHeader),
|
|
1237
|
+
longContextBetas: ["context-1m-2025-08-07", "interleaved-thinking-2025-05-14"],
|
|
1238
|
+
modelOverrides: {
|
|
1239
|
+
"4-6": {
|
|
1240
|
+
add: ["effort-2025-11-24"]
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
function getCliVersion() {
|
|
1245
|
+
return process.env.ANTHROPIC_CLI_VERSION ?? config.ccVersion;
|
|
1246
|
+
}
|
|
1247
|
+
function getUserAgent() {
|
|
1248
|
+
if (process.env.ANTHROPIC_USER_AGENT) {
|
|
1249
|
+
return process.env.ANTHROPIC_USER_AGENT;
|
|
1250
|
+
}
|
|
1251
|
+
if (process.env.ANTHROPIC_CLI_VERSION) {
|
|
1252
|
+
return `claude-cli/${getCliVersion()} (external, cli)`;
|
|
1253
|
+
}
|
|
1254
|
+
return ANTHROPIC_OAUTH_ADAPTER.cliUserAgent;
|
|
1255
|
+
}
|
|
1256
|
+
function getRequiredBetas() {
|
|
1257
|
+
return splitBetaFlags(process.env.ANTHROPIC_BETA_FLAGS ?? config.baseBetas.join(","));
|
|
1258
|
+
}
|
|
1259
|
+
function getModelOverride(modelId) {
|
|
1260
|
+
const lowerModelId = modelId.toLowerCase();
|
|
1261
|
+
for (const [pattern, override] of Object.entries(config.modelOverrides)) {
|
|
1262
|
+
if (lowerModelId.includes(pattern)) {
|
|
1263
|
+
return override;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/betas.ts
|
|
1270
|
+
var LONG_CONTEXT_BETAS = config.longContextBetas;
|
|
1271
|
+
var excludedBetas = /* @__PURE__ */ new Map();
|
|
1272
|
+
var lastBetaFlagsEnv = process.env.ANTHROPIC_BETA_FLAGS;
|
|
1273
|
+
var lastModelId;
|
|
1274
|
+
function getExcludedBetas(modelId) {
|
|
1275
|
+
const currentBetaFlags = process.env.ANTHROPIC_BETA_FLAGS;
|
|
1276
|
+
if (currentBetaFlags !== lastBetaFlagsEnv) {
|
|
1277
|
+
excludedBetas.clear();
|
|
1278
|
+
lastBetaFlagsEnv = currentBetaFlags;
|
|
1279
|
+
}
|
|
1280
|
+
if (lastModelId !== void 0 && lastModelId !== modelId) {
|
|
1281
|
+
excludedBetas.clear();
|
|
1282
|
+
}
|
|
1283
|
+
lastModelId = modelId;
|
|
1284
|
+
return excludedBetas.get(modelId) ?? /* @__PURE__ */ new Set();
|
|
1285
|
+
}
|
|
1286
|
+
function addExcludedBeta(modelId, beta) {
|
|
1287
|
+
const nextExcludedBetas = excludedBetas.get(modelId) ?? /* @__PURE__ */ new Set();
|
|
1288
|
+
nextExcludedBetas.add(beta);
|
|
1289
|
+
excludedBetas.set(modelId, nextExcludedBetas);
|
|
1290
|
+
}
|
|
1291
|
+
function isLongContextError(responseBody) {
|
|
1292
|
+
return responseBody.includes("Extra usage is required for long context requests") || responseBody.includes("long context beta is not yet available");
|
|
1293
|
+
}
|
|
1294
|
+
function getNextBetaToExclude(modelId) {
|
|
1295
|
+
const excluded = getExcludedBetas(modelId);
|
|
1296
|
+
for (const beta of LONG_CONTEXT_BETAS) {
|
|
1297
|
+
if (!excluded.has(beta)) {
|
|
1298
|
+
return beta;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
function supports1mContext(modelId) {
|
|
1304
|
+
const lowerModelId = modelId.toLowerCase();
|
|
1305
|
+
if (!lowerModelId.includes("opus") && !lowerModelId.includes("sonnet")) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
const versionMatch = lowerModelId.match(/(opus|sonnet)-(\d+)-(\d+)/);
|
|
1309
|
+
if (!versionMatch) {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
const major = Number.parseInt(versionMatch[2] ?? "0", 10);
|
|
1313
|
+
const minor = Number.parseInt(versionMatch[3] ?? "0", 10);
|
|
1314
|
+
const effectiveMinor = minor > 99 ? 0 : minor;
|
|
1315
|
+
return major > 4 || major === 4 && effectiveMinor >= 6;
|
|
1316
|
+
}
|
|
1317
|
+
function getModelBetas(modelId, excluded) {
|
|
1318
|
+
const betas = [...getRequiredBetas()];
|
|
1319
|
+
const longContextBeta = config.longContextBetas[0];
|
|
1320
|
+
if (longContextBeta && process.env.ANTHROPIC_ENABLE_1M_CONTEXT === "true" && supports1mContext(modelId)) {
|
|
1321
|
+
betas.push(longContextBeta);
|
|
1322
|
+
}
|
|
1323
|
+
const override = getModelOverride(modelId);
|
|
1324
|
+
if (override?.exclude) {
|
|
1325
|
+
for (const excludedBeta of override.exclude) {
|
|
1326
|
+
const index = betas.indexOf(excludedBeta);
|
|
1327
|
+
if (index !== -1) {
|
|
1328
|
+
betas.splice(index, 1);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (override?.add) {
|
|
1333
|
+
for (const addedBeta of override.add) {
|
|
1334
|
+
if (!betas.includes(addedBeta)) {
|
|
1335
|
+
betas.push(addedBeta);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (!excluded || excluded.size === 0) {
|
|
1340
|
+
return betas;
|
|
1341
|
+
}
|
|
1342
|
+
return betas.filter((beta) => !excluded.has(beta));
|
|
1343
|
+
}
|
|
3356
1344
|
|
|
3357
1345
|
// src/anthropic-prompt.ts
|
|
3358
1346
|
var SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
@@ -3565,7 +1553,7 @@ function processCompleteLines(buffer) {
|
|
|
3565
1553
|
`;
|
|
3566
1554
|
return { output, remaining };
|
|
3567
1555
|
}
|
|
3568
|
-
function buildRequestHeaders(input, init, accessToken) {
|
|
1556
|
+
function buildRequestHeaders(input, init, accessToken, modelId = "unknown", excludedBetas2) {
|
|
3569
1557
|
const headers = new Headers();
|
|
3570
1558
|
if (input instanceof Request) {
|
|
3571
1559
|
input.headers.forEach((value, key) => {
|
|
@@ -3588,13 +1576,14 @@ function buildRequestHeaders(input, init, accessToken) {
|
|
|
3588
1576
|
}
|
|
3589
1577
|
}
|
|
3590
1578
|
const incomingBetas = (headers.get("anthropic-beta") || "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
1579
|
+
const modelBetas = getModelBetas(modelId, excludedBetas2);
|
|
3591
1580
|
const mergedBetas = [.../* @__PURE__ */ new Set([
|
|
3592
|
-
...
|
|
1581
|
+
...modelBetas,
|
|
3593
1582
|
...incomingBetas
|
|
3594
1583
|
])].join(",");
|
|
3595
1584
|
headers.set("authorization", `Bearer ${accessToken}`);
|
|
3596
1585
|
headers.set("anthropic-beta", mergedBetas);
|
|
3597
|
-
headers.set("user-agent",
|
|
1586
|
+
headers.set("user-agent", getUserAgent());
|
|
3598
1587
|
headers.set("anthropic-dangerous-direct-browser-access", "true");
|
|
3599
1588
|
headers.set("x-app", "cli");
|
|
3600
1589
|
headers.delete("x-api-key");
|
|
@@ -3639,6 +1628,17 @@ function transformRequestBody(body) {
|
|
|
3639
1628
|
return body;
|
|
3640
1629
|
}
|
|
3641
1630
|
}
|
|
1631
|
+
function extractModelIdFromBody(body) {
|
|
1632
|
+
if (typeof body !== "string") {
|
|
1633
|
+
return "unknown";
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
const parsed = JSON.parse(body);
|
|
1637
|
+
return parsed.model ?? "unknown";
|
|
1638
|
+
} catch {
|
|
1639
|
+
return "unknown";
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
3642
1642
|
function transformRequestUrl(input) {
|
|
3643
1643
|
let url = null;
|
|
3644
1644
|
try {
|
|
@@ -3705,6 +1705,7 @@ function createResponseStreamTransform(response) {
|
|
|
3705
1705
|
}
|
|
3706
1706
|
|
|
3707
1707
|
// src/proactive-refresh.ts
|
|
1708
|
+
import { createProactiveRefreshQueueForProvider } from "opencode-multi-account-core";
|
|
3708
1709
|
var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
|
|
3709
1710
|
providerAuthId: "anthropic",
|
|
3710
1711
|
getConfig,
|
|
@@ -3714,12 +1715,15 @@ var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
|
|
|
3714
1715
|
});
|
|
3715
1716
|
|
|
3716
1717
|
// src/runtime-factory.ts
|
|
1718
|
+
import { TokenRefreshError } from "opencode-multi-account-core";
|
|
3717
1719
|
var TOKEN_REFRESH_PERMANENT_FAILURE_STATUS = 401;
|
|
3718
1720
|
var AccountRuntimeFactory = class {
|
|
3719
1721
|
constructor(store, client) {
|
|
3720
1722
|
this.store = store;
|
|
3721
1723
|
this.client = client;
|
|
3722
1724
|
}
|
|
1725
|
+
store;
|
|
1726
|
+
client;
|
|
3723
1727
|
runtimes = /* @__PURE__ */ new Map();
|
|
3724
1728
|
initLocks = /* @__PURE__ */ new Map();
|
|
3725
1729
|
async getRuntime(uuid) {
|
|
@@ -3775,13 +1779,41 @@ var AccountRuntimeFactory = class {
|
|
|
3775
1779
|
}
|
|
3776
1780
|
async executeTransformedFetch(input, init, accessToken) {
|
|
3777
1781
|
const transformedInput = transformRequestUrl(input);
|
|
3778
|
-
const
|
|
1782
|
+
const modelId = extractModelIdFromBody(init?.body);
|
|
1783
|
+
const excludedBetas2 = getExcludedBetas(modelId);
|
|
1784
|
+
const headers = buildRequestHeaders(transformedInput, init, accessToken, modelId, excludedBetas2);
|
|
3779
1785
|
const transformedBody = typeof init?.body === "string" ? transformRequestBody(init.body) : init?.body;
|
|
3780
|
-
|
|
1786
|
+
let response = await fetch(transformedInput, {
|
|
3781
1787
|
...init,
|
|
3782
1788
|
headers,
|
|
3783
1789
|
body: transformedBody
|
|
3784
1790
|
});
|
|
1791
|
+
for (let attempt = 0; attempt < LONG_CONTEXT_BETAS.length; attempt += 1) {
|
|
1792
|
+
if (response.status !== 400 && response.status !== 429) {
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
const responseBody = await response.clone().text();
|
|
1796
|
+
if (!isLongContextError(responseBody)) {
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
const betaToExclude = getNextBetaToExclude(modelId);
|
|
1800
|
+
if (!betaToExclude) {
|
|
1801
|
+
break;
|
|
1802
|
+
}
|
|
1803
|
+
addExcludedBeta(modelId, betaToExclude);
|
|
1804
|
+
const retryHeaders = buildRequestHeaders(
|
|
1805
|
+
transformedInput,
|
|
1806
|
+
init,
|
|
1807
|
+
accessToken,
|
|
1808
|
+
modelId,
|
|
1809
|
+
getExcludedBetas(modelId)
|
|
1810
|
+
);
|
|
1811
|
+
response = await fetch(transformedInput, {
|
|
1812
|
+
...init,
|
|
1813
|
+
headers: retryHeaders,
|
|
1814
|
+
body: transformedBody
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
3785
1817
|
return createResponseStreamTransform(response);
|
|
3786
1818
|
}
|
|
3787
1819
|
async createRuntime(uuid) {
|
|
@@ -3806,7 +1838,91 @@ var AccountRuntimeFactory = class {
|
|
|
3806
1838
|
}
|
|
3807
1839
|
};
|
|
3808
1840
|
|
|
1841
|
+
// src/bootstrap-auth.ts
|
|
1842
|
+
import { promises as fs } from "fs";
|
|
1843
|
+
import { join } from "path";
|
|
1844
|
+
import { getConfigDir as getConfigDir2 } from "opencode-multi-account-core";
|
|
1845
|
+
var AUTH_JSON_FILENAME = "auth.json";
|
|
1846
|
+
function hasCompleteOAuthCredential(account) {
|
|
1847
|
+
return typeof account.refreshToken === "string" && account.refreshToken.length > 0 && typeof account.accessToken === "string" && account.accessToken.length > 0 && typeof account.expiresAt === "number" && Number.isFinite(account.expiresAt);
|
|
1848
|
+
}
|
|
1849
|
+
function selectBootstrapAccount(accounts, activeAccountUuid) {
|
|
1850
|
+
const completeAccounts = accounts.filter(hasCompleteOAuthCredential);
|
|
1851
|
+
if (completeAccounts.length === 0) {
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
const activeAccount = activeAccountUuid ? completeAccounts.find((account) => account.uuid === activeAccountUuid) : void 0;
|
|
1855
|
+
if (activeAccount) {
|
|
1856
|
+
return activeAccount;
|
|
1857
|
+
}
|
|
1858
|
+
const firstUsableAccount = completeAccounts.find(
|
|
1859
|
+
(account) => account.enabled !== false && account.isAuthDisabled !== true
|
|
1860
|
+
);
|
|
1861
|
+
return firstUsableAccount ?? completeAccounts[0] ?? null;
|
|
1862
|
+
}
|
|
1863
|
+
async function readCurrentAuth(providerId) {
|
|
1864
|
+
const authPath = join(getConfigDir2(), AUTH_JSON_FILENAME);
|
|
1865
|
+
let raw;
|
|
1866
|
+
try {
|
|
1867
|
+
raw = await fs.readFile(authPath, "utf-8");
|
|
1868
|
+
} catch {
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
const parsed = JSON.parse(raw);
|
|
1873
|
+
const providerAuth = parsed[providerId];
|
|
1874
|
+
if (providerAuth?.type !== "oauth" || typeof providerAuth.refresh !== "string" || typeof providerAuth.access !== "string" || typeof providerAuth.expires !== "number") {
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
type: "oauth",
|
|
1879
|
+
refresh: providerAuth.refresh,
|
|
1880
|
+
access: providerAuth.access,
|
|
1881
|
+
expires: providerAuth.expires
|
|
1882
|
+
};
|
|
1883
|
+
} catch {
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function shouldSyncBootstrapAuth(currentAuth, nextAuth) {
|
|
1888
|
+
if (!currentAuth) {
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
if (currentAuth.refresh === nextAuth.refresh && currentAuth.access === nextAuth.access && currentAuth.expires === nextAuth.expires) {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
return currentAuth.expires < nextAuth.expires;
|
|
1895
|
+
}
|
|
1896
|
+
async function syncBootstrapAuth(client, store) {
|
|
1897
|
+
const storage = await store.load();
|
|
1898
|
+
const bootstrapAccount = selectBootstrapAccount(storage.accounts, storage.activeAccountUuid);
|
|
1899
|
+
if (!bootstrapAccount) {
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
const nextAuth = {
|
|
1903
|
+
type: "oauth",
|
|
1904
|
+
refresh: bootstrapAccount.refreshToken,
|
|
1905
|
+
access: bootstrapAccount.accessToken,
|
|
1906
|
+
expires: bootstrapAccount.expiresAt
|
|
1907
|
+
};
|
|
1908
|
+
const currentAuth = await readCurrentAuth(ANTHROPIC_OAUTH_ADAPTER.authProviderId);
|
|
1909
|
+
if (!shouldSyncBootstrapAuth(currentAuth, nextAuth)) {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
await client.auth.set({
|
|
1913
|
+
path: { id: ANTHROPIC_OAUTH_ADAPTER.authProviderId },
|
|
1914
|
+
body: nextAuth
|
|
1915
|
+
});
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
3809
1919
|
// src/index.ts
|
|
1920
|
+
var EMPTY_OAUTH_CREDENTIALS = {
|
|
1921
|
+
type: "oauth",
|
|
1922
|
+
refresh: "",
|
|
1923
|
+
access: "",
|
|
1924
|
+
expires: 0
|
|
1925
|
+
};
|
|
3810
1926
|
function extractFirstUserText(input) {
|
|
3811
1927
|
try {
|
|
3812
1928
|
const raw = input;
|
|
@@ -3839,12 +1955,65 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
|
|
|
3839
1955
|
const { client } = ctx;
|
|
3840
1956
|
await loadConfig();
|
|
3841
1957
|
const store = new AccountStore();
|
|
1958
|
+
await syncBootstrapAuth(client, store).catch(() => {
|
|
1959
|
+
});
|
|
3842
1960
|
let manager = null;
|
|
3843
1961
|
let runtimeFactory = null;
|
|
3844
1962
|
let refreshQueue = null;
|
|
3845
1963
|
let poolManager = null;
|
|
3846
1964
|
let cascadeStateManager = null;
|
|
3847
1965
|
let poolChainConfig = { pools: [], chains: [] };
|
|
1966
|
+
async function ensureExecutionInfrastructure() {
|
|
1967
|
+
runtimeFactory ??= new AccountRuntimeFactory(store, client);
|
|
1968
|
+
poolChainConfig = await loadPoolChainConfig();
|
|
1969
|
+
poolManager ??= new PoolManager();
|
|
1970
|
+
poolManager.loadPools(poolChainConfig.pools);
|
|
1971
|
+
cascadeStateManager ??= new CascadeStateManager();
|
|
1972
|
+
if (manager) {
|
|
1973
|
+
manager.setRuntimeFactory(runtimeFactory);
|
|
1974
|
+
manager.setClient(client);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async function startRefreshQueueIfNeeded() {
|
|
1978
|
+
if (!manager || manager.getAccountCount() === 0) {
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
await ensureExecutionInfrastructure();
|
|
1982
|
+
if (refreshQueue) {
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
refreshQueue = new ProactiveRefreshQueue(
|
|
1986
|
+
client,
|
|
1987
|
+
store,
|
|
1988
|
+
(uuid) => {
|
|
1989
|
+
runtimeFactory?.invalidate(uuid);
|
|
1990
|
+
void manager?.refresh();
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
refreshQueue.start();
|
|
1994
|
+
}
|
|
1995
|
+
async function initializeManagerFromStore() {
|
|
1996
|
+
if (manager) {
|
|
1997
|
+
return manager.getAccountCount() > 0;
|
|
1998
|
+
}
|
|
1999
|
+
const storage = await store.load();
|
|
2000
|
+
if (storage.accounts.length === 0) {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
manager = await AccountManager.create(store, EMPTY_OAUTH_CREDENTIALS, client);
|
|
2004
|
+
await ensureExecutionInfrastructure();
|
|
2005
|
+
await startRefreshQueueIfNeeded();
|
|
2006
|
+
return manager.getAccountCount() > 0;
|
|
2007
|
+
}
|
|
2008
|
+
async function initializeManagerFromAuth(credentials) {
|
|
2009
|
+
if (!manager) {
|
|
2010
|
+
manager = await AccountManager.create(store, credentials, client);
|
|
2011
|
+
}
|
|
2012
|
+
await ensureExecutionInfrastructure();
|
|
2013
|
+
await startRefreshQueueIfNeeded();
|
|
2014
|
+
}
|
|
2015
|
+
await initializeManagerFromStore().catch(() => {
|
|
2016
|
+
});
|
|
3848
2017
|
return {
|
|
3849
2018
|
"experimental.chat.system.transform": (input, output) => {
|
|
3850
2019
|
injectSystemPrompt(output);
|
|
@@ -3932,22 +2101,21 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
|
|
|
3932
2101
|
}
|
|
3933
2102
|
const credentials = auth;
|
|
3934
2103
|
await migrateFromAuthJson("anthropic", store);
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
const activeLabel = manager.getActiveAccount() ? getAccountLabel(manager.getActiveAccount()) : "none";
|
|
2104
|
+
await initializeManagerFromAuth(credentials);
|
|
2105
|
+
const initializedManager = manager;
|
|
2106
|
+
if (!initializedManager) {
|
|
2107
|
+
return { apiKey: "", fetch };
|
|
2108
|
+
}
|
|
2109
|
+
if (initializedManager.getAccountCount() > 0) {
|
|
2110
|
+
const activeAccount = initializedManager.getActiveAccount();
|
|
2111
|
+
const activeLabel = activeAccount ? getAccountLabel(activeAccount) : "none";
|
|
3944
2112
|
void showToast(
|
|
3945
2113
|
client,
|
|
3946
|
-
`Multi-Auth: ${
|
|
2114
|
+
`Multi-Auth: ${initializedManager.getAccountCount()} account(s) loaded. Active: ${activeLabel}`,
|
|
3947
2115
|
"info"
|
|
3948
2116
|
);
|
|
3949
|
-
await
|
|
3950
|
-
const disabledCount =
|
|
2117
|
+
await initializedManager.validateNonActiveTokens(client);
|
|
2118
|
+
const disabledCount = initializedManager.getAccounts().filter((a) => a.isAuthDisabled).length;
|
|
3951
2119
|
if (disabledCount > 0) {
|
|
3952
2120
|
void showToast(
|
|
3953
2121
|
client,
|
|
@@ -3955,32 +2123,20 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
|
|
|
3955
2123
|
"warning"
|
|
3956
2124
|
);
|
|
3957
2125
|
}
|
|
3958
|
-
if (refreshQueue) {
|
|
3959
|
-
await refreshQueue.stop();
|
|
3960
|
-
}
|
|
3961
|
-
refreshQueue = new ProactiveRefreshQueue(
|
|
3962
|
-
client,
|
|
3963
|
-
store,
|
|
3964
|
-
(uuid) => {
|
|
3965
|
-
runtimeFactory?.invalidate(uuid);
|
|
3966
|
-
void manager?.refresh();
|
|
3967
|
-
}
|
|
3968
|
-
);
|
|
3969
|
-
refreshQueue.start();
|
|
3970
2126
|
}
|
|
3971
2127
|
return {
|
|
3972
2128
|
apiKey: "",
|
|
3973
2129
|
"chat.headers": async (input, output) => {
|
|
3974
2130
|
if (input.provider?.info?.id !== ANTHROPIC_OAUTH_ADAPTER.authProviderId) return;
|
|
3975
|
-
output.headers["user-agent"] =
|
|
2131
|
+
output.headers["user-agent"] = getUserAgent();
|
|
3976
2132
|
output.headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER;
|
|
3977
2133
|
output.headers["x-app"] = "cli";
|
|
3978
2134
|
},
|
|
3979
2135
|
async fetch(input, init) {
|
|
3980
|
-
if (!
|
|
2136
|
+
if (!initializedManager || !runtimeFactory) {
|
|
3981
2137
|
return fetch(input, init);
|
|
3982
2138
|
}
|
|
3983
|
-
if (
|
|
2139
|
+
if (initializedManager.getAccountCount() === 0) {
|
|
3984
2140
|
throw new Error(
|
|
3985
2141
|
"No Anthropic accounts configured. Run `opencode auth login` to add an account."
|
|
3986
2142
|
);
|
|
@@ -3991,7 +2147,7 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
|
|
|
3991
2147
|
cascadeStateManager = new CascadeStateManager();
|
|
3992
2148
|
}
|
|
3993
2149
|
return executeWithAccountRotation(
|
|
3994
|
-
|
|
2150
|
+
initializedManager,
|
|
3995
2151
|
runtimeFactory,
|
|
3996
2152
|
client,
|
|
3997
2153
|
input,
|
|
@@ -4011,3 +2167,4 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
|
|
|
4011
2167
|
export {
|
|
4012
2168
|
ClaudeMultiAuthPlugin
|
|
4013
2169
|
};
|
|
2170
|
+
//# sourceMappingURL=index.js.map
|