opencode-anthropic-multi-account 0.2.6 → 0.2.9

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.js CHANGED
@@ -1,2238 +1,36 @@
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
- // ../multi-account-core/src/account-manager.ts
5
- import { randomUUID } from "node:crypto";
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 ANTHROPIC_DEFAULT_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1824
- var ANTHROPIC_DEFAULT_CLI_VERSION = "2.1.80";
1825
- var ANTHROPIC_DEFAULT_USER_AGENT = "claude-cli/2.1.2 (external, cli)";
1826
- var ANTHROPIC_DEFAULT_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
1827
- var ANTHROPIC_DEFAULT_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
1828
- var ANTHROPIC_DEFAULT_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
1829
- var ANTHROPIC_DEFAULT_SCOPES = "org:create_api_key user:profile user:inference";
1830
- var ANTHROPIC_DEFAULT_BETA_FLAGS = "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,prompt-caching-scope-2026-01-05";
1831
- function buildCliUserAgent(cliVersion) {
1832
- return `claude-cli/${cliVersion} (external, cli)`;
1833
- }
1834
- function resolveAnthropicOAuthEnv(env = process.env) {
1835
- const cliVersion = env.ANTHROPIC_CLI_VERSION || ANTHROPIC_DEFAULT_CLI_VERSION;
1836
- const composedUserAgent = buildCliUserAgent(cliVersion);
1837
- const userAgent = env.ANTHROPIC_USER_AGENT || (env.ANTHROPIC_CLI_VERSION ? composedUserAgent : "") || ANTHROPIC_DEFAULT_USER_AGENT;
1838
- return {
1839
- clientId: env.ANTHROPIC_CLIENT_ID || ANTHROPIC_DEFAULT_CLIENT_ID,
1840
- cliVersion,
1841
- userAgent,
1842
- authorizeUrl: env.ANTHROPIC_AUTHORIZE_URL || ANTHROPIC_DEFAULT_AUTHORIZE_URL,
1843
- tokenUrl: env.ANTHROPIC_TOKEN_URL || ANTHROPIC_DEFAULT_TOKEN_URL,
1844
- redirectUri: env.ANTHROPIC_REDIRECT_URI || ANTHROPIC_DEFAULT_REDIRECT_URI,
1845
- scopes: env.ANTHROPIC_SCOPES || ANTHROPIC_DEFAULT_SCOPES,
1846
- betaFlags: env.ANTHROPIC_BETA_FLAGS || ANTHROPIC_DEFAULT_BETA_FLAGS
1847
- };
1848
- }
1849
- var anthropicEnv = resolveAnthropicOAuthEnv();
1850
- var anthropicOAuthAdapter = {
1851
- id: "anthropic",
1852
- authProviderId: "anthropic",
1853
- modelDisplayName: "Claude",
1854
- statusToolName: "claude_multiauth_status",
1855
- authMethodLabel: "Claude Pro/Max (Multi-Auth)",
1856
- serviceLogName: "claude-multiauth",
1857
- oauthClientId: anthropicEnv.clientId,
1858
- tokenEndpoint: anthropicEnv.tokenUrl,
1859
- usageEndpoint: "https://api.anthropic.com/api/oauth/usage",
1860
- profileEndpoint: "https://api.anthropic.com/api/oauth/profile",
1861
- oauthBetaHeader: "oauth-2025-04-20",
1862
- requestBetaHeader: anthropicEnv.betaFlags,
1863
- cliUserAgent: anthropicEnv.userAgent,
1864
- cliVersion: anthropicEnv.cliVersion,
1865
- billingSalt: "59cf53e54c78",
1866
- toolPrefix: "mcp_",
1867
- accountStorageFilename: "anthropic-multi-account-accounts.json",
1868
- transform: {
1869
- rewriteOpenCodeBranding: true,
1870
- addToolPrefix: true,
1871
- stripToolPrefixInResponse: true,
1872
- enableMessagesBetaQuery: true
1873
- },
1874
- planLabels: {
1875
- max: "Claude Max",
1876
- pro: "Claude Pro",
1877
- free: "Free"
1878
- },
1879
- supported: true
1880
- };
1881
-
1882
- // ../multi-account-core/src/pool-types.ts
1883
- import * as v5 from "valibot";
1884
- var PoolConfigSchema = v5.object({
1885
- name: v5.string(),
1886
- baseProvider: v5.string(),
1887
- members: v5.array(v5.string()),
1888
- enabled: v5.boolean()
1889
- });
1890
- var ChainEntryConfigSchema = v5.object({
1891
- pool: v5.string(),
1892
- model: v5.optional(v5.string()),
1893
- enabled: v5.boolean()
1894
- });
1895
- var ChainConfigSchema = v5.object({
1896
- name: v5.string(),
1897
- entries: v5.array(ChainEntryConfigSchema),
1898
- enabled: v5.boolean()
1899
- });
1900
- var PoolChainConfigSchema = v5.object({
1901
- pools: v5.optional(v5.array(PoolConfigSchema), []),
1902
- chains: v5.optional(v5.array(ChainConfigSchema), [])
1903
- });
1904
-
1905
- // ../multi-account-core/src/pool-config-store.ts
1906
- import { promises as fs6 } from "node:fs";
1907
- import { dirname as dirname5, join as join7 } from "node:path";
1908
- import lockfile2 from "proper-lockfile";
1909
- import * as v6 from "valibot";
1910
- var POOL_CONFIG_FILENAME = "multiauth-pools.json";
1911
- function createEmptyConfig() {
1912
- return { pools: [], chains: [] };
1913
- }
1914
- function getGlobalConfigPath() {
1915
- return join7(getConfigDir2(), POOL_CONFIG_FILENAME);
1916
- }
1917
- async function resolveConfigPath() {
1918
- const projectPath = join7(process.cwd(), ".opencode", POOL_CONFIG_FILENAME);
1919
- try {
1920
- await fs6.access(projectPath);
1921
- return projectPath;
1922
- } catch {
1923
- }
1924
- return getGlobalConfigPath();
1925
- }
1926
- function parsePoolChainConfig(content) {
1927
- let parsed;
1928
- try {
1929
- parsed = JSON.parse(content);
1930
- } catch {
1931
- return null;
1932
- }
1933
- const validation = v6.safeParse(PoolChainConfigSchema, parsed);
1934
- return validation.success ? validation.output : null;
1935
- }
1936
- async function loadPoolChainConfig() {
1937
- const path = await resolveConfigPath();
1938
- try {
1939
- const content = await fs6.readFile(path, "utf-8");
1940
- return parsePoolChainConfig(content) ?? createEmptyConfig();
1941
- } catch {
1942
- return createEmptyConfig();
1943
- }
1944
- }
1945
-
1946
- // ../multi-account-core/src/pool-manager.ts
1947
- var DEFAULT_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1e3;
1948
- var PoolManager = class {
1949
- poolsByName = /* @__PURE__ */ new Map();
1950
- exhaustedUntilByAccount = /* @__PURE__ */ new Map();
1951
- exhaustedCooldownMs;
1952
- constructor(options) {
1953
- this.exhaustedCooldownMs = options?.exhaustedCooldownMs ?? DEFAULT_EXHAUSTED_COOLDOWN_MS;
1954
- }
1955
- loadPools(configs) {
1956
- this.poolsByName.clear();
1957
- for (const pool of configs) {
1958
- this.poolsByName.set(pool.name, pool);
1959
- }
1960
- }
1961
- getPoolForAccount(accountUuid) {
1962
- for (const pool of this.poolsByName.values()) {
1963
- if (!pool.enabled) continue;
1964
- if (pool.members.includes(accountUuid)) return pool;
1965
- }
1966
- return null;
1967
- }
1968
- getAvailableMembers(pool, accountManager) {
1969
- if (!pool.enabled) return [];
1970
- this.clearExpiredExhausted();
1971
- const accountsByUuid = /* @__PURE__ */ new Map();
1972
- for (const account of accountManager.getAccounts()) {
1973
- if (!account.uuid) continue;
1974
- accountsByUuid.set(account.uuid, account);
1975
- }
1976
- return pool.members.filter((accountUuid) => {
1977
- const account = accountsByUuid.get(accountUuid);
1978
- if (!account) return false;
1979
- if (!account.enabled || account.isAuthDisabled) return false;
1980
- if (this.isExhausted(accountUuid)) return false;
1981
- if (accountManager.isRateLimited(account)) return false;
1982
- return true;
1983
- });
1984
- }
1985
- markExhausted(accountUuid) {
1986
- this.exhaustedUntilByAccount.set(accountUuid, Date.now() + this.exhaustedCooldownMs);
1987
- }
1988
- async getNextMember(pool, currentUuid, accountManager) {
1989
- const availableMembers = this.getAvailableMembers(pool, accountManager);
1990
- if (availableMembers.length === 0) return null;
1991
- const excluded = /* @__PURE__ */ new Set();
1992
- if (currentUuid) excluded.add(currentUuid);
1993
- const preferred = await this.selectPreferredMember(availableMembers, excluded, accountManager);
1994
- if (preferred) return preferred;
1995
- for (const candidate of availableMembers) {
1996
- if (candidate !== currentUuid) return candidate;
1997
- }
1998
- return null;
1999
- }
2000
- async buildFailoverPlan(currentAccount, config, accountManager, options) {
2001
- this.loadPools(config.pools ?? []);
2002
- if ((config.pools?.length ?? 0) === 0 && (config.chains?.length ?? 0) === 0) {
2003
- return { candidates: [], skips: [] };
2004
- }
2005
- const attemptedAccounts = options?.attemptedAccounts ?? /* @__PURE__ */ new Set();
2006
- const visitedChainIndexes = options?.visitedChainIndexes ?? /* @__PURE__ */ new Set();
2007
- const currentUuid = currentAccount?.uuid;
2008
- const candidates = [];
2009
- const skips = [];
2010
- const addedCandidateUuids = /* @__PURE__ */ new Set();
2011
- const appendPoolCandidates = async (poolName, source, chainIndex) => {
2012
- const pool = this.poolsByName.get(poolName);
2013
- if (!pool || !pool.enabled) {
2014
- skips.push({
2015
- type: "chain_disabled",
2016
- poolName,
2017
- reason: "Pool is missing or disabled"
2018
- });
2019
- return;
2020
- }
2021
- const available = this.getAvailableMembers(pool, accountManager);
2022
- if (available.length === 0) {
2023
- skips.push({
2024
- type: "pool_exhausted",
2025
- poolName,
2026
- reason: "No available members"
2027
- });
2028
- return;
2029
- }
2030
- const poolExclusions = /* @__PURE__ */ new Set();
2031
- if (currentUuid) poolExclusions.add(currentUuid);
2032
- while (poolExclusions.size < available.length + (currentUuid ? 1 : 0)) {
2033
- const nextMember = await this.selectPreferredMember(available, poolExclusions, accountManager);
2034
- if (!nextMember) break;
2035
- poolExclusions.add(nextMember);
2036
- if (attemptedAccounts.has(nextMember)) {
2037
- skips.push({
2038
- type: "account_attempted",
2039
- poolName,
2040
- reason: "Already attempted in this cascade",
2041
- detail: nextMember
2042
- });
2043
- continue;
2044
- }
2045
- if (addedCandidateUuids.has(nextMember)) continue;
2046
- candidates.push({
2047
- poolName,
2048
- accountUuid: nextMember,
2049
- source,
2050
- chainIndex
2051
- });
2052
- addedCandidateUuids.add(nextMember);
2053
- }
2054
- for (const memberUuid of available) {
2055
- if (poolExclusions.has(memberUuid)) continue;
2056
- if (attemptedAccounts.has(memberUuid)) {
2057
- skips.push({
2058
- type: "account_attempted",
2059
- poolName,
2060
- reason: "Already attempted in this cascade",
2061
- detail: memberUuid
2062
- });
2063
- continue;
2064
- }
2065
- if (addedCandidateUuids.has(memberUuid)) continue;
2066
- candidates.push({
2067
- poolName,
2068
- accountUuid: memberUuid,
2069
- source,
2070
- chainIndex
2071
- });
2072
- addedCandidateUuids.add(memberUuid);
2073
- }
2074
- };
2075
- if (currentUuid) {
2076
- const currentPool = this.getPoolForAccount(currentUuid);
2077
- if (currentPool) {
2078
- await appendPoolCandidates(currentPool.name, "pool");
2079
- }
2080
- }
2081
- let flattenedChainIndex = 0;
2082
- for (const chain of config.chains ?? []) {
2083
- if (!chain.enabled) {
2084
- for (let i = 0; i < chain.entries.length; i++) {
2085
- skips.push({
2086
- type: "chain_disabled",
2087
- poolName: chain.entries[i]?.pool ?? chain.name,
2088
- reason: `Chain '${chain.name}' is disabled`
2089
- });
2090
- flattenedChainIndex += 1;
2091
- }
2092
- continue;
2093
- }
2094
- for (const entry of chain.entries) {
2095
- if (visitedChainIndexes.has(flattenedChainIndex)) {
2096
- skips.push({
2097
- type: "chain_disabled",
2098
- poolName: entry.pool,
2099
- reason: "Chain entry already visited in this cascade",
2100
- detail: `${flattenedChainIndex}`
2101
- });
2102
- flattenedChainIndex += 1;
2103
- continue;
2104
- }
2105
- if (!entry.enabled) {
2106
- skips.push({
2107
- type: "chain_disabled",
2108
- poolName: entry.pool,
2109
- reason: "Chain entry is disabled",
2110
- detail: `${flattenedChainIndex}`
2111
- });
2112
- flattenedChainIndex += 1;
2113
- continue;
2114
- }
2115
- await appendPoolCandidates(entry.pool, "chain", flattenedChainIndex);
2116
- flattenedChainIndex += 1;
2117
- }
2118
- }
2119
- return { candidates, skips };
2120
- }
2121
- isExhausted(accountUuid) {
2122
- const exhaustedUntil = this.exhaustedUntilByAccount.get(accountUuid);
2123
- if (!exhaustedUntil) return false;
2124
- if (Date.now() >= exhaustedUntil) {
2125
- this.exhaustedUntilByAccount.delete(accountUuid);
2126
- return false;
2127
- }
2128
- return true;
2129
- }
2130
- clearExpiredExhausted() {
2131
- const now = Date.now();
2132
- for (const [accountUuid, exhaustedUntil] of this.exhaustedUntilByAccount.entries()) {
2133
- if (now >= exhaustedUntil) this.exhaustedUntilByAccount.delete(accountUuid);
2134
- }
2135
- }
2136
- async selectPreferredMember(availableMembers, excludedMembers, accountManager) {
2137
- const availableSet = new Set(availableMembers);
2138
- const maxAttempts = Math.max(availableMembers.length * 2, 6);
2139
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
2140
- const selected = await accountManager.selectAccount();
2141
- if (!selected?.uuid) continue;
2142
- if (!availableSet.has(selected.uuid)) continue;
2143
- if (excludedMembers.has(selected.uuid)) continue;
2144
- return selected.uuid;
2145
- }
2146
- for (const memberUuid of availableMembers) {
2147
- if (!excludedMembers.has(memberUuid)) return memberUuid;
2148
- }
2149
- return null;
2150
- }
2151
- };
2152
-
2153
- // ../multi-account-core/src/cascade-state.ts
2154
- function createCascadeState(prompt, currentAccountUuid) {
2155
- const attemptedAccounts = /* @__PURE__ */ new Set();
2156
- if (currentAccountUuid) {
2157
- attemptedAccounts.add(currentAccountUuid);
2158
- }
2159
- return {
2160
- prompt,
2161
- attemptedAccounts,
2162
- visitedChainIndexes: /* @__PURE__ */ new Set()
2163
- };
2164
- }
2165
- var CascadeStateManager = class {
2166
- suppressNextStartTurn = false;
2167
- cascadeState = null;
2168
- startTurn(prompt, currentAccountUuid) {
2169
- if (this.suppressNextStartTurn) {
2170
- this.suppressNextStartTurn = false;
2171
- return this.ensureCascadeState(prompt, currentAccountUuid);
2172
- }
2173
- const shouldReset = !this.cascadeState || this.cascadeState.prompt !== prompt;
2174
- if (shouldReset) {
2175
- this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2176
- return this.cascadeState;
2177
- }
2178
- return this.ensureCascadeState(prompt, currentAccountUuid);
2179
- }
2180
- ensureCascadeState(prompt, currentAccountUuid) {
2181
- if (!this.cascadeState || this.cascadeState.prompt !== prompt) {
2182
- this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2183
- return this.cascadeState;
2184
- }
2185
- if (currentAccountUuid) {
2186
- this.cascadeState.attemptedAccounts.add(currentAccountUuid);
2187
- }
2188
- return this.cascadeState;
2189
- }
2190
- markAttempted(accountUuid) {
2191
- if (!this.cascadeState) return;
2192
- this.cascadeState.attemptedAccounts.add(accountUuid);
2193
- }
2194
- markVisitedChainIndex(index) {
2195
- if (!this.cascadeState) return;
2196
- this.cascadeState.visitedChainIndexes.add(index);
2197
- }
2198
- clearCascadeState() {
2199
- this.cascadeState = null;
2200
- this.suppressNextStartTurn = false;
2201
- }
2202
- getSnapshot() {
2203
- if (!this.cascadeState) return null;
2204
- return {
2205
- prompt: this.cascadeState.prompt,
2206
- attemptedAccounts: new Set(this.cascadeState.attemptedAccounts),
2207
- visitedChainIndexes: new Set(this.cascadeState.visitedChainIndexes)
2208
- };
2209
- }
2210
- };
10
+ // src/account-manager.ts
11
+ import { createAccountManagerForProvider } from "opencode-multi-account-core";
2211
12
 
