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.d.ts +5 -2
- package/dist/index.js +330 -2397
- package/dist/index.js.map +1 -0
- package/package.json +3 -3
- package/dist/account-manager.d.ts +0 -2
- package/dist/account-store.d.ts +0 -2
- package/dist/anthropic-prompt.d.ts +0 -1
- package/dist/auth-handler.d.ts +0 -20
- package/dist/claims.d.ts +0 -1
- package/dist/config.d.ts +0 -2
- package/dist/constants.d.ts +0 -26
- package/dist/executor.d.ts +0 -8
- package/dist/pi-ai-adapter.d.ts +0 -20
- package/dist/pool-chain-executor.d.ts +0 -11
- package/dist/proactive-refresh.d.ts +0 -2
- package/dist/rate-limit.d.ts +0 -2
- package/dist/request-transform.d.ts +0 -6
- package/dist/runtime-factory.d.ts +0 -20
- package/dist/storage.d.ts +0 -1
- package/dist/token-node-request.d.ts +0 -10
- package/dist/token.d.ts +0 -4
- package/dist/types.d.ts +0 -214
- package/dist/ui/ansi.d.ts +0 -1
- package/dist/ui/auth-menu.d.ts +0 -26
- package/dist/ui/confirm.d.ts +0 -1
- package/dist/ui/select.d.ts +0 -1
- package/dist/usage.d.ts +0 -23
- package/dist/utils.d.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -1,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
|
-
//
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// ../multi-account-core/src/claims.ts
|
|
8
|
-
import { promises as fs2 } from "node:fs";
|
|
9
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
10
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
11
|
-
|
|
12
|
-
// ../multi-account-core/src/utils.ts
|
|
13
|
-
import { join as join2 } from "node:path";
|
|
14
|
-
import { homedir as homedir2 } from "node:os";
|
|
15
|
-
|
|
16
|
-
// ../multi-account-core/src/config.ts
|
|
17
|
-
import { promises as fs } from "node:fs";
|
|
18
|
-
import { randomBytes } from "node:crypto";
|
|
19
|
-
import { dirname, join } from "node:path";
|
|
20
|
-
import { homedir } from "node:os";
|
|
21
|
-
import * as v2 from "valibot";
|
|
22
|
-
|
|
23
|
-
// ../multi-account-core/src/types.ts
|
|
24
|
-
import * as v from "valibot";
|
|
25
|
-
var OAuthCredentialsSchema = v.object({
|
|
26
|
-
type: v.literal("oauth"),
|
|
27
|
-
refresh: v.string(),
|
|
28
|
-
access: v.string(),
|
|
29
|
-
expires: v.number()
|
|
30
|
-
});
|
|
31
|
-
var UsageLimitEntrySchema = v.object({
|
|
32
|
-
utilization: v.number(),
|
|
33
|
-
resets_at: v.nullable(v.string())
|
|
34
|
-
});
|
|
35
|
-
var UsageLimitsSchema = v.object({
|
|
36
|
-
five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
37
|
-
seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
38
|
-
seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
|
|
39
|
-
});
|
|
40
|
-
var CredentialRefreshPatchSchema = v.object({
|
|
41
|
-
accessToken: v.string(),
|
|
42
|
-
expiresAt: v.number(),
|
|
43
|
-
refreshToken: v.optional(v.string()),
|
|
44
|
-
uuid: v.optional(v.string()),
|
|
45
|
-
accountId: v.optional(v.string()),
|
|
46
|
-
email: v.optional(v.string())
|
|
47
|
-
});
|
|
48
|
-
var StoredAccountSchema = v.object({
|
|
49
|
-
uuid: v.optional(v.string()),
|
|
50
|
-
accountId: v.optional(v.string()),
|
|
51
|
-
label: v.optional(v.string()),
|
|
52
|
-
email: v.optional(v.string()),
|
|
53
|
-
planTier: v.optional(v.string(), ""),
|
|
54
|
-
refreshToken: v.string(),
|
|
55
|
-
accessToken: v.optional(v.string()),
|
|
56
|
-
expiresAt: v.optional(v.number()),
|
|
57
|
-
addedAt: v.number(),
|
|
58
|
-
lastUsed: v.number(),
|
|
59
|
-
enabled: v.optional(v.boolean(), true),
|
|
60
|
-
rateLimitResetAt: v.optional(v.number()),
|
|
61
|
-
cachedUsage: v.optional(UsageLimitsSchema),
|
|
62
|
-
cachedUsageAt: v.optional(v.number()),
|
|
63
|
-
consecutiveAuthFailures: v.optional(v.number(), 0),
|
|
64
|
-
isAuthDisabled: v.optional(v.boolean(), false),
|
|
65
|
-
authDisabledReason: v.optional(v.string())
|
|
66
|
-
});
|
|
67
|
-
var AccountStorageSchema = v.object({
|
|
68
|
-
version: v.literal(1),
|
|
69
|
-
accounts: v.optional(v.array(StoredAccountSchema), []),
|
|
70
|
-
activeAccountUuid: v.optional(v.string())
|
|
71
|
-
});
|
|
72
|
-
var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
|
|
73
|
-
var PluginConfigSchema = v.object({
|
|
74
|
-
account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
|
|
75
|
-
cross_process_claims: v.optional(v.boolean(), true),
|
|
76
|
-
soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
|
|
77
|
-
rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
78
|
-
default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
|
|
79
|
-
max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
|
|
80
|
-
token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
|
|
81
|
-
proactive_refresh: v.optional(v.boolean(), true),
|
|
82
|
-
proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
|
|
83
|
-
proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
|
|
84
|
-
quiet_mode: v.optional(v.boolean(), false),
|
|
85
|
-
debug: v.optional(v.boolean(), false)
|
|
86
|
-
});
|
|
87
|
-
var TokenRefreshError = class _TokenRefreshError extends Error {
|
|
88
|
-
status;
|
|
89
|
-
permanent;
|
|
90
|
-
constructor(permanent, status) {
|
|
91
|
-
super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
|
|
92
|
-
this.name = "TokenRefreshError";
|
|
93
|
-
this.status = status;
|
|
94
|
-
this.permanent = permanent;
|
|
95
|
-
Object.setPrototypeOf(this, _TokenRefreshError.prototype);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
function isTokenRefreshError(error) {
|
|
99
|
-
if (error instanceof TokenRefreshError) return true;
|
|
100
|
-
if (!(error instanceof Error)) return false;
|
|
101
|
-
const candidate = error;
|
|
102
|
-
return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ../multi-account-core/src/config.ts
|
|
106
|
-
var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
|
|
107
|
-
var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
|
|
108
|
-
var configFilename = DEFAULT_CONFIG_FILENAME;
|
|
109
|
-
var cachedConfig = null;
|
|
110
|
-
var externalConfigGetter = null;
|
|
111
|
-
function getConfigDir() {
|
|
112
|
-
return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
113
|
-
}
|
|
114
|
-
function getConfigPath() {
|
|
115
|
-
return join(getConfigDir(), configFilename);
|
|
116
|
-
}
|
|
117
|
-
function parseConfig(raw) {
|
|
118
|
-
const result = v2.safeParse(PluginConfigSchema, raw);
|
|
119
|
-
return result.success ? result.output : DEFAULT_CONFIG;
|
|
120
|
-
}
|
|
121
|
-
function initCoreConfig(filename) {
|
|
122
|
-
configFilename = filename || DEFAULT_CONFIG_FILENAME;
|
|
123
|
-
cachedConfig = null;
|
|
124
|
-
}
|
|
125
|
-
async function loadConfig() {
|
|
126
|
-
if (cachedConfig) return cachedConfig;
|
|
127
|
-
const path = getConfigPath();
|
|
128
|
-
try {
|
|
129
|
-
const content = await fs.readFile(path, "utf-8");
|
|
130
|
-
cachedConfig = parseConfig(JSON.parse(content));
|
|
131
|
-
} catch {
|
|
132
|
-
cachedConfig = DEFAULT_CONFIG;
|
|
133
|
-
}
|
|
134
|
-
return cachedConfig;
|
|
135
|
-
}
|
|
136
|
-
function getConfig() {
|
|
137
|
-
if (cachedConfig) return cachedConfig;
|
|
138
|
-
if (externalConfigGetter && externalConfigGetter !== getConfig) {
|
|
139
|
-
try {
|
|
140
|
-
return parseConfig(externalConfigGetter());
|
|
141
|
-
} catch {
|
|
142
|
-
return DEFAULT_CONFIG;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return DEFAULT_CONFIG;
|
|
146
|
-
}
|
|
147
|
-
function setConfigGetter(getter) {
|
|
148
|
-
if (getter === getConfig) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
externalConfigGetter = getter;
|
|
152
|
-
}
|
|
153
|
-
async function updateConfigField(key, value) {
|
|
154
|
-
const path = getConfigPath();
|
|
155
|
-
let existing = {};
|
|
156
|
-
try {
|
|
157
|
-
const content2 = await fs.readFile(path, "utf-8");
|
|
158
|
-
existing = JSON.parse(content2);
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
existing[key] = value;
|
|
162
|
-
await fs.mkdir(dirname(path), { recursive: true });
|
|
163
|
-
const content = `${JSON.stringify(existing, null, 2)}
|
|
164
|
-
`;
|
|
165
|
-
const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
166
|
-
try {
|
|
167
|
-
await fs.writeFile(tempPath, content, "utf-8");
|
|
168
|
-
await fs.rename(tempPath, path);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
try {
|
|
171
|
-
await fs.unlink(tempPath);
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
cachedConfig = null;
|
|
177
|
-
await loadConfig();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ../multi-account-core/src/utils.ts
|
|
181
|
-
function getConfigDir2() {
|
|
182
|
-
return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
|
|
183
|
-
}
|
|
184
|
-
function getErrorCode(error) {
|
|
185
|
-
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
186
|
-
return void 0;
|
|
187
|
-
}
|
|
188
|
-
const code = error.code;
|
|
189
|
-
return typeof code === "string" ? code : void 0;
|
|
190
|
-
}
|
|
191
|
-
function formatWaitTime(ms) {
|
|
192
|
-
const totalSeconds = Math.ceil(ms / 1e3);
|
|
193
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
194
|
-
const days = Math.floor(totalSeconds / 86400);
|
|
195
|
-
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
196
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
197
|
-
const seconds = totalSeconds % 60;
|
|
198
|
-
const parts = [];
|
|
199
|
-
if (days > 0) parts.push(`${days}d`);
|
|
200
|
-
if (hours > 0) parts.push(`${hours}h`);
|
|
201
|
-
if (minutes > 0) parts.push(`${minutes}m`);
|
|
202
|
-
if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
|
|
203
|
-
return parts.join(" ") || "0s";
|
|
204
|
-
}
|
|
205
|
-
function getAccountLabel(account) {
|
|
206
|
-
if (account.label) return account.label;
|
|
207
|
-
if (account.email) return account.email;
|
|
208
|
-
if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
|
|
209
|
-
return `Account ${account.index + 1}`;
|
|
210
|
-
}
|
|
211
|
-
function sleep(ms) {
|
|
212
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
213
|
-
}
|
|
214
|
-
async function showToast(client, message, variant) {
|
|
215
|
-
if (getConfig().quiet_mode) return;
|
|
216
|
-
try {
|
|
217
|
-
await client.tui.showToast({ body: { message, variant } });
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function debugLog(client, message, extra) {
|
|
222
|
-
if (!getConfig().debug) return;
|
|
223
|
-
client.app.log({
|
|
224
|
-
body: { service: "claude-multiauth", level: "debug", message, extra }
|
|
225
|
-
}).catch(() => {
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
function createMinimalClient() {
|
|
229
|
-
return {
|
|
230
|
-
auth: {
|
|
231
|
-
set: async () => {
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
tui: {
|
|
235
|
-
showToast: async () => {
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
app: {
|
|
239
|
-
log: async () => {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function getClearedOAuthBody() {
|
|
245
|
-
return {
|
|
246
|
-
type: "oauth",
|
|
247
|
-
refresh: "",
|
|
248
|
-
access: "",
|
|
249
|
-
expires: 0
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ../multi-account-core/src/claims.ts
|
|
254
|
-
var CLAIMS_FILENAME = "multiauth-claims.json";
|
|
255
|
-
var CLAIM_EXPIRY_MS = 6e4;
|
|
256
|
-
function getClaimsPath() {
|
|
257
|
-
return join3(getConfigDir2(), CLAIMS_FILENAME);
|
|
258
|
-
}
|
|
259
|
-
function isClaimShape(value) {
|
|
260
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
261
|
-
const claim = value;
|
|
262
|
-
return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
|
|
263
|
-
}
|
|
264
|
-
function parseClaims(raw) {
|
|
265
|
-
let parsed;
|
|
266
|
-
try {
|
|
267
|
-
parsed = JSON.parse(raw);
|
|
268
|
-
} catch {
|
|
269
|
-
return {};
|
|
270
|
-
}
|
|
271
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
272
|
-
return {};
|
|
273
|
-
}
|
|
274
|
-
const claims = {};
|
|
275
|
-
for (const [accountId, claim] of Object.entries(parsed)) {
|
|
276
|
-
if (isClaimShape(claim)) {
|
|
277
|
-
claims[accountId] = claim;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return claims;
|
|
281
|
-
}
|
|
282
|
-
function isProcessAlive(pid) {
|
|
283
|
-
try {
|
|
284
|
-
process.kill(pid, 0);
|
|
285
|
-
return true;
|
|
286
|
-
} catch {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function cleanClaims(claims, now) {
|
|
291
|
-
const cleaned = {};
|
|
292
|
-
let changed = false;
|
|
293
|
-
for (const [accountId, claim] of Object.entries(claims)) {
|
|
294
|
-
const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
|
|
295
|
-
const zombieClaim = !isProcessAlive(claim.pid);
|
|
296
|
-
if (expiredByTime || zombieClaim) {
|
|
297
|
-
changed = true;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
cleaned[accountId] = claim;
|
|
301
|
-
}
|
|
302
|
-
return { cleaned, changed };
|
|
303
|
-
}
|
|
304
|
-
async function writeClaimsFile(claims) {
|
|
305
|
-
const path = getClaimsPath();
|
|
306
|
-
const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
307
|
-
await fs2.mkdir(dirname2(path), { recursive: true });
|
|
308
|
-
try {
|
|
309
|
-
await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
|
|
310
|
-
await fs2.rename(tempPath, path);
|
|
311
|
-
} catch (error) {
|
|
312
|
-
try {
|
|
313
|
-
await fs2.unlink(tempPath);
|
|
314
|
-
} catch {
|
|
315
|
-
}
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async function readClaims() {
|
|
320
|
-
try {
|
|
321
|
-
const data = await fs2.readFile(getClaimsPath(), "utf-8");
|
|
322
|
-
const parsed = parseClaims(data);
|
|
323
|
-
const now = Date.now();
|
|
324
|
-
const { cleaned, changed } = cleanClaims(parsed, now);
|
|
325
|
-
if (changed) {
|
|
326
|
-
try {
|
|
327
|
-
await writeClaimsFile(cleaned);
|
|
328
|
-
} catch {
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return cleaned;
|
|
332
|
-
} catch {
|
|
333
|
-
return {};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function writeClaim(accountId) {
|
|
337
|
-
const now = Date.now();
|
|
338
|
-
const claims = await readClaims();
|
|
339
|
-
const { cleaned } = cleanClaims(claims, now);
|
|
340
|
-
cleaned[accountId] = { pid: process.pid, at: now };
|
|
341
|
-
try {
|
|
342
|
-
await writeClaimsFile(cleaned);
|
|
343
|
-
} catch {
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function isClaimedByOther(claims, accountId) {
|
|
347
|
-
if (!accountId) return false;
|
|
348
|
-
const claim = claims[accountId];
|
|
349
|
-
if (!claim) return false;
|
|
350
|
-
if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
|
|
351
|
-
if (!isProcessAlive(claim.pid)) return false;
|
|
352
|
-
return claim.pid !== process.pid;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ../multi-account-core/src/account-manager.ts
|
|
356
|
-
var STARTUP_REFRESH_CONCURRENCY = 3;
|
|
357
|
-
var RECENT_429_COOLDOWN_MS = 3e4;
|
|
358
|
-
var HYBRID_SWITCH_MARGIN = 40;
|
|
359
|
-
function createAccountManagerForProvider(dependencies) {
|
|
360
|
-
const {
|
|
361
|
-
providerAuthId,
|
|
362
|
-
isTokenExpired: isTokenExpired2,
|
|
363
|
-
refreshToken: refreshToken2
|
|
364
|
-
} = dependencies;
|
|
365
|
-
return class AccountManager2 {
|
|
366
|
-
constructor(store) {
|
|
367
|
-
this.store = store;
|
|
368
|
-
}
|
|
369
|
-
cached = [];
|
|
370
|
-
activeAccountUuid;
|
|
371
|
-
client = null;
|
|
372
|
-
runtimeFactory = null;
|
|
373
|
-
roundRobinCursor = 0;
|
|
374
|
-
last429Map = /* @__PURE__ */ new Map();
|
|
375
|
-
static async create(store, currentAuth, client) {
|
|
376
|
-
const manager = new AccountManager2(store);
|
|
377
|
-
await manager.initialize(currentAuth, client);
|
|
378
|
-
return manager;
|
|
379
|
-
}
|
|
380
|
-
async initialize(currentAuth, client) {
|
|
381
|
-
if (client) this.client = client;
|
|
382
|
-
const storage = await this.store.load();
|
|
383
|
-
if (storage.accounts.length > 0) {
|
|
384
|
-
this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
|
|
385
|
-
this.activeAccountUuid = storage.activeAccountUuid;
|
|
386
|
-
if (!this.getActiveAccount() && this.cached.length > 0) {
|
|
387
|
-
this.activeAccountUuid = this.cached[0].uuid;
|
|
388
|
-
}
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (currentAuth.refresh) {
|
|
392
|
-
const newAccount = this.createNewAccount(currentAuth, Date.now());
|
|
393
|
-
await this.store.addAccount(newAccount);
|
|
394
|
-
await this.store.setActiveUuid(newAccount.uuid);
|
|
395
|
-
this.cached = [this.toManagedAccount(newAccount, 0)];
|
|
396
|
-
this.activeAccountUuid = newAccount.uuid;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
async refresh() {
|
|
400
|
-
const storage = await this.store.load();
|
|
401
|
-
this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
|
|
402
|
-
if (storage.activeAccountUuid) {
|
|
403
|
-
this.activeAccountUuid = storage.activeAccountUuid;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
toManagedAccount(storedAccount, index) {
|
|
407
|
-
return {
|
|
408
|
-
index,
|
|
409
|
-
uuid: storedAccount.uuid,
|
|
410
|
-
accountId: storedAccount.accountId,
|
|
411
|
-
label: storedAccount.label,
|
|
412
|
-
email: storedAccount.email,
|
|
413
|
-
planTier: storedAccount.planTier,
|
|
414
|
-
refreshToken: storedAccount.refreshToken,
|
|
415
|
-
accessToken: storedAccount.accessToken,
|
|
416
|
-
expiresAt: storedAccount.expiresAt,
|
|
417
|
-
addedAt: storedAccount.addedAt,
|
|
418
|
-
lastUsed: storedAccount.lastUsed,
|
|
419
|
-
enabled: storedAccount.enabled,
|
|
420
|
-
rateLimitResetAt: storedAccount.rateLimitResetAt,
|
|
421
|
-
cachedUsage: storedAccount.cachedUsage,
|
|
422
|
-
cachedUsageAt: storedAccount.cachedUsageAt,
|
|
423
|
-
consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
|
|
424
|
-
isAuthDisabled: storedAccount.isAuthDisabled,
|
|
425
|
-
authDisabledReason: storedAccount.authDisabledReason,
|
|
426
|
-
last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
createNewAccount(auth, now) {
|
|
430
|
-
return {
|
|
431
|
-
uuid: randomUUID(),
|
|
432
|
-
refreshToken: auth.refresh,
|
|
433
|
-
accessToken: auth.access,
|
|
434
|
-
expiresAt: auth.expires,
|
|
435
|
-
addedAt: now,
|
|
436
|
-
lastUsed: now,
|
|
437
|
-
enabled: true,
|
|
438
|
-
planTier: "",
|
|
439
|
-
consecutiveAuthFailures: 0,
|
|
440
|
-
isAuthDisabled: false
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
getAccountCount() {
|
|
444
|
-
return this.getEligibleAccounts().length;
|
|
445
|
-
}
|
|
446
|
-
getAccounts() {
|
|
447
|
-
return [...this.cached];
|
|
448
|
-
}
|
|
449
|
-
getActiveAccount() {
|
|
450
|
-
if (this.activeAccountUuid) {
|
|
451
|
-
return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
|
|
452
|
-
}
|
|
453
|
-
return this.cached[0] ?? null;
|
|
454
|
-
}
|
|
455
|
-
setClient(client) {
|
|
456
|
-
this.client = client;
|
|
457
|
-
}
|
|
458
|
-
setRuntimeFactory(factory) {
|
|
459
|
-
this.runtimeFactory = factory;
|
|
460
|
-
}
|
|
461
|
-
getEligibleAccounts() {
|
|
462
|
-
return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
|
|
463
|
-
}
|
|
464
|
-
exceedsSoftQuota(account) {
|
|
465
|
-
const threshold = getConfig().soft_quota_threshold_percent;
|
|
466
|
-
if (threshold >= 100) return false;
|
|
467
|
-
const usage = account.cachedUsage;
|
|
468
|
-
if (!usage) return false;
|
|
469
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
470
|
-
return tiers.some((tier) => tier != null && tier.utilization >= threshold);
|
|
471
|
-
}
|
|
472
|
-
hasAnyUsableAccount() {
|
|
473
|
-
return this.getEligibleAccounts().length > 0;
|
|
474
|
-
}
|
|
475
|
-
isRateLimited(account) {
|
|
476
|
-
if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
return this.isUsageExhausted(account);
|
|
480
|
-
}
|
|
481
|
-
isUsageExhausted(account) {
|
|
482
|
-
const usage = account.cachedUsage;
|
|
483
|
-
if (!usage) return false;
|
|
484
|
-
const now = Date.now();
|
|
485
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
486
|
-
return tiers.some(
|
|
487
|
-
(tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
clearExpiredRateLimits() {
|
|
491
|
-
const now = Date.now();
|
|
492
|
-
for (const account of this.cached) {
|
|
493
|
-
if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
|
|
494
|
-
account.rateLimitResetAt = void 0;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
getMinWaitTime() {
|
|
499
|
-
const eligible = this.getEligibleAccounts();
|
|
500
|
-
const available = eligible.filter((account) => !this.isRateLimited(account));
|
|
501
|
-
if (available.length > 0) return 0;
|
|
502
|
-
const now = Date.now();
|
|
503
|
-
const waits = [];
|
|
504
|
-
for (const account of eligible) {
|
|
505
|
-
if (account.rateLimitResetAt) {
|
|
506
|
-
const ms = account.rateLimitResetAt - now;
|
|
507
|
-
if (ms > 0) waits.push(ms);
|
|
508
|
-
}
|
|
509
|
-
const usageResetMs = this.getUsageResetMs(account);
|
|
510
|
-
if (usageResetMs !== null && usageResetMs > 0) {
|
|
511
|
-
waits.push(usageResetMs);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return waits.length > 0 ? Math.min(...waits) : 0;
|
|
515
|
-
}
|
|
516
|
-
getUsageResetMs(account) {
|
|
517
|
-
const usage = account.cachedUsage;
|
|
518
|
-
if (!usage) return null;
|
|
519
|
-
const now = Date.now();
|
|
520
|
-
const candidates = [];
|
|
521
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
522
|
-
for (const tier of tiers) {
|
|
523
|
-
if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
|
|
524
|
-
const ms = Date.parse(tier.resets_at) - now;
|
|
525
|
-
if (ms > 0) candidates.push(ms);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
529
|
-
}
|
|
530
|
-
async selectAccount() {
|
|
531
|
-
await this.refresh();
|
|
532
|
-
this.clearExpiredRateLimits();
|
|
533
|
-
const eligible = this.getEligibleAccounts();
|
|
534
|
-
if (eligible.length === 0) return null;
|
|
535
|
-
const config = getConfig();
|
|
536
|
-
const claims = config.cross_process_claims ? await readClaims() : {};
|
|
537
|
-
const strategy = config.account_selection_strategy;
|
|
538
|
-
let selected;
|
|
539
|
-
switch (strategy) {
|
|
540
|
-
case "round-robin":
|
|
541
|
-
selected = this.selectRoundRobin(eligible, claims);
|
|
542
|
-
break;
|
|
543
|
-
case "hybrid":
|
|
544
|
-
selected = this.selectHybrid(eligible, claims);
|
|
545
|
-
break;
|
|
546
|
-
case "sticky":
|
|
547
|
-
default:
|
|
548
|
-
selected = this.selectSticky(eligible, claims);
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
if (selected?.uuid) {
|
|
552
|
-
this.activeAccountUuid = selected.uuid;
|
|
553
|
-
this.store.setActiveUuid(selected.uuid).catch(() => {
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
if (config.cross_process_claims && selected?.uuid) {
|
|
557
|
-
writeClaim(selected.uuid).catch(() => {
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
return selected;
|
|
561
|
-
}
|
|
562
|
-
isUsable(account) {
|
|
563
|
-
return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
|
|
564
|
-
}
|
|
565
|
-
isInRecentCooldown(account) {
|
|
566
|
-
if (!account.last429At) return false;
|
|
567
|
-
return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
|
|
568
|
-
}
|
|
569
|
-
fallbackNotRateLimited(eligible) {
|
|
570
|
-
const account = eligible.find((candidate) => !this.isRateLimited(candidate));
|
|
571
|
-
if (account) {
|
|
572
|
-
this.activateAccount(account);
|
|
573
|
-
return account;
|
|
574
|
-
}
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
selectSticky(eligible, claims) {
|
|
578
|
-
const current = this.getActiveAccount();
|
|
579
|
-
if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
|
|
580
|
-
this.activateAccount(current);
|
|
581
|
-
return current;
|
|
582
|
-
}
|
|
583
|
-
const unclaimed = eligible.find(
|
|
584
|
-
(account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
|
|
585
|
-
);
|
|
586
|
-
if (unclaimed) {
|
|
587
|
-
this.activateAccount(unclaimed);
|
|
588
|
-
return unclaimed;
|
|
589
|
-
}
|
|
590
|
-
const available = eligible.find((account) => this.isUsable(account));
|
|
591
|
-
if (available) {
|
|
592
|
-
this.activateAccount(available);
|
|
593
|
-
return available;
|
|
594
|
-
}
|
|
595
|
-
return this.fallbackNotRateLimited(eligible);
|
|
596
|
-
}
|
|
597
|
-
selectRoundRobin(eligible, claims) {
|
|
598
|
-
for (let i = 0; i < eligible.length; i++) {
|
|
599
|
-
const index = (this.roundRobinCursor + i) % eligible.length;
|
|
600
|
-
const account = eligible[index];
|
|
601
|
-
if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
|
|
602
|
-
this.roundRobinCursor = (index + 1) % eligible.length;
|
|
603
|
-
this.activateAccount(account);
|
|
604
|
-
return account;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
for (let i = 0; i < eligible.length; i++) {
|
|
608
|
-
const index = (this.roundRobinCursor + i) % eligible.length;
|
|
609
|
-
const account = eligible[index];
|
|
610
|
-
if (this.isUsable(account)) {
|
|
611
|
-
this.roundRobinCursor = (index + 1) % eligible.length;
|
|
612
|
-
this.activateAccount(account);
|
|
613
|
-
return account;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return this.fallbackNotRateLimited(eligible);
|
|
617
|
-
}
|
|
618
|
-
selectHybrid(eligible, claims) {
|
|
619
|
-
const usable = eligible.filter((account) => this.isUsable(account));
|
|
620
|
-
const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
|
|
621
|
-
if (pool.length === 0) return null;
|
|
622
|
-
const activeUuid = this.activeAccountUuid;
|
|
623
|
-
let best = pool[0];
|
|
624
|
-
let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
|
|
625
|
-
for (let i = 1; i < pool.length; i++) {
|
|
626
|
-
const account = pool[i];
|
|
627
|
-
const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
|
|
628
|
-
if (score > bestScore) {
|
|
629
|
-
best = account;
|
|
630
|
-
bestScore = score;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
const current = pool.find((account) => account.uuid === activeUuid);
|
|
634
|
-
if (current && current !== best) {
|
|
635
|
-
const currentScore = this.calculateHybridScore(current, true, claims);
|
|
636
|
-
const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
|
|
637
|
-
if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
|
|
638
|
-
this.activateAccount(current);
|
|
639
|
-
return current;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
this.activateAccount(best);
|
|
643
|
-
return best;
|
|
644
|
-
}
|
|
645
|
-
calculateHybridScore(account, isActive, claims) {
|
|
646
|
-
const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
|
|
647
|
-
const usageScore = (100 - maxUtilization) / 100 * 450;
|
|
648
|
-
const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
|
|
649
|
-
const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
|
|
650
|
-
const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
|
|
651
|
-
const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
|
|
652
|
-
const stickinessBonus = isActive ? 120 : 0;
|
|
653
|
-
const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
|
|
654
|
-
return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
|
|
655
|
-
}
|
|
656
|
-
getMaxUtilization(account) {
|
|
657
|
-
const usage = account.cachedUsage;
|
|
658
|
-
if (!usage) return 65;
|
|
659
|
-
const tiers = [usage.five_hour, usage.seven_day];
|
|
660
|
-
const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
|
|
661
|
-
return utilizations.length > 0 ? Math.max(...utilizations) : 65;
|
|
662
|
-
}
|
|
663
|
-
activateAccount(account) {
|
|
664
|
-
this.activeAccountUuid = account.uuid;
|
|
665
|
-
account.lastUsed = Date.now();
|
|
666
|
-
}
|
|
667
|
-
async markRateLimited(uuid, backoffMs) {
|
|
668
|
-
const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
|
|
669
|
-
this.last429Map.set(uuid, Date.now());
|
|
670
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
671
|
-
account.rateLimitResetAt = Date.now() + effectiveBackoff;
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
async markRevoked(uuid) {
|
|
675
|
-
await this.removeAccountByUuid(uuid);
|
|
676
|
-
}
|
|
677
|
-
async markSuccess(uuid) {
|
|
678
|
-
this.last429Map.delete(uuid);
|
|
679
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
680
|
-
account.rateLimitResetAt = void 0;
|
|
681
|
-
account.consecutiveAuthFailures = 0;
|
|
682
|
-
account.lastUsed = Date.now();
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
syncToOpenCode(account) {
|
|
686
|
-
if (!this.client || !account.accessToken || !account.expiresAt) return;
|
|
687
|
-
this.client.auth.set({
|
|
688
|
-
path: { id: providerAuthId },
|
|
689
|
-
body: {
|
|
690
|
-
type: "oauth",
|
|
691
|
-
refresh: account.refreshToken,
|
|
692
|
-
access: account.accessToken,
|
|
693
|
-
expires: account.expiresAt
|
|
694
|
-
}
|
|
695
|
-
}).catch(() => {
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
699
|
-
if (!this.client) return;
|
|
700
|
-
const storage = await this.store.load();
|
|
701
|
-
if (storage.accounts.length > 0) return;
|
|
702
|
-
await this.client.auth.set({
|
|
703
|
-
path: { id: providerAuthId },
|
|
704
|
-
body: getClearedOAuthBody()
|
|
705
|
-
}).catch(() => {
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
async removeAccountByUuid(uuid) {
|
|
709
|
-
const removed = await this.store.removeAccount(uuid);
|
|
710
|
-
if (!removed) return;
|
|
711
|
-
this.last429Map.delete(uuid);
|
|
712
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
713
|
-
await this.refresh();
|
|
714
|
-
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
715
|
-
}
|
|
716
|
-
async markAuthFailure(uuid, result) {
|
|
717
|
-
if (!result.ok && result.permanent) {
|
|
718
|
-
await this.removeAccountByUuid(uuid);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
await this.store.mutateStorage((storage) => {
|
|
722
|
-
const account = storage.accounts.find((entry) => entry.uuid === uuid);
|
|
723
|
-
if (!account) return;
|
|
724
|
-
account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
|
|
725
|
-
const maxFailures = getConfig().max_consecutive_auth_failures;
|
|
726
|
-
const usableCount = storage.accounts.filter(
|
|
727
|
-
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
|
|
728
|
-
).length;
|
|
729
|
-
if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
730
|
-
account.isAuthDisabled = true;
|
|
731
|
-
account.authDisabledReason = `${maxFailures} consecutive auth failures`;
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
async applyUsageCache(uuid, usage) {
|
|
736
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
737
|
-
const now = Date.now();
|
|
738
|
-
const exhaustedTierResetTimes = [usage.five_hour, usage.seven_day].flatMap((tier) => {
|
|
739
|
-
if (tier == null || tier.utilization < 100 || tier.resets_at == null) {
|
|
740
|
-
return [];
|
|
741
|
-
}
|
|
742
|
-
return [Date.parse(tier.resets_at)];
|
|
743
|
-
}).filter((resetAt) => Number.isFinite(resetAt) && resetAt > now);
|
|
744
|
-
account.cachedUsage = usage;
|
|
745
|
-
account.cachedUsageAt = Date.now();
|
|
746
|
-
account.rateLimitResetAt = exhaustedTierResetTimes.length > 0 ? Math.min(...exhaustedTierResetTimes) : void 0;
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
async applyProfileCache(uuid, profile) {
|
|
750
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
751
|
-
account.email = profile.email ?? account.email;
|
|
752
|
-
account.planTier = profile.planTier;
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
async ensureValidToken(uuid, client) {
|
|
756
|
-
const credentials = await this.store.readCredentials(uuid);
|
|
757
|
-
if (!credentials) return { ok: false, permanent: true };
|
|
758
|
-
if (credentials.accessToken && credentials.expiresAt && !isTokenExpired2(credentials)) {
|
|
759
|
-
return {
|
|
760
|
-
ok: true,
|
|
761
|
-
patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
const result = await refreshToken2(credentials.refreshToken, uuid, client);
|
|
765
|
-
if (!result.ok) return result;
|
|
766
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
767
|
-
account.accessToken = result.patch.accessToken;
|
|
768
|
-
account.expiresAt = result.patch.expiresAt;
|
|
769
|
-
if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
|
|
770
|
-
if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
|
|
771
|
-
if (result.patch.accountId) account.accountId = result.patch.accountId;
|
|
772
|
-
if (result.patch.email) account.email = result.patch.email;
|
|
773
|
-
account.consecutiveAuthFailures = 0;
|
|
774
|
-
account.isAuthDisabled = false;
|
|
775
|
-
account.authDisabledReason = void 0;
|
|
776
|
-
});
|
|
777
|
-
if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
|
|
778
|
-
this.activeAccountUuid = result.patch.uuid;
|
|
779
|
-
this.store.setActiveUuid(result.patch.uuid).catch(() => {
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
|
|
783
|
-
this.syncToOpenCode(updated);
|
|
784
|
-
}
|
|
785
|
-
return result;
|
|
786
|
-
}
|
|
787
|
-
async validateNonActiveTokens(client) {
|
|
788
|
-
await this.refresh();
|
|
789
|
-
const activeUuid = this.activeAccountUuid;
|
|
790
|
-
const eligible = this.cached.filter(
|
|
791
|
-
(account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
|
|
792
|
-
);
|
|
793
|
-
for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
|
|
794
|
-
const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
|
|
795
|
-
await Promise.all(
|
|
796
|
-
batch.map(async (account) => {
|
|
797
|
-
if (!account.uuid || !isTokenExpired2(account)) return;
|
|
798
|
-
const result = await this.ensureValidToken(account.uuid, client);
|
|
799
|
-
if (!result.ok) {
|
|
800
|
-
await this.markAuthFailure(account.uuid, result);
|
|
801
|
-
}
|
|
802
|
-
})
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
async removeAccount(index) {
|
|
807
|
-
const account = this.cached[index];
|
|
808
|
-
if (!account?.uuid) return false;
|
|
809
|
-
const removed = await this.store.removeAccount(account.uuid);
|
|
810
|
-
if (removed) {
|
|
811
|
-
await this.refresh();
|
|
812
|
-
}
|
|
813
|
-
return removed;
|
|
814
|
-
}
|
|
815
|
-
async clearAllAccounts() {
|
|
816
|
-
await this.store.clear();
|
|
817
|
-
this.cached = [];
|
|
818
|
-
this.activeAccountUuid = void 0;
|
|
819
|
-
}
|
|
820
|
-
async addAccount(auth, email) {
|
|
821
|
-
if (!auth.refresh) return;
|
|
822
|
-
const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
|
|
823
|
-
if (existingByToken) return;
|
|
824
|
-
if (email) {
|
|
825
|
-
const existingByEmail = this.cached.find(
|
|
826
|
-
(account) => account.email && account.email === email
|
|
827
|
-
);
|
|
828
|
-
if (existingByEmail?.uuid) {
|
|
829
|
-
await this.replaceAccountCredentials(existingByEmail.uuid, auth);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
const newAccount = this.createNewAccount(auth, Date.now());
|
|
834
|
-
if (email) newAccount.email = email;
|
|
835
|
-
await this.store.addAccount(newAccount);
|
|
836
|
-
this.activeAccountUuid = newAccount.uuid;
|
|
837
|
-
await this.store.setActiveUuid(newAccount.uuid);
|
|
838
|
-
await this.refresh();
|
|
839
|
-
}
|
|
840
|
-
async toggleEnabled(uuid) {
|
|
841
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
842
|
-
account.enabled = !(account.enabled ?? true);
|
|
843
|
-
if (account.enabled) {
|
|
844
|
-
account.isAuthDisabled = false;
|
|
845
|
-
account.authDisabledReason = void 0;
|
|
846
|
-
account.consecutiveAuthFailures = 0;
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
async replaceAccountCredentials(uuid, auth) {
|
|
851
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
852
|
-
account.refreshToken = auth.refresh;
|
|
853
|
-
account.accessToken = auth.access;
|
|
854
|
-
account.expiresAt = auth.expires;
|
|
855
|
-
account.lastUsed = Date.now();
|
|
856
|
-
account.enabled = true;
|
|
857
|
-
account.isAuthDisabled = false;
|
|
858
|
-
account.authDisabledReason = void 0;
|
|
859
|
-
account.consecutiveAuthFailures = 0;
|
|
860
|
-
account.rateLimitResetAt = void 0;
|
|
861
|
-
});
|
|
862
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
863
|
-
if (updated && uuid === this.activeAccountUuid) {
|
|
864
|
-
this.syncToOpenCode(updated);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async retryAuth(uuid, client) {
|
|
868
|
-
await this.store.mutateAccount(uuid, (account) => {
|
|
869
|
-
account.consecutiveAuthFailures = 0;
|
|
870
|
-
account.isAuthDisabled = false;
|
|
871
|
-
account.authDisabledReason = void 0;
|
|
872
|
-
});
|
|
873
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
874
|
-
const credentials = await this.store.readCredentials(uuid);
|
|
875
|
-
if (!credentials) return { ok: false, permanent: true };
|
|
876
|
-
const result = await refreshToken2(credentials.refreshToken, uuid, client);
|
|
877
|
-
if (result.ok) {
|
|
878
|
-
const updated = await this.store.mutateAccount(uuid, (account) => {
|
|
879
|
-
account.accessToken = result.patch.accessToken;
|
|
880
|
-
account.expiresAt = result.patch.expiresAt;
|
|
881
|
-
if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
|
|
882
|
-
if (result.patch.uuid) account.uuid = result.patch.uuid;
|
|
883
|
-
if (result.patch.accountId) account.accountId = result.patch.accountId;
|
|
884
|
-
if (result.patch.email) account.email = result.patch.email;
|
|
885
|
-
account.enabled = true;
|
|
886
|
-
account.consecutiveAuthFailures = 0;
|
|
887
|
-
});
|
|
888
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
889
|
-
if (result.patch.uuid) {
|
|
890
|
-
this.runtimeFactory?.invalidate(result.patch.uuid);
|
|
891
|
-
}
|
|
892
|
-
const nextUuid = result.patch.uuid ?? uuid;
|
|
893
|
-
if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
|
|
894
|
-
this.activeAccountUuid = result.patch.uuid;
|
|
895
|
-
await this.store.setActiveUuid(result.patch.uuid);
|
|
896
|
-
}
|
|
897
|
-
if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
|
|
898
|
-
const freshCredentials = await this.store.readCredentials(nextUuid);
|
|
899
|
-
if (freshCredentials) {
|
|
900
|
-
this.syncToOpenCode({
|
|
901
|
-
refreshToken: freshCredentials.refreshToken,
|
|
902
|
-
accessToken: freshCredentials.accessToken,
|
|
903
|
-
expiresAt: freshCredentials.expiresAt
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
} else {
|
|
908
|
-
await this.markAuthFailure(uuid, result);
|
|
909
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
910
|
-
}
|
|
911
|
-
return result;
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// ../multi-account-core/src/account-store.ts
|
|
917
|
-
import { promises as fs4 } from "node:fs";
|
|
918
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
919
|
-
import { dirname as dirname4, join as join5 } from "node:path";
|
|
920
|
-
import lockfile from "proper-lockfile";
|
|
921
|
-
import * as v4 from "valibot";
|
|
922
|
-
|
|
923
|
-
// ../multi-account-core/src/storage.ts
|
|
924
|
-
import { promises as fs3 } from "node:fs";
|
|
925
|
-
import { dirname as dirname3, join as join4 } from "node:path";
|
|
926
|
-
import * as v3 from "valibot";
|
|
927
|
-
|
|
928
|
-
// ../multi-account-core/src/constants.ts
|
|
929
|
-
var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
|
|
930
|
-
var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
|
|
931
|
-
function setAccountsFilename(filename) {
|
|
932
|
-
if (!filename) {
|
|
933
|
-
ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
ACCOUNTS_FILENAME = filename;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ../multi-account-core/src/storage.ts
|
|
940
|
-
function getStoragePath() {
|
|
941
|
-
return join4(getConfigDir2(), ACCOUNTS_FILENAME);
|
|
942
|
-
}
|
|
943
|
-
async function backupCorruptFile(targetPath, content) {
|
|
944
|
-
const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
|
|
945
|
-
await fs3.mkdir(dirname3(backupPath), { recursive: true });
|
|
946
|
-
await fs3.writeFile(backupPath, content, "utf-8");
|
|
947
|
-
}
|
|
948
|
-
async function readStorageFromDisk(targetPath, backupOnCorrupt) {
|
|
949
|
-
let content;
|
|
950
|
-
try {
|
|
951
|
-
content = await fs3.readFile(targetPath, "utf-8");
|
|
952
|
-
} catch (error) {
|
|
953
|
-
if (getErrorCode(error) === "ENOENT") {
|
|
954
|
-
return null;
|
|
955
|
-
}
|
|
956
|
-
throw error;
|
|
957
|
-
}
|
|
958
|
-
let parsed;
|
|
959
|
-
try {
|
|
960
|
-
parsed = JSON.parse(content);
|
|
961
|
-
} catch {
|
|
962
|
-
if (backupOnCorrupt) {
|
|
963
|
-
try {
|
|
964
|
-
await backupCorruptFile(targetPath, content);
|
|
965
|
-
} catch {
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
const validation = v3.safeParse(AccountStorageSchema, parsed);
|
|
971
|
-
if (!validation.success) {
|
|
972
|
-
if (backupOnCorrupt) {
|
|
973
|
-
try {
|
|
974
|
-
await backupCorruptFile(targetPath, content);
|
|
975
|
-
} catch {
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
return validation.output;
|
|
981
|
-
}
|
|
982
|
-
function deduplicateAccounts(accounts) {
|
|
983
|
-
const deduplicated = [];
|
|
984
|
-
const indexByUuid = /* @__PURE__ */ new Map();
|
|
985
|
-
for (const account of accounts) {
|
|
986
|
-
if (!account.uuid) {
|
|
987
|
-
deduplicated.push(account);
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
const existingIndex = indexByUuid.get(account.uuid);
|
|
991
|
-
if (existingIndex === void 0) {
|
|
992
|
-
indexByUuid.set(account.uuid, deduplicated.length);
|
|
993
|
-
deduplicated.push(account);
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
const existingAccount = deduplicated[existingIndex];
|
|
997
|
-
if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
|
|
998
|
-
deduplicated[existingIndex] = account;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return deduplicated;
|
|
1002
|
-
}
|
|
1003
|
-
async function loadAccounts() {
|
|
1004
|
-
const storagePath = getStoragePath();
|
|
1005
|
-
const storage = await readStorageFromDisk(storagePath, true);
|
|
1006
|
-
if (!storage) {
|
|
1007
|
-
return null;
|
|
1008
|
-
}
|
|
1009
|
-
return {
|
|
1010
|
-
...storage,
|
|
1011
|
-
accounts: deduplicateAccounts(storage.accounts || [])
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// ../multi-account-core/src/account-store.ts
|
|
1016
|
-
var FILE_MODE = 384;
|
|
1017
|
-
var LOCK_OPTIONS = {
|
|
1018
|
-
stale: 1e4,
|
|
1019
|
-
retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
|
|
1020
|
-
};
|
|
1021
|
-
function getStoragePath2() {
|
|
1022
|
-
return join5(getConfigDir2(), ACCOUNTS_FILENAME);
|
|
1023
|
-
}
|
|
1024
|
-
function createEmptyStorage() {
|
|
1025
|
-
return { version: 1, accounts: [] };
|
|
1026
|
-
}
|
|
1027
|
-
function buildTempPath(targetPath) {
|
|
1028
|
-
return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
|
|
1029
|
-
}
|
|
1030
|
-
async function writeAtomicText(targetPath, content) {
|
|
1031
|
-
await fs4.mkdir(dirname4(targetPath), { recursive: true });
|
|
1032
|
-
const tempPath = buildTempPath(targetPath);
|
|
1033
|
-
try {
|
|
1034
|
-
await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
|
|
1035
|
-
await fs4.chmod(tempPath, FILE_MODE);
|
|
1036
|
-
await fs4.rename(tempPath, targetPath);
|
|
1037
|
-
await fs4.chmod(targetPath, FILE_MODE);
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
try {
|
|
1040
|
-
await fs4.unlink(tempPath);
|
|
1041
|
-
} catch {
|
|
1042
|
-
}
|
|
1043
|
-
throw error;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
async function writeStorageAtomic(targetPath, storage) {
|
|
1047
|
-
const validation = v4.safeParse(AccountStorageSchema, storage);
|
|
1048
|
-
if (!validation.success) {
|
|
1049
|
-
throw new Error("Invalid account storage payload");
|
|
1050
|
-
}
|
|
1051
|
-
await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
|
|
1052
|
-
`);
|
|
1053
|
-
}
|
|
1054
|
-
async function ensureStorageFileExists(targetPath) {
|
|
1055
|
-
await fs4.mkdir(dirname4(targetPath), { recursive: true });
|
|
1056
|
-
const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
|
|
1057
|
-
`;
|
|
1058
|
-
try {
|
|
1059
|
-
await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
if (getErrorCode(error) !== "EEXIST") throw error;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
async function withFileLock(fn) {
|
|
1065
|
-
const storagePath = getStoragePath2();
|
|
1066
|
-
await ensureStorageFileExists(storagePath);
|
|
1067
|
-
let release = null;
|
|
1068
|
-
try {
|
|
1069
|
-
release = await lockfile.lock(storagePath, LOCK_OPTIONS);
|
|
1070
|
-
return await fn(storagePath);
|
|
1071
|
-
} finally {
|
|
1072
|
-
if (release) {
|
|
1073
|
-
try {
|
|
1074
|
-
await release();
|
|
1075
|
-
} catch {
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
var AccountStore = class {
|
|
1081
|
-
async load() {
|
|
1082
|
-
const storage = await loadAccounts();
|
|
1083
|
-
return storage ?? createEmptyStorage();
|
|
1084
|
-
}
|
|
1085
|
-
async readCredentials(uuid) {
|
|
1086
|
-
const storagePath = getStoragePath2();
|
|
1087
|
-
const storage = await readStorageFromDisk(storagePath, false);
|
|
1088
|
-
if (!storage) return null;
|
|
1089
|
-
const account = storage.accounts.find((a) => a.uuid === uuid);
|
|
1090
|
-
if (!account) return null;
|
|
1091
|
-
return {
|
|
1092
|
-
refreshToken: account.refreshToken,
|
|
1093
|
-
accessToken: account.accessToken,
|
|
1094
|
-
expiresAt: account.expiresAt,
|
|
1095
|
-
accountId: account.accountId
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
async mutateAccount(uuid, fn) {
|
|
1099
|
-
return await withFileLock(async (storagePath) => {
|
|
1100
|
-
const current = await readStorageFromDisk(storagePath, false);
|
|
1101
|
-
if (!current) return null;
|
|
1102
|
-
const account = current.accounts.find((a) => a.uuid === uuid);
|
|
1103
|
-
if (!account) return null;
|
|
1104
|
-
fn(account);
|
|
1105
|
-
await writeStorageAtomic(storagePath, current);
|
|
1106
|
-
return { ...account };
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
async mutateStorage(fn) {
|
|
1110
|
-
await withFileLock(async (storagePath) => {
|
|
1111
|
-
const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
|
|
1112
|
-
fn(current);
|
|
1113
|
-
await writeStorageAtomic(storagePath, current);
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
async addAccount(account) {
|
|
1117
|
-
await withFileLock(async (storagePath) => {
|
|
1118
|
-
const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
|
|
1119
|
-
const exists = current.accounts.some(
|
|
1120
|
-
(a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
|
|
1121
|
-
);
|
|
1122
|
-
if (exists) return;
|
|
1123
|
-
current.accounts.push(account);
|
|
1124
|
-
await writeStorageAtomic(storagePath, current);
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
async removeAccount(uuid) {
|
|
1128
|
-
return await withFileLock(async (storagePath) => {
|
|
1129
|
-
const current = await readStorageFromDisk(storagePath, false);
|
|
1130
|
-
if (!current) return false;
|
|
1131
|
-
const initialLength = current.accounts.length;
|
|
1132
|
-
current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
|
|
1133
|
-
if (current.accounts.length === initialLength) return false;
|
|
1134
|
-
if (current.activeAccountUuid === uuid) {
|
|
1135
|
-
current.activeAccountUuid = current.accounts[0]?.uuid;
|
|
1136
|
-
}
|
|
1137
|
-
await writeStorageAtomic(storagePath, current);
|
|
1138
|
-
return true;
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
async setActiveUuid(uuid) {
|
|
1142
|
-
await this.mutateStorage((storage) => {
|
|
1143
|
-
storage.activeAccountUuid = uuid;
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
async clear() {
|
|
1147
|
-
await withFileLock(async (storagePath) => {
|
|
1148
|
-
await writeStorageAtomic(storagePath, createEmptyStorage());
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
// ../multi-account-core/src/executor.ts
|
|
1154
|
-
var MIN_MAX_RETRIES = 6;
|
|
1155
|
-
var RETRIES_PER_ACCOUNT = 3;
|
|
1156
|
-
var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
|
|
1157
|
-
var MAX_RESOLVE_ATTEMPTS = 10;
|
|
1158
|
-
var SERVER_RETRY_BASE_MS = 1e3;
|
|
1159
|
-
var SERVER_RETRY_MAX_MS = 4e3;
|
|
1160
|
-
function isAbortError(error) {
|
|
1161
|
-
return error instanceof Error && error.name === "AbortError";
|
|
1162
|
-
}
|
|
1163
|
-
function createExecutorForProvider(providerName, dependencies) {
|
|
1164
|
-
const {
|
|
1165
|
-
handleRateLimitResponse: handleRateLimitResponse2,
|
|
1166
|
-
formatWaitTime: formatWaitTime2,
|
|
1167
|
-
sleep: sleep2,
|
|
1168
|
-
showToast: showToast2,
|
|
1169
|
-
getAccountLabel: getAccountLabel2
|
|
1170
|
-
} = dependencies;
|
|
1171
|
-
async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
|
|
1172
|
-
const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
|
|
1173
|
-
let previousAccountUuid;
|
|
1174
|
-
async function retryServerErrors(account, runtime) {
|
|
1175
|
-
for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
|
|
1176
|
-
const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
|
|
1177
|
-
const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
|
|
1178
|
-
await sleep2(jitteredBackoff);
|
|
1179
|
-
let retryResponse;
|
|
1180
|
-
try {
|
|
1181
|
-
retryResponse = await runtime.fetch(input, init);
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
if (isAbortError(error)) throw error;
|
|
1184
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1185
|
-
return null;
|
|
1186
|
-
}
|
|
1187
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1188
|
-
return null;
|
|
1189
|
-
}
|
|
1190
|
-
if (retryResponse.status < 500) return retryResponse;
|
|
1191
|
-
}
|
|
1192
|
-
return null;
|
|
1193
|
-
}
|
|
1194
|
-
const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
|
|
1195
|
-
if (response.status >= 500) {
|
|
1196
|
-
const recovered = await retryServerErrors(account, runtime);
|
|
1197
|
-
if (recovered === null) {
|
|
1198
|
-
return { type: "retryOuter" };
|
|
1199
|
-
}
|
|
1200
|
-
response = recovered;
|
|
1201
|
-
}
|
|
1202
|
-
if (response.status === 401) {
|
|
1203
|
-
if (allow401Retry) {
|
|
1204
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1205
|
-
try {
|
|
1206
|
-
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1207
|
-
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1208
|
-
return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
if (isAbortError(error)) throw error;
|
|
1211
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1212
|
-
return { type: "retryOuter" };
|
|
1213
|
-
}
|
|
1214
|
-
return { type: "retryOuter" };
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
|
|
1218
|
-
await manager.refresh();
|
|
1219
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1220
|
-
void showToast2(client, "All accounts have auth failures.", "error");
|
|
1221
|
-
throw new Error(
|
|
1222
|
-
`All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1225
|
-
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1226
|
-
return { type: "retryOuter" };
|
|
1227
|
-
}
|
|
1228
|
-
if (response.status === 403) {
|
|
1229
|
-
const revoked = await isRevokedTokenResponse(response);
|
|
1230
|
-
if (revoked) {
|
|
1231
|
-
await manager.markRevoked(accountUuid);
|
|
1232
|
-
await manager.refresh();
|
|
1233
|
-
void showToast2(
|
|
1234
|
-
client,
|
|
1235
|
-
`${getAccountLabel2(account)} disabled: OAuth token revoked.`,
|
|
1236
|
-
"error"
|
|
1237
|
-
);
|
|
1238
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1239
|
-
throw new Error(
|
|
1240
|
-
`All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
return { type: "retryOuter" };
|
|
1244
|
-
}
|
|
1245
|
-
if (from401RefreshRetry) {
|
|
1246
|
-
return { type: "handled", response };
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
if (response.status === 429) {
|
|
1250
|
-
await handleRateLimitResponse2(manager, client, account, response);
|
|
1251
|
-
return { type: "handled" };
|
|
1252
|
-
}
|
|
1253
|
-
return { type: "success", response };
|
|
1254
|
-
};
|
|
1255
|
-
for (let retries = 1; retries <= maxRetries; retries++) {
|
|
1256
|
-
await manager.refresh();
|
|
1257
|
-
const account = await resolveAccount(manager, client);
|
|
1258
|
-
const accountUuid = account.uuid;
|
|
1259
|
-
if (!accountUuid) continue;
|
|
1260
|
-
if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
|
|
1261
|
-
void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
|
|
1262
|
-
}
|
|
1263
|
-
previousAccountUuid = accountUuid;
|
|
1264
|
-
let runtime;
|
|
1265
|
-
let response;
|
|
1266
|
-
try {
|
|
1267
|
-
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1268
|
-
response = await runtime.fetch(input, init);
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
if (isAbortError(error)) throw error;
|
|
1271
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
|
|
1278
|
-
if (transition.type === "retryOuter" || transition.type === "handled") {
|
|
1279
|
-
if (transition.type === "handled" && transition.response) {
|
|
1280
|
-
return transition.response;
|
|
1281
|
-
}
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
await manager.markSuccess(accountUuid);
|
|
1285
|
-
return transition.response;
|
|
1286
|
-
}
|
|
1287
|
-
throw new Error(
|
|
1288
|
-
`Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
|
|
1292
|
-
if (!isTokenRefreshError(error)) return false;
|
|
1293
|
-
if (!account.uuid) return false;
|
|
1294
|
-
const accountUuid = account.uuid;
|
|
1295
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1296
|
-
await manager.markAuthFailure(accountUuid, {
|
|
1297
|
-
ok: false,
|
|
1298
|
-
permanent: error.permanent
|
|
1299
|
-
});
|
|
1300
|
-
await manager.refresh();
|
|
1301
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1302
|
-
void showToast2(client, "All accounts have auth failures.", "error");
|
|
1303
|
-
throw new Error(
|
|
1304
|
-
`All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1308
|
-
return true;
|
|
1309
|
-
}
|
|
1310
|
-
async function resolveAccount(manager, client) {
|
|
1311
|
-
let attempts = 0;
|
|
1312
|
-
while (true) {
|
|
1313
|
-
if (++attempts > MAX_RESOLVE_ATTEMPTS) {
|
|
1314
|
-
throw new Error(
|
|
1315
|
-
`Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
const account = await manager.selectAccount();
|
|
1319
|
-
if (account) return account;
|
|
1320
|
-
if (!manager.hasAnyUsableAccount()) {
|
|
1321
|
-
throw new Error(
|
|
1322
|
-
`All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
const waitMs = manager.getMinWaitTime();
|
|
1326
|
-
if (waitMs <= 0) {
|
|
1327
|
-
throw new Error(
|
|
1328
|
-
`All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
await showToast2(
|
|
1332
|
-
client,
|
|
1333
|
-
`All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
|
|
1334
|
-
"warning"
|
|
1335
|
-
);
|
|
1336
|
-
await sleep2(waitMs);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return {
|
|
1340
|
-
executeWithAccountRotation: executeWithAccountRotation2
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
async function isRevokedTokenResponse(response) {
|
|
1344
|
-
try {
|
|
1345
|
-
const cloned = response.clone();
|
|
1346
|
-
const body = await cloned.text();
|
|
1347
|
-
return body.includes("revoked");
|
|
1348
|
-
} catch {
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// ../multi-account-core/src/proactive-refresh.ts
|
|
1354
|
-
var INITIAL_DELAY_MS = 5e3;
|
|
1355
|
-
function createProactiveRefreshQueueForProvider(dependencies) {
|
|
1356
|
-
const {
|
|
1357
|
-
providerAuthId,
|
|
1358
|
-
getConfig: getConfig2,
|
|
1359
|
-
refreshToken: refreshToken2,
|
|
1360
|
-
isTokenExpired: isTokenExpired2,
|
|
1361
|
-
debugLog: debugLog2
|
|
1362
|
-
} = dependencies;
|
|
1363
|
-
return class ProactiveRefreshQueue {
|
|
1364
|
-
constructor(client, store, onInvalidate) {
|
|
1365
|
-
this.client = client;
|
|
1366
|
-
this.store = store;
|
|
1367
|
-
this.onInvalidate = onInvalidate;
|
|
1368
|
-
}
|
|
1369
|
-
timeoutHandle = null;
|
|
1370
|
-
runToken = 0;
|
|
1371
|
-
inFlight = null;
|
|
1372
|
-
start() {
|
|
1373
|
-
const config = getConfig2();
|
|
1374
|
-
if (!config.proactive_refresh) return;
|
|
1375
|
-
this.runToken++;
|
|
1376
|
-
if (this.timeoutHandle) {
|
|
1377
|
-
clearTimeout(this.timeoutHandle);
|
|
1378
|
-
this.timeoutHandle = null;
|
|
1379
|
-
}
|
|
1380
|
-
this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
|
|
1381
|
-
debugLog2(this.client, "Proactive refresh started", {
|
|
1382
|
-
intervalSeconds: config.proactive_refresh_interval_seconds,
|
|
1383
|
-
bufferSeconds: config.proactive_refresh_buffer_seconds
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
async stop() {
|
|
1387
|
-
this.runToken++;
|
|
1388
|
-
if (this.timeoutHandle) {
|
|
1389
|
-
clearTimeout(this.timeoutHandle);
|
|
1390
|
-
this.timeoutHandle = null;
|
|
1391
|
-
}
|
|
1392
|
-
if (this.inFlight) {
|
|
1393
|
-
await this.inFlight;
|
|
1394
|
-
this.inFlight = null;
|
|
1395
|
-
}
|
|
1396
|
-
debugLog2(this.client, "Proactive refresh stopped");
|
|
1397
|
-
}
|
|
1398
|
-
scheduleNext(token, delayMs) {
|
|
1399
|
-
this.timeoutHandle = setTimeout(() => {
|
|
1400
|
-
if (token !== this.runToken) return;
|
|
1401
|
-
this.inFlight = this.runCheck(token).finally(() => {
|
|
1402
|
-
this.inFlight = null;
|
|
1403
|
-
});
|
|
1404
|
-
}, delayMs);
|
|
1405
|
-
}
|
|
1406
|
-
needsProactiveRefresh(account) {
|
|
1407
|
-
if (!account.accessToken || !account.expiresAt) return false;
|
|
1408
|
-
if (isTokenExpired2(account)) return false;
|
|
1409
|
-
const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
|
|
1410
|
-
return account.expiresAt <= Date.now() + bufferMs;
|
|
1411
|
-
}
|
|
1412
|
-
async runCheck(token) {
|
|
1413
|
-
try {
|
|
1414
|
-
const stored = await this.store.load();
|
|
1415
|
-
if (token !== this.runToken) return;
|
|
1416
|
-
const candidates = stored.accounts.filter(
|
|
1417
|
-
(a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
|
|
1418
|
-
);
|
|
1419
|
-
if (candidates.length === 0) return;
|
|
1420
|
-
debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
|
|
1421
|
-
for (const account of candidates) {
|
|
1422
|
-
if (token !== this.runToken) return;
|
|
1423
|
-
const credentials = await this.store.readCredentials(account.uuid);
|
|
1424
|
-
if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
|
|
1425
|
-
const result = await refreshToken2(credentials.refreshToken, account.uuid, this.client);
|
|
1426
|
-
if (result.ok) {
|
|
1427
|
-
await this.store.mutateAccount(account.uuid, (target) => {
|
|
1428
|
-
target.accessToken = result.patch.accessToken;
|
|
1429
|
-
target.expiresAt = result.patch.expiresAt;
|
|
1430
|
-
if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
|
|
1431
|
-
if (result.patch.uuid) target.uuid = result.patch.uuid;
|
|
1432
|
-
if (result.patch.email) target.email = result.patch.email;
|
|
1433
|
-
if (result.patch.accountId) target.accountId = result.patch.accountId;
|
|
1434
|
-
target.consecutiveAuthFailures = 0;
|
|
1435
|
-
target.isAuthDisabled = false;
|
|
1436
|
-
target.authDisabledReason = void 0;
|
|
1437
|
-
});
|
|
1438
|
-
this.onInvalidate?.(account.uuid);
|
|
1439
|
-
} else {
|
|
1440
|
-
await this.persistFailure(account, result.permanent);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
debugLog2(this.client, `Proactive refresh check error: ${error}`);
|
|
1445
|
-
} finally {
|
|
1446
|
-
if (token === this.runToken) {
|
|
1447
|
-
const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
|
|
1448
|
-
this.scheduleNext(token, intervalMs);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
async persistFailure(account, permanent) {
|
|
1453
|
-
try {
|
|
1454
|
-
const accountUuid = account.uuid;
|
|
1455
|
-
if (!accountUuid) return;
|
|
1456
|
-
if (permanent) {
|
|
1457
|
-
const removed = await this.store.removeAccount(accountUuid);
|
|
1458
|
-
if (!removed) return;
|
|
1459
|
-
this.onInvalidate?.(accountUuid);
|
|
1460
|
-
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
await this.store.mutateStorage((storage) => {
|
|
1464
|
-
const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
|
|
1465
|
-
if (!target) return;
|
|
1466
|
-
target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
|
|
1467
|
-
const maxFailures = getConfig2().max_consecutive_auth_failures;
|
|
1468
|
-
const usableCount = storage.accounts.filter(
|
|
1469
|
-
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
|
|
1470
|
-
).length;
|
|
1471
|
-
if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
1472
|
-
target.isAuthDisabled = true;
|
|
1473
|
-
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1474
|
-
}
|
|
1475
|
-
});
|
|
1476
|
-
} catch {
|
|
1477
|
-
debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
1481
|
-
const storage = await this.store.load();
|
|
1482
|
-
if (storage.accounts.length > 0) return;
|
|
1483
|
-
await this.client.auth.set({
|
|
1484
|
-
path: { id: providerAuthId },
|
|
1485
|
-
body: getClearedOAuthBody()
|
|
1486
|
-
}).catch(() => {
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// ../multi-account-core/src/rate-limit.ts
|
|
1493
|
-
var USAGE_FETCH_COOLDOWN_MS = 3e4;
|
|
1494
|
-
function createRateLimitHandlers(dependencies) {
|
|
1495
|
-
const {
|
|
1496
|
-
fetchUsage: fetchUsage2,
|
|
1497
|
-
getConfig: getConfig2,
|
|
1498
|
-
formatWaitTime: formatWaitTime2,
|
|
1499
|
-
getAccountLabel: getAccountLabel2,
|
|
1500
|
-
showToast: showToast2
|
|
1501
|
-
} = dependencies;
|
|
1502
|
-
function retryAfterMsFromResponse2(response) {
|
|
1503
|
-
const retryAfterMs = response.headers.get("retry-after-ms");
|
|
1504
|
-
if (retryAfterMs) {
|
|
1505
|
-
const parsed = parseInt(retryAfterMs, 10);
|
|
1506
|
-
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
1507
|
-
}
|
|
1508
|
-
const retryAfter = response.headers.get("retry-after");
|
|
1509
|
-
if (retryAfter) {
|
|
1510
|
-
const parsed = parseInt(retryAfter, 10);
|
|
1511
|
-
if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
|
|
1512
|
-
}
|
|
1513
|
-
return getConfig2().default_retry_after_ms;
|
|
1514
|
-
}
|
|
1515
|
-
function getResetMsFromUsage2(account) {
|
|
1516
|
-
const usage = account.cachedUsage;
|
|
1517
|
-
if (!usage) return null;
|
|
1518
|
-
const now = Date.now();
|
|
1519
|
-
const candidates = [];
|
|
1520
|
-
if (usage.five_hour?.resets_at) {
|
|
1521
|
-
const ms = Date.parse(usage.five_hour.resets_at) - now;
|
|
1522
|
-
if (ms > 0) candidates.push(ms);
|
|
1523
|
-
}
|
|
1524
|
-
if (usage.seven_day?.resets_at) {
|
|
1525
|
-
const ms = Date.parse(usage.seven_day.resets_at) - now;
|
|
1526
|
-
if (ms > 0) candidates.push(ms);
|
|
1527
|
-
}
|
|
1528
|
-
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
1529
|
-
}
|
|
1530
|
-
async function fetchUsageLimits2(accessToken, accountId) {
|
|
1531
|
-
if (!accessToken) return null;
|
|
1532
|
-
try {
|
|
1533
|
-
const result = await fetchUsage2(accessToken, accountId);
|
|
1534
|
-
return result.ok ? result.data : null;
|
|
1535
|
-
} catch {
|
|
1536
|
-
return null;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
async function handleRateLimitResponse2(manager, client, account, response) {
|
|
1540
|
-
if (!account.uuid) return;
|
|
1541
|
-
const resetMs = getResetMsFromUsage2(account) ?? retryAfterMsFromResponse2(response);
|
|
1542
|
-
await manager.markRateLimited(account.uuid, resetMs);
|
|
1543
|
-
const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
|
|
1544
|
-
if (shouldFetchUsage) {
|
|
1545
|
-
const usage = await fetchUsageLimits2(account.accessToken, account.accountId);
|
|
1546
|
-
if (usage) {
|
|
1547
|
-
await manager.applyUsageCache(account.uuid, usage);
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
if (manager.getAccountCount() > 1) {
|
|
1551
|
-
void showToast2(
|
|
1552
|
-
client,
|
|
1553
|
-
`${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
|
|
1554
|
-
"warning"
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return {
|
|
1559
|
-
retryAfterMsFromResponse: retryAfterMsFromResponse2,
|
|
1560
|
-
getResetMsFromUsage: getResetMsFromUsage2,
|
|
1561
|
-
fetchUsageLimits: fetchUsageLimits2,
|
|
1562
|
-
handleRateLimitResponse: handleRateLimitResponse2
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// ../multi-account-core/src/auth-migration.ts
|
|
1567
|
-
import { promises as fs5 } from "node:fs";
|
|
1568
|
-
import { join as join6 } from "node:path";
|
|
1569
|
-
var AUTH_JSON_FILENAME = "auth.json";
|
|
1570
|
-
function isValidOAuthCredential(value) {
|
|
1571
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1572
|
-
const candidate = value;
|
|
1573
|
-
return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
|
|
1574
|
-
}
|
|
1575
|
-
function resolveAuthJsonPath() {
|
|
1576
|
-
return join6(getConfigDir2(), AUTH_JSON_FILENAME);
|
|
1577
|
-
}
|
|
1578
|
-
async function readAuthJson() {
|
|
1579
|
-
const authPath = resolveAuthJsonPath();
|
|
1580
|
-
let content;
|
|
1581
|
-
try {
|
|
1582
|
-
content = await fs5.readFile(authPath, "utf-8");
|
|
1583
|
-
} catch {
|
|
1584
|
-
return null;
|
|
1585
|
-
}
|
|
1586
|
-
try {
|
|
1587
|
-
const parsed = JSON.parse(content);
|
|
1588
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1589
|
-
return null;
|
|
1590
|
-
}
|
|
1591
|
-
return parsed;
|
|
1592
|
-
} catch {
|
|
1593
|
-
return null;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
async function migrateFromAuthJson(providerKey, store) {
|
|
1597
|
-
const storage = await store.load();
|
|
1598
|
-
const hasExistingAccounts = storage.accounts.length > 0;
|
|
1599
|
-
if (hasExistingAccounts) return false;
|
|
1600
|
-
const authData = await readAuthJson();
|
|
1601
|
-
if (!authData) return false;
|
|
1602
|
-
const providerCredential = authData[providerKey];
|
|
1603
|
-
if (!isValidOAuthCredential(providerCredential)) return false;
|
|
1604
|
-
const now = Date.now();
|
|
1605
|
-
const newAccount = {
|
|
1606
|
-
uuid: crypto.randomUUID(),
|
|
1607
|
-
refreshToken: providerCredential.refresh,
|
|
1608
|
-
accessToken: providerCredential.access,
|
|
1609
|
-
expiresAt: providerCredential.expires,
|
|
1610
|
-
addedAt: now,
|
|
1611
|
-
lastUsed: now,
|
|
1612
|
-
enabled: true,
|
|
1613
|
-
planTier: "",
|
|
1614
|
-
consecutiveAuthFailures: 0,
|
|
1615
|
-
isAuthDisabled: false
|
|
1616
|
-
};
|
|
1617
|
-
await store.addAccount(newAccount);
|
|
1618
|
-
await store.setActiveUuid(newAccount.uuid);
|
|
1619
|
-
return true;
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
// ../multi-account-core/src/ui/ansi.ts
|
|
1623
|
-
var ANSI = {
|
|
1624
|
-
hide: "\x1B[?25l",
|
|
1625
|
-
show: "\x1B[?25h",
|
|
1626
|
-
up: (n = 1) => `\x1B[${n}A`,
|
|
1627
|
-
down: (n = 1) => `\x1B[${n}B`,
|
|
1628
|
-
clearLine: "\x1B[2K",
|
|
1629
|
-
cyan: "\x1B[36m",
|
|
1630
|
-
green: "\x1B[32m",
|
|
1631
|
-
red: "\x1B[31m",
|
|
1632
|
-
yellow: "\x1B[33m",
|
|
1633
|
-
dim: "\x1B[2m",
|
|
1634
|
-
bold: "\x1B[1m",
|
|
1635
|
-
reset: "\x1B[0m"
|
|
1636
|
-
};
|
|
1637
|
-
function parseKey(data) {
|
|
1638
|
-
const s = data.toString();
|
|
1639
|
-
if (s === "\x1B[A" || s === "\x1BOA") return "up";
|
|
1640
|
-
if (s === "\x1B[B" || s === "\x1BOB") return "down";
|
|
1641
|
-
if (s === "\r" || s === "\n") return "enter";
|
|
1642
|
-
if (s === "") return "escape";
|
|
1643
|
-
if (s === "\x1B") return "escape-start";
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
function isTTY() {
|
|
1647
|
-
return Boolean(process.stdin.isTTY);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// ../multi-account-core/src/ui/select.ts
|
|
1651
|
-
var ESCAPE_TIMEOUT_MS = 50;
|
|
1652
|
-
var COLOR_MAP = {
|
|
1653
|
-
red: ANSI.red,
|
|
1654
|
-
green: ANSI.green,
|
|
1655
|
-
yellow: ANSI.yellow,
|
|
1656
|
-
cyan: ANSI.cyan
|
|
1657
|
-
};
|
|
1658
|
-
async function select(items, options) {
|
|
1659
|
-
if (!isTTY()) {
|
|
1660
|
-
throw new Error("Interactive select requires a TTY terminal");
|
|
1661
|
-
}
|
|
1662
|
-
const enabledItems = items.filter((i) => !i.disabled && !i.separator);
|
|
1663
|
-
if (enabledItems.length === 0) {
|
|
1664
|
-
throw new Error("All items disabled");
|
|
1665
|
-
}
|
|
1666
|
-
if (enabledItems.length === 1) {
|
|
1667
|
-
return enabledItems[0].value;
|
|
1668
|
-
}
|
|
1669
|
-
const { message, subtitle } = options;
|
|
1670
|
-
const { stdin, stdout } = process;
|
|
1671
|
-
let cursor = items.findIndex((i) => !i.disabled && !i.separator);
|
|
1672
|
-
if (cursor === -1) cursor = 0;
|
|
1673
|
-
let escapeTimeout = null;
|
|
1674
|
-
let isCleanedUp = false;
|
|
1675
|
-
let isFirstRender = true;
|
|
1676
|
-
const getTotalLines = () => {
|
|
1677
|
-
const subtitleLines = subtitle ? 3 : 0;
|
|
1678
|
-
return 1 + subtitleLines + items.length + 1 + 1;
|
|
1679
|
-
};
|
|
1680
|
-
const renderItemLabel = (item, isSelected) => {
|
|
1681
|
-
const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
|
|
1682
|
-
if (item.disabled) {
|
|
1683
|
-
return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
|
|
1684
|
-
}
|
|
1685
|
-
const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
|
|
1686
|
-
if (isSelected) {
|
|
1687
|
-
const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
|
|
1688
|
-
return `${label}${hintSuffix}`;
|
|
1689
|
-
}
|
|
1690
|
-
const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
|
|
1691
|
-
return `${dimLabel}${hintSuffix}`;
|
|
1692
|
-
};
|
|
1693
|
-
const render = () => {
|
|
1694
|
-
const totalLines = getTotalLines();
|
|
1695
|
-
if (!isFirstRender) {
|
|
1696
|
-
stdout.write(ANSI.up(totalLines) + "\r");
|
|
1697
|
-
}
|
|
1698
|
-
isFirstRender = false;
|
|
1699
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
|
|
1700
|
-
`);
|
|
1701
|
-
if (subtitle) {
|
|
1702
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
|
|
1703
|
-
`);
|
|
1704
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
|
|
1705
|
-
`);
|
|
1706
|
-
stdout.write(`${ANSI.clearLine}
|
|
1707
|
-
`);
|
|
1708
|
-
}
|
|
1709
|
-
for (let i = 0; i < items.length; i++) {
|
|
1710
|
-
const item = items[i];
|
|
1711
|
-
if (!item) continue;
|
|
1712
|
-
if (item.separator) {
|
|
1713
|
-
stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
|
|
1714
|
-
`);
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
const isSelected = i === cursor;
|
|
1718
|
-
const labelText = renderItemLabel(item, isSelected);
|
|
1719
|
-
const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
|
|
1720
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
|
|
1721
|
-
`);
|
|
1722
|
-
}
|
|
1723
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
|
|
1724
|
-
`);
|
|
1725
|
-
stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
|
|
1726
|
-
`);
|
|
1727
|
-
};
|
|
1728
|
-
return new Promise((resolve) => {
|
|
1729
|
-
const wasRaw = stdin.isRaw ?? false;
|
|
1730
|
-
const cleanup = () => {
|
|
1731
|
-
if (isCleanedUp) return;
|
|
1732
|
-
isCleanedUp = true;
|
|
1733
|
-
if (escapeTimeout) {
|
|
1734
|
-
clearTimeout(escapeTimeout);
|
|
1735
|
-
escapeTimeout = null;
|
|
1736
|
-
}
|
|
1737
|
-
try {
|
|
1738
|
-
stdin.removeListener("data", onKey);
|
|
1739
|
-
stdin.setRawMode(wasRaw);
|
|
1740
|
-
stdin.pause();
|
|
1741
|
-
stdout.write(ANSI.show);
|
|
1742
|
-
} catch {
|
|
1743
|
-
}
|
|
1744
|
-
process.removeListener("SIGINT", onSignal);
|
|
1745
|
-
process.removeListener("SIGTERM", onSignal);
|
|
1746
|
-
};
|
|
1747
|
-
const onSignal = () => {
|
|
1748
|
-
cleanup();
|
|
1749
|
-
resolve(null);
|
|
1750
|
-
};
|
|
1751
|
-
const finishWithValue = (value) => {
|
|
1752
|
-
cleanup();
|
|
1753
|
-
resolve(value);
|
|
1754
|
-
};
|
|
1755
|
-
const findNextSelectable = (from, direction) => {
|
|
1756
|
-
if (items.length === 0) return from;
|
|
1757
|
-
let next = from;
|
|
1758
|
-
do {
|
|
1759
|
-
next = (next + direction + items.length) % items.length;
|
|
1760
|
-
} while (items[next]?.disabled || items[next]?.separator);
|
|
1761
|
-
return next;
|
|
1762
|
-
};
|
|
1763
|
-
const onKey = (data) => {
|
|
1764
|
-
if (escapeTimeout) {
|
|
1765
|
-
clearTimeout(escapeTimeout);
|
|
1766
|
-
escapeTimeout = null;
|
|
1767
|
-
}
|
|
1768
|
-
const action = parseKey(data);
|
|
1769
|
-
switch (action) {
|
|
1770
|
-
case "up":
|
|
1771
|
-
cursor = findNextSelectable(cursor, -1);
|
|
1772
|
-
render();
|
|
1773
|
-
return;
|
|
1774
|
-
case "down":
|
|
1775
|
-
cursor = findNextSelectable(cursor, 1);
|
|
1776
|
-
render();
|
|
1777
|
-
return;
|
|
1778
|
-
case "enter":
|
|
1779
|
-
finishWithValue(items[cursor]?.value ?? null);
|
|
1780
|
-
return;
|
|
1781
|
-
case "escape":
|
|
1782
|
-
finishWithValue(null);
|
|
1783
|
-
return;
|
|
1784
|
-
case "escape-start":
|
|
1785
|
-
escapeTimeout = setTimeout(() => {
|
|
1786
|
-
finishWithValue(null);
|
|
1787
|
-
}, ESCAPE_TIMEOUT_MS);
|
|
1788
|
-
return;
|
|
1789
|
-
default:
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
};
|
|
1793
|
-
process.once("SIGINT", onSignal);
|
|
1794
|
-
process.once("SIGTERM", onSignal);
|
|
1795
|
-
try {
|
|
1796
|
-
stdin.setRawMode(true);
|
|
1797
|
-
} catch {
|
|
1798
|
-
cleanup();
|
|
1799
|
-
resolve(null);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
stdin.resume();
|
|
1803
|
-
stdout.write(ANSI.hide);
|
|
1804
|
-
render();
|
|
1805
|
-
stdin.on("data", onKey);
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// ../multi-account-core/src/ui/confirm.ts
|
|
1810
|
-
async function confirm(message, defaultYes = false) {
|
|
1811
|
-
const items = defaultYes ? [
|
|
1812
|
-
{ label: "Yes", value: true },
|
|
1813
|
-
{ label: "No", value: false }
|
|
1814
|
-
] : [
|
|
1815
|
-
{ label: "No", value: false },
|
|
1816
|
-
{ label: "Yes", value: true }
|
|
1817
|
-
];
|
|
1818
|
-
const result = await select(items, { message });
|
|
1819
|
-
return result ?? false;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// ../multi-account-core/src/adapters/anthropic.ts
|
|
1823
|
-
var 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
|
-
|
|
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
|
|
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 "
|
|
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 "
|
|
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
|
|
148
|
+
import * as v2 from "valibot";
|
|
2331
149
|
|
|
2332
150
|
// src/types.ts
|
|
2333
|
-
import * as
|
|
2334
|
-
var
|
|
2335
|
-
type:
|
|
2336
|
-
refresh:
|
|
2337
|
-
access:
|
|
2338
|
-
expires:
|
|
151
|
+
import * as v from "valibot";
|
|
152
|
+
var OAuthCredentialsSchema = v.object({
|
|
153
|
+
type: v.literal("oauth"),
|
|
154
|
+
refresh: v.string(),
|
|
155
|
+
access: v.string(),
|
|
156
|
+
expires: v.number()
|
|
2339
157
|
});
|
|
2340
|
-
var
|
|
2341
|
-
utilization:
|
|
2342
|
-
resets_at:
|
|
158
|
+
var UsageLimitEntrySchema = v.object({
|
|
159
|
+
utilization: v.number(),
|
|
160
|
+
resets_at: v.nullable(v.string())
|
|
2343
161
|
});
|
|
2344
|
-
var
|
|
2345
|
-
five_hour:
|
|
2346
|
-
seven_day:
|
|
2347
|
-
seven_day_sonnet:
|
|
162
|
+
var UsageLimitsSchema = v.object({
|
|
163
|
+
five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
164
|
+
seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
|
|
165
|
+
seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
|
|
2348
166
|
});
|
|
2349
|
-
var
|
|
2350
|
-
accessToken:
|
|
2351
|
-
expiresAt:
|
|
2352
|
-
refreshToken:
|
|
2353
|
-
uuid:
|
|
2354
|
-
email:
|
|
167
|
+
var CredentialRefreshPatchSchema = v.object({
|
|
168
|
+
accessToken: v.string(),
|
|
169
|
+
expiresAt: v.number(),
|
|
170
|
+
refreshToken: v.optional(v.string()),
|
|
171
|
+
uuid: v.optional(v.string()),
|
|
172
|
+
email: v.optional(v.string())
|
|
2355
173
|
});
|
|
2356
|
-
var
|
|
2357
|
-
uuid:
|
|
2358
|
-
label:
|
|
2359
|
-
email:
|
|
2360
|
-
planTier:
|
|
2361
|
-
refreshToken:
|
|
2362
|
-
accessToken:
|
|
2363
|
-
expiresAt:
|
|
2364
|
-
addedAt:
|
|
2365
|
-
lastUsed:
|
|
2366
|
-
enabled:
|
|
2367
|
-
rateLimitResetAt:
|
|
2368
|
-
cachedUsage:
|
|
2369
|
-
cachedUsageAt:
|
|
2370
|
-
consecutiveAuthFailures:
|
|
2371
|
-
isAuthDisabled:
|
|
2372
|
-
authDisabledReason:
|
|
174
|
+
var StoredAccountSchema = v.object({
|
|
175
|
+
uuid: v.optional(v.string()),
|
|
176
|
+
label: v.optional(v.string()),
|
|
177
|
+
email: v.optional(v.string()),
|
|
178
|
+
planTier: v.optional(v.string(), ""),
|
|
179
|
+
refreshToken: v.string(),
|
|
180
|
+
accessToken: v.optional(v.string()),
|
|
181
|
+
expiresAt: v.optional(v.number()),
|
|
182
|
+
addedAt: v.number(),
|
|
183
|
+
lastUsed: v.number(),
|
|
184
|
+
enabled: v.optional(v.boolean(), true),
|
|
185
|
+
rateLimitResetAt: v.optional(v.number()),
|
|
186
|
+
cachedUsage: v.optional(UsageLimitsSchema),
|
|
187
|
+
cachedUsageAt: v.optional(v.number()),
|
|
188
|
+
consecutiveAuthFailures: v.optional(v.number(), 0),
|
|
189
|
+
isAuthDisabled: v.optional(v.boolean(), false),
|
|
190
|
+
authDisabledReason: v.optional(v.string())
|
|
2373
191
|
});
|
|
2374
|
-
var
|
|
2375
|
-
version:
|
|
2376
|
-
accounts:
|
|
2377
|
-
activeAccountUuid:
|
|
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 =
|
|
2380
|
-
access_token:
|
|
2381
|
-
refresh_token:
|
|
2382
|
-
expires_in:
|
|
2383
|
-
account:
|
|
2384
|
-
uuid:
|
|
2385
|
-
email_address:
|
|
197
|
+
var TokenResponseSchema = v.object({
|
|
198
|
+
access_token: v.string(),
|
|
199
|
+
refresh_token: v.optional(v.string()),
|
|
200
|
+
expires_in: v.number(),
|
|
201
|
+
account: v.optional(v.object({
|
|
202
|
+
uuid: v.optional(v.string()),
|
|
203
|
+
email_address: v.optional(v.string())
|
|
2386
204
|
}))
|
|
2387
205
|
});
|
|
2388
|
-
var
|
|
2389
|
-
var
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
227
|
+
proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
|
|
2410
228
|
/** Suppress toast notifications */
|
|
2411
|
-
quiet_mode:
|
|
229
|
+
quiet_mode: v.optional(v.boolean(), false),
|
|
2412
230
|
/** Enable debug logging */
|
|
2413
|
-
debug:
|
|
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 =
|
|
2419
|
-
account:
|
|
2420
|
-
email:
|
|
2421
|
-
has_claude_pro:
|
|
2422
|
-
has_claude_max:
|
|
236
|
+
var ProfileResponseSchema = v2.object({
|
|
237
|
+
account: v2.object({
|
|
238
|
+
email: v2.optional(v2.string()),
|
|
239
|
+
has_claude_pro: v2.optional(v2.boolean(), false),
|
|
240
|
+
has_claude_max: v2.optional(v2.boolean(), false)
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 } =
|
|
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(
|
|
2898
|
-
return (
|
|
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
|
-
|
|
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
|
|
3132
|
-
import { createInterface } from "
|
|
3133
|
-
import { exec } from "
|
|
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:
|
|
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 "
|
|
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,
|
|
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
|
-
...
|
|
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",
|
|
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
|
|
3900
|
-
const
|
|
3901
|
-
const
|
|
3902
|
-
const
|
|
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": (
|
|
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"] =
|
|
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
|