2212
13
  // src/constants.ts
2213
- var resolvedAnthropicOAuthEnv = resolveAnthropicOAuthEnv();
14
+ import { anthropicOAuthAdapter } from "opencode-multi-account-core";
2214
15
  var ANTHROPIC_OAUTH_ADAPTER = anthropicOAuthAdapter;
2215
16
  var ANTHROPIC_CLIENT_ID = ANTHROPIC_OAUTH_ADAPTER.oauthClientId;
2216
- var ANTHROPIC_AUTHORIZE_ENDPOINT = resolvedAnthropicOAuthEnv.authorizeUrl;
2217
- var ANTHROPIC_REDIRECT_URI = resolvedAnthropicOAuthEnv.redirectUri;
2218
- var ANTHROPIC_SCOPES = resolvedAnthropicOAuthEnv.scopes;
2219
17
  var ANTHROPIC_TOKEN_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.tokenEndpoint;
2220
18
  var ANTHROPIC_USAGE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.usageEndpoint;
2221
19
  var ANTHROPIC_PROFILE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.profileEndpoint;
2222
20
  var ANTHROPIC_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.requestBetaHeader;
2223
21
  var CLAUDE_CLI_USER_AGENT = ANTHROPIC_OAUTH_ADAPTER.cliUserAgent;
2224
22
  var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
2225
- var ACCOUNTS_FILENAME2 = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
23
+ var ACCOUNTS_FILENAME = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
2226
24
  var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
2227
25
  var TOKEN_EXPIRY_BUFFER_MS = 6e4;
2228
26
  var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
2229
27
 
2230
28
  // src/pi-ai-adapter.ts
2231
- import { AsyncLocalStorage } from "node:async_hooks";
29
+ import { AsyncLocalStorage } from "async_hooks";
2232
30
  import * as piAiOauth from "@mariozechner/pi-ai/oauth";
2233
31
 
2234
32
  // src/token-node-request.ts
2235
- import * as childProcess from "node:child_process";
33
+ import * as childProcess from "child_process";
2236
34
  function buildNodeTokenRequestScript() {
2237
35
  return `
2238
36
  const https = require("node:https");
@@ -2320,106 +118,126 @@ async function runNodeTokenRequest(options) {
2320
118
  return await nodeTokenRequestRunner(options);
2321
119
  }
2322
120
 
121
+ // src/utils.ts
122
+ import { setConfigGetter } from "opencode-multi-account-core";
123
+
2323
124
  // src/config.ts
125
+ import {
126
+ getConfig,
127
+ initCoreConfig,
128
+ loadConfig,
129
+ resetConfigCache,
130
+ updateConfigField
131
+ } from "opencode-multi-account-core";
2324
132
  initCoreConfig("claude-multiauth.json");
2325
133
 
2326
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";
2327
145
  setConfigGetter(getConfig);
2328
146
 
2329
147
  // src/usage.ts
2330
- import * as v8 from "valibot";
148
+ import * as v2 from "valibot";
2331
149
 
2332
150
  // src/types.ts
2333
- import * as v7 from "valibot";
2334
- var OAuthCredentialsSchema2 = v7.object({
2335
- type: v7.literal("oauth"),
2336
- refresh: v7.string(),
2337
- access: v7.string(),
2338
- expires: v7.number()
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()
2339
157
  });
2340
- var UsageLimitEntrySchema2 = v7.object({
2341
- utilization: v7.number(),
2342
- resets_at: v7.nullable(v7.string())
158
+ var UsageLimitEntrySchema = v.object({
159
+ utilization: v.number(),
160
+ resets_at: v.nullable(v.string())
2343
161
  });
2344
- var UsageLimitsSchema2 = v7.object({
2345
- five_hour: v7.optional(v7.nullable(UsageLimitEntrySchema2), null),
2346
- seven_day: v7.optional(v7.nullable(UsageLimitEntrySchema2), null),
2347
- seven_day_sonnet: v7.optional(v7.nullable(UsageLimitEntrySchema2), null)
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)
2348
166
  });
2349
- var CredentialRefreshPatchSchema2 = v7.object({
2350
- accessToken: v7.string(),
2351
- expiresAt: v7.number(),
2352
- refreshToken: v7.optional(v7.string()),
2353
- uuid: v7.optional(v7.string()),
2354
- email: v7.optional(v7.string())
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())
2355
173
  });
2356
- var StoredAccountSchema2 = v7.object({
2357
- uuid: v7.optional(v7.string()),
2358
- label: v7.optional(v7.string()),
2359
- email: v7.optional(v7.string()),
2360
- planTier: v7.optional(v7.string(), ""),
2361
- refreshToken: v7.string(),
2362
- accessToken: v7.optional(v7.string()),
2363
- expiresAt: v7.optional(v7.number()),
2364
- addedAt: v7.number(),
2365
- lastUsed: v7.number(),
2366
- enabled: v7.optional(v7.boolean(), true),
2367
- rateLimitResetAt: v7.optional(v7.number()),
2368
- cachedUsage: v7.optional(UsageLimitsSchema2),
2369
- cachedUsageAt: v7.optional(v7.number()),
2370
- consecutiveAuthFailures: v7.optional(v7.number(), 0),
2371
- isAuthDisabled: v7.optional(v7.boolean(), false),
2372
- authDisabledReason: v7.optional(v7.string())
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())
2373
191
  });
2374
- var AccountStorageSchema2 = v7.object({
2375
- version: v7.literal(1),
2376
- accounts: v7.optional(v7.array(StoredAccountSchema2), []),
2377
- activeAccountUuid: v7.optional(v7.string())
192
+ var AccountStorageSchema = v.object({
193
+ version: v.literal(1),
194
+ accounts: v.optional(v.array(StoredAccountSchema), []),
195
+ activeAccountUuid: v.optional(v.string())
2378
196
  });
2379
- var TokenResponseSchema = v7.object({
2380
- access_token: v7.string(),
2381
- refresh_token: v7.optional(v7.string()),
2382
- expires_in: v7.number(),
2383
- account: v7.optional(v7.object({
2384
- uuid: v7.optional(v7.string()),
2385
- email_address: v7.optional(v7.string())
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())
2386
204
  }))
2387
205
  });
2388
- var AccountSelectionStrategySchema2 = v7.picklist(["sticky", "round-robin", "hybrid"]);
2389
- var PluginConfigSchema2 = v7.object({
206
+ var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
207
+ var PluginConfigSchema = v.object({
2390
208
  /** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
2391
- account_selection_strategy: v7.optional(AccountSelectionStrategySchema2, "sticky"),
209
+ account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
2392
210
  /** Use cross-process claim file to distribute parallel sessions across accounts */
2393
- cross_process_claims: v7.optional(v7.boolean(), true),
211
+ cross_process_claims: v.optional(v.boolean(), true),
2394
212
  /** Skip account when any usage tier utilization >= this % (100 = disabled) */
2395
- soft_quota_threshold_percent: v7.optional(v7.pipe(v7.number(), v7.minValue(0), v7.maxValue(100)), 100),
213
+ soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
2396
214
  /** Minimum backoff after rate limit (ms) */
2397
- rate_limit_min_backoff_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 3e4),
215
+ rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
2398
216
  /** Default retry-after when header is missing (ms) */
2399
- default_retry_after_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 6e4),
217
+ default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
2400
218
  /** Consecutive auth failures before disabling account */
2401
- max_consecutive_auth_failures: v7.optional(v7.pipe(v7.number(), v7.integer(), v7.minValue(1)), 3),
219
+ max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
2402
220
  /** Backoff after token refresh failure (ms) */
2403
- token_failure_backoff_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 3e4),
221
+ token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
2404
222
  /** Enable proactive background token refresh */
2405
- proactive_refresh: v7.optional(v7.boolean(), true),
223
+ proactive_refresh: v.optional(v.boolean(), true),
2406
224
  /** Seconds before expiry to trigger proactive refresh (default 30 min) */
2407
- proactive_refresh_buffer_seconds: v7.optional(v7.pipe(v7.number(), v7.minValue(60)), 1800),
225
+ proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
2408
226
  /** Interval between background refresh checks in seconds (default 5 min) */
2409
- proactive_refresh_interval_seconds: v7.optional(v7.pipe(v7.number(), v7.minValue(30)), 300),
227
+ proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
2410
228
  /** Suppress toast notifications */
2411
- quiet_mode: v7.optional(v7.boolean(), false),
229
+ quiet_mode: v.optional(v.boolean(), false),
2412
230
  /** Enable debug logging */
2413
- debug: v7.optional(v7.boolean(), false)
231
+ debug: v.optional(v.boolean(), false)
2414
232
  });
2415
233
 
2416
234
  // src/usage.ts
2417
235
  var OAUTH_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.oauthBetaHeader;
2418
- var ProfileResponseSchema = v8.object({
2419
- account: v8.object({
2420
- email: v8.optional(v8.string()),
2421
- has_claude_pro: v8.optional(v8.boolean(), false),
2422
- has_claude_max: v8.optional(v8.boolean(), false)
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)
2423
241
  })
2424
242
  });
2425
243
  async function fetchUsage(accessToken) {
@@ -2434,7 +252,7 @@ async function fetchUsage(accessToken) {
2434
252
  if (!response.ok) {
2435
253
  return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
2436
254
  }
2437
- const result = v8.safeParse(UsageLimitsSchema2, await response.json());
255
+ const result = v2.safeParse(UsageLimitsSchema, await response.json());
2438
256
  if (!result.success) {
2439
257
  return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
2440
258
  }
@@ -2456,7 +274,7 @@ async function fetchProfile(accessToken) {
2456
274
  if (!response.ok) {
2457
275
  return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
2458
276
  }
2459
- const result = v8.safeParse(ProfileResponseSchema, await response.json());
277
+ const result = v2.safeParse(ProfileResponseSchema, await response.json());
2460
278
  if (!result.success) {
2461
279
  return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
2462
280
  }
@@ -2500,14 +318,12 @@ function fromPiAiCredentials(creds) {
2500
318
  expiresAt: creds.expires
2501
319
  };
2502
320
  }
2503
- var ANTHROPIC_REFRESH_ENDPOINT = ANTHROPIC_TOKEN_ENDPOINT;
2504
- var LEGACY_ANTHROPIC_TOKEN_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
321
+ var ANTHROPIC_REFRESH_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
2505
322
  var REFRESH_NODE_EXECUTABLE = process.env.OPENCODE_REFRESH_NODE_EXECUTABLE || "node";
2506
323
  var tokenProxyContext = new AsyncLocalStorage();
2507
324
  var tokenProxyInstalled = false;
2508
325
  var tokenProxyOriginalFetch = null;
2509
326
  var refreshEndpointUrl = new URL(ANTHROPIC_REFRESH_ENDPOINT);
2510
- var legacyRefreshEndpointUrl = new URL(LEGACY_ANTHROPIC_TOKEN_ENDPOINT);
2511
327
  function buildRefreshRequestError(details) {
2512
328
  return new Error(`Anthropic token refresh request failed. url=${ANTHROPIC_REFRESH_ENDPOINT}; details=${details}`);
2513
329
  }
@@ -2523,65 +339,9 @@ function isAnthropicTokenEndpoint(input) {
2523
339
  const rawUrl = getRequestUrlString(input);
2524
340
  try {
2525
341
  const url = new URL(rawUrl);
2526
- const isConfiguredEndpoint = url.origin === refreshEndpointUrl.origin && url.pathname === refreshEndpointUrl.pathname;
2527
- const isLegacyEndpoint = url.origin === legacyRefreshEndpointUrl.origin && url.pathname === legacyRefreshEndpointUrl.pathname;
2528
- return isConfiguredEndpoint || isLegacyEndpoint;
2529
- } catch {
2530
- return rawUrl === ANTHROPIC_REFRESH_ENDPOINT || rawUrl === LEGACY_ANTHROPIC_TOKEN_ENDPOINT;
2531
- }
2532
- }
2533
- function applyOverridesToTokenParams(params) {
2534
- params.set("client_id", ANTHROPIC_CLIENT_ID);
2535
- if (params.get("grant_type") === "authorization_code") {
2536
- params.set("redirect_uri", ANTHROPIC_REDIRECT_URI);
2537
- params.set("scope", ANTHROPIC_SCOPES);
2538
- }
2539
- }
2540
- function isRecord(value) {
2541
- return typeof value === "object" && value !== null;
2542
- }
2543
- function applyAnthropicTokenRequestOverrides(rawBody) {
2544
- const trimmed = rawBody.trim();
2545
- if (trimmed.length === 0) {
2546
- return rawBody;
2547
- }
2548
- if (trimmed.startsWith("{")) {
2549
- try {
2550
- const parsed = JSON.parse(trimmed);
2551
- if (!isRecord(parsed)) {
2552
- return rawBody;
2553
- }
2554
- const grantType = typeof parsed.grant_type === "string" ? parsed.grant_type : "";
2555
- parsed.client_id = ANTHROPIC_CLIENT_ID;
2556
- if (grantType === "authorization_code") {
2557
- parsed.redirect_uri = ANTHROPIC_REDIRECT_URI;
2558
- parsed.scope = ANTHROPIC_SCOPES;
2559
- }
2560
- return JSON.stringify(parsed);
2561
- } catch {
2562
- return rawBody;
2563
- }
2564
- }
2565
- const params = new URLSearchParams(rawBody);
2566
- applyOverridesToTokenParams(params);
2567
- return params.toString();
2568
- }
2569
- function rewriteAnthropicAuthUrl(rawUrl) {
2570
- try {
2571
- const configuredAuthorizeUrl = new URL(ANTHROPIC_AUTHORIZE_ENDPOINT);
2572
- const rewritten = new URL(rawUrl);
2573
- rewritten.protocol = configuredAuthorizeUrl.protocol;
2574
- rewritten.host = configuredAuthorizeUrl.host;
2575
- rewritten.pathname = configuredAuthorizeUrl.pathname;
2576
- for (const [key, value] of configuredAuthorizeUrl.searchParams.entries()) {
2577
- rewritten.searchParams.set(key, value);
2578
- }
2579
- rewritten.searchParams.set("client_id", ANTHROPIC_CLIENT_ID);
2580
- rewritten.searchParams.set("redirect_uri", ANTHROPIC_REDIRECT_URI);
2581
- rewritten.searchParams.set("scope", ANTHROPIC_SCOPES);
2582
- return rewritten.toString();
342
+ return url.origin === refreshEndpointUrl.origin && url.pathname === refreshEndpointUrl.pathname;
2583
343
  } catch {
2584
- return rawUrl;
344
+ return rawUrl === ANTHROPIC_REFRESH_ENDPOINT;
2585
345
  }
2586
346
  }
2587
347
  function getRequestBodySource(input, init) {
@@ -2619,11 +379,10 @@ function shouldProxyTokenRequest(input) {
2619
379
  return tokenProxyContext.getStore() === true && isAnthropicTokenEndpoint(input);
2620
380
  }
2621
381
  async function postAnthropicTokenViaNode(body) {
2622
- const overriddenBody = applyAnthropicTokenRequestOverrides(body);
2623
382
  let output;
2624
383
  try {
2625
384
  output = await runNodeTokenRequest({
2626
- body: overriddenBody,
385
+ body,
2627
386
  endpoint: ANTHROPIC_REFRESH_ENDPOINT,
2628
387
  executable: REFRESH_NODE_EXECUTABLE,
2629
388
  timeoutMs: TOKEN_REFRESH_TIMEOUT_MS
@@ -2686,12 +445,7 @@ async function fetchProfileWithSingleRetry(accessToken) {
2686
445
  }
2687
446
  async function loginWithPiAi(callbacks) {
2688
447
  const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.loginAnthropic({
2689
- onAuth: (info) => {
2690
- callbacks.onAuth({
2691
- ...info,
2692
- url: info.url ? rewriteAnthropicAuthUrl(info.url) : info.url
2693
- });
2694
- },
448
+ onAuth: callbacks.onAuth,
2695
449
  onPrompt: callbacks.onPrompt,
2696
450
  onProgress: callbacks.onProgress,
2697
451
  onManualCodeInput: callbacks.onManualCodeInput
@@ -2770,7 +524,11 @@ var AccountManager = createAccountManagerForProvider({
2770
524
  refreshToken
2771
525
  });
2772
526
 
527
+ // src/executor.ts
528
+ import { createExecutorForProvider as createExecutorForProvider2, getClearedOAuthBody } from "opencode-multi-account-core";
529
+
2773
530
  // src/rate-limit.ts
531
+ import { createRateLimitHandlers } from "opencode-multi-account-core";
2774
532
  var {
2775
533
  fetchUsageLimits,
2776
534
  getResetMsFromUsage,
@@ -2785,6 +543,7 @@ var {
2785
543
  });
2786
544
 
2787
545
  // src/pool-chain-executor.ts
546
+ import { createExecutorForProvider } from "opencode-multi-account-core";
2788
547
  function buildCascadePrompt(input, init) {
2789
548
  if (typeof init?.body === "string" && init.body.length > 0) {
2790
549
  return init.body;
@@ -2869,7 +628,7 @@ async function executeWithPoolChainRotation(manager, runtimeFactory, poolManager
2869
628
  }
2870
629
 
2871
630
  // src/executor.ts
2872
- var { executeWithAccountRotation: executeWithCoreAccountRotation } = createExecutorForProvider("Anthropic", {
631
+ var { executeWithAccountRotation: executeWithCoreAccountRotation } = createExecutorForProvider2("Anthropic", {
2873
632
  handleRateLimitResponse: async (manager, client, account, response) => handleRateLimitResponse(
2874
633
  manager,
2875
634
  client,
@@ -2894,8 +653,8 @@ async function clearAuthIfNoUsableAccount(manager, client) {
2894
653
  }).catch(() => {
2895
654
  });
2896
655
  }
2897
- function hasPoolChainEntries(config) {
2898
- return (config.pools?.length ?? 0) > 0 || (config.chains?.length ?? 0) > 0;
656
+ function hasPoolChainEntries(config2) {
657
+ return (config2.pools?.length ?? 0) > 0 || (config2.chains?.length ?? 0) > 0;
2899
658
  }
2900
659
  async function executeWithAccountRotation(manager, runtimeFactory, client, input, init, options) {
2901
660
  try {
@@ -2920,6 +679,21 @@ async function executeWithAccountRotation(manager, runtimeFactory, client, input
2920
679
  }
2921
680
  }
2922
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
+
2923
697
  // src/ui/auth-menu.ts
2924
698
  function formatRelativeTime(timestamp) {
2925
699
  if (!timestamp) return "never";
@@ -3125,12 +899,16 @@ function printQuotaError(account, error) {
3125
899
  }
3126
900
 
3127
901
  // src/account-store.ts
3128
- setAccountsFilename(ACCOUNTS_FILENAME2);
902
+ import {
903
+ AccountStore,
904
+ setAccountsFilename
905
+ } from "opencode-multi-account-core";
906
+ setAccountsFilename(ACCOUNTS_FILENAME);
3129
907
 
3130
908
  // src/auth-handler.ts
3131
- import { randomUUID as randomUUID2 } from "node:crypto";
3132
- import { createInterface } from "node:readline";
3133
- import { exec } from "node:child_process";
909
+ import { randomUUID } from "crypto";
910
+ import { createInterface } from "readline";
911
+ import { exec } from "child_process";
3134
912
  function makeFailedFlowResult(message) {
3135
913
  return {
3136
914
  url: "",
@@ -3270,7 +1048,7 @@ async function persistFallback(auth) {
3270
1048
  const store = new AccountStore();
3271
1049
  const now = Date.now();
3272
1050
  const account = {
3273
- uuid: randomUUID2(),
1051
+ uuid: randomUUID(),
3274
1052
  refreshToken: auth.refresh,
3275
1053
  accessToken: auth.access,
3276
1054
  expiresAt: auth.expires,
@@ -3447,7 +1225,122 @@ Retrying authentication for ${label}...
3447
1225
  }
3448
1226
 
3449
1227
  // src/request-transform.ts
3450
- import { createHash } from "node:crypto";
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
+ }
3451
1344
 
3452
1345
  // src/anthropic-prompt.ts
3453
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.
@@ -3630,30 +1523,11 @@ function buildBillingHeader(firstUserMessage) {
3630
1523
  if (!version || !salt) return "";
3631
1524
  const sampled = sampleCodeUnits(firstUserMessage, [4, 7, 20]);
3632
1525
  const hash = createHash("sha256").update(`${salt}${sampled}${version}`).digest("hex").slice(0, 3);
3633
- return `cc_version=${version}.${hash}; cc_entrypoint=cli; cch=00000;`;
1526
+ return `x-anthropic-billing-header: cc_version=${version}.${hash}; cc_entrypoint=cli; cch=00000;`;
3634
1527
  }
3635
1528
  var OPENCODE_CAMEL_RE = /OpenCode/g;
3636
1529
  var OPENCODE_LOWER_RE = /(?<!\/)opencode/gi;
3637
1530
  var TOOL_PREFIX_RESPONSE_RE = /"name"\s*:\s*"mcp_([^"]+)"/g;
3638
- function extractFirstUserTextFromBody(body) {
3639
- if (!body) return "";
3640
- try {
3641
- const parsed = JSON.parse(body);
3642
- if (!Array.isArray(parsed.messages)) return "";
3643
- for (const message of parsed.messages) {
3644
- if (message.role !== "user") continue;
3645
- if (typeof message.content === "string") return message.content;
3646
- if (!Array.isArray(message.content)) continue;
3647
- for (const block of message.content) {
3648
- if (block.type === "text" && typeof block.text === "string") {
3649
- return block.text;
3650
- }
3651
- }
3652
- }
3653
- } catch {
3654
- }
3655
- return "";
3656
- }
3657
1531
  function addToolPrefix(name) {
3658
1532
  if (!ANTHROPIC_OAUTH_ADAPTER.transform.addToolPrefix) {
3659
1533
  return name;
@@ -3679,7 +1553,7 @@ function processCompleteLines(buffer) {
3679
1553
  `;
3680
1554
  return { output, remaining };
3681
1555
  }
3682
- function buildRequestHeaders(input, init, accessToken, bodyString) {
1556
+ function buildRequestHeaders(input, init, accessToken, modelId = "unknown", excludedBetas2) {
3683
1557
  const headers = new Headers();
3684
1558
  if (input instanceof Request) {
3685
1559
  input.headers.forEach((value, key) => {
@@ -3702,22 +1576,16 @@ function buildRequestHeaders(input, init, accessToken, bodyString) {
3702
1576
  }
3703
1577
  }
3704
1578
  const incomingBetas = (headers.get("anthropic-beta") || "").split(",").map((b) => b.trim()).filter(Boolean);
1579
+ const modelBetas = getModelBetas(modelId, excludedBetas2);
3705
1580
  const mergedBetas = [.../* @__PURE__ */ new Set([
3706
- ...ANTHROPIC_BETA_HEADER.split(","),
1581
+ ...modelBetas,
3707
1582
  ...incomingBetas
3708
1583
  ])].join(",");
3709
1584
  headers.set("authorization", `Bearer ${accessToken}`);
3710
1585
  headers.set("anthropic-beta", mergedBetas);
3711
- headers.set("user-agent", CLAUDE_CLI_USER_AGENT);
1586
+ headers.set("user-agent", getUserAgent());
3712
1587
  headers.set("anthropic-dangerous-direct-browser-access", "true");
3713
1588
  headers.set("x-app", "cli");
3714
- const resolvedBody = bodyString ?? (typeof init?.body === "string" ? init.body : void 0);
3715
- const billingHeader = buildBillingHeader(
3716
- extractFirstUserTextFromBody(resolvedBody)
3717
- );
3718
- if (billingHeader) {
3719
- headers.set("x-anthropic-billing-header", billingHeader);
3720
- }
3721
1589
  headers.delete("x-api-key");
3722
1590
  return headers;
3723
1591
  }
@@ -3760,6 +1628,17 @@ function transformRequestBody(body) {
3760
1628
  return body;
3761
1629
  }
3762
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
+ }
3763
1642
  function transformRequestUrl(input) {
3764
1643
  let url = null;
3765
1644
  try {
@@ -3826,6 +1705,7 @@ function createResponseStreamTransform(response) {
3826
1705
  }
3827
1706
 
3828
1707
  // src/proactive-refresh.ts
1708
+ import { createProactiveRefreshQueueForProvider } from "opencode-multi-account-core";
3829
1709
  var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
3830
1710
  providerAuthId: "anthropic",
3831
1711
  getConfig,
@@ -3835,12 +1715,15 @@ var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
3835
1715
  });
3836
1716
 
3837
1717
  // src/runtime-factory.ts
1718
+ import { TokenRefreshError } from "opencode-multi-account-core";
3838
1719
  var TOKEN_REFRESH_PERMANENT_FAILURE_STATUS = 401;
3839
1720
  var AccountRuntimeFactory = class {
3840
1721
  constructor(store, client) {
3841
1722
  this.store = store;
3842
1723
  this.client = client;
3843
1724
  }
1725
+ store;
1726
+ client;
3844
1727
  runtimes = /* @__PURE__ */ new Map();
3845
1728
  initLocks = /* @__PURE__ */ new Map();
3846
1729
  async getRuntime(uuid) {
@@ -3896,14 +1779,41 @@ var AccountRuntimeFactory = class {
3896
1779
  }
3897
1780
  async executeTransformedFetch(input, init, accessToken) {
3898
1781
  const transformedInput = transformRequestUrl(input);
3899
- const rawBody = typeof init?.body === "string" ? init.body : void 0;
3900
- const headers = buildRequestHeaders(transformedInput, init, accessToken, rawBody);
3901
- const transformedBody = rawBody !== void 0 ? transformRequestBody(rawBody) : init?.body;
3902
- const response = await fetch(transformedInput, {
1782
+ const modelId = extractModelIdFromBody(init?.body);
1783
+ const excludedBetas2 = getExcludedBetas(modelId);
1784
+ const headers = buildRequestHeaders(transformedInput, init, accessToken, modelId, excludedBetas2);
1785
+ const transformedBody = typeof init?.body === "string" ? transformRequestBody(init.body) : init?.body;
1786
+ let response = await fetch(transformedInput, {
3903
1787
  ...init,
3904
1788
  headers,
3905
1789
  body: transformedBody
3906
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
+ }
3907
1817
  return createResponseStreamTransform(response);
3908
1818
  }
3909
1819
  async createRuntime(uuid) {
@@ -3929,6 +1839,24 @@ var AccountRuntimeFactory = class {
3929
1839
  };
3930
1840
 
3931
1841
  // src/index.ts
1842
+ function extractFirstUserText(input) {
1843
+ try {
1844
+ const raw = input;
1845
+ const messages = raw.messages ?? raw.request?.messages;
1846
+ if (!Array.isArray(messages)) return "";
1847
+ for (const msg of messages) {
1848
+ if (msg.role !== "user") continue;
1849
+ if (typeof msg.content === "string") return msg.content;
1850
+ if (Array.isArray(msg.content)) {
1851
+ for (const block of msg.content) {
1852
+ if (block.type === "text" && block.text) return block.text;
1853
+ }
1854
+ }
1855
+ }
1856
+ } catch {
1857
+ }
1858
+ return "";
1859
+ }
3932
1860
  function injectSystemPrompt(output) {
3933
1861
  const systemPrompt = getSystemPrompt();
3934
1862
  if (!Array.isArray(output.system)) {
@@ -3950,8 +1878,12 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
3950
1878
  let cascadeStateManager = null;
3951
1879
  let poolChainConfig = { pools: [], chains: [] };
3952
1880
  return {
3953
- "experimental.chat.system.transform": (_input, output) => {
1881
+ "experimental.chat.system.transform": (input, output) => {
3954
1882
  injectSystemPrompt(output);
1883
+ const billingHeader = buildBillingHeader(extractFirstUserText(input));
1884
+ if (billingHeader && !output.system?.includes(billingHeader)) {
1885
+ output.system?.unshift(billingHeader);
1886
+ }
3955
1887
  },
3956
1888
  tool: {
3957
1889
  [ANTHROPIC_OAUTH_ADAPTER.statusToolName]: tool({
@@ -4072,7 +2004,7 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
4072
2004
  apiKey: "",
4073
2005
  "chat.headers": async (input, output) => {
4074
2006
  if (input.provider?.info?.id !== ANTHROPIC_OAUTH_ADAPTER.authProviderId) return;
4075
- output.headers["user-agent"] = CLAUDE_CLI_USER_AGENT;
2007
+ output.headers["user-agent"] = getUserAgent();
4076
2008
  output.headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER;
4077
2009
  output.headers["x-app"] = "cli";
4078
2010
  },
@@ -4111,3 +2043,4 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
4111
2043
  export {
4112
2044
  ClaudeMultiAuthPlugin
4113
2045
  };
2046
+ //# sourceMappingURL=index.js.map