opencode-multi-account-core 0.2.3 → 0.2.5
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/account-manager.d.ts +1 -1
- package/dist/adapters/types.d.ts +2 -0
- package/dist/cascade-state.d.ts +11 -0
- package/dist/executor.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +575 -103
- package/dist/pool-config-store.d.ts +3 -0
- package/dist/pool-manager.d.ts +33 -0
- package/dist/pool-types.d.ts +59 -0
- package/dist/proactive-refresh.d.ts +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils.d.ts +6 -0
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ export interface AccountManagerInstance {
|
|
|
35
35
|
validateNonActiveTokens(client: PluginClient): Promise<void>;
|
|
36
36
|
removeAccount(index: number): Promise<boolean>;
|
|
37
37
|
clearAllAccounts(): Promise<void>;
|
|
38
|
-
addAccount(auth: OAuthCredentials): Promise<void>;
|
|
38
|
+
addAccount(auth: OAuthCredentials, email?: string): Promise<void>;
|
|
39
39
|
toggleEnabled(uuid: string): Promise<void>;
|
|
40
40
|
replaceAccountCredentials(uuid: string, auth: OAuthCredentials): Promise<void>;
|
|
41
41
|
retryAuth(uuid: string, client: PluginClient): Promise<TokenRefreshResult>;
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface OAuthAdapter {
|
|
|
19
19
|
oauthBetaHeader: string;
|
|
20
20
|
requestBetaHeader: string;
|
|
21
21
|
cliUserAgent: string;
|
|
22
|
+
cliVersion: string;
|
|
23
|
+
billingSalt: string;
|
|
22
24
|
toolPrefix: string;
|
|
23
25
|
accountStorageFilename: string;
|
|
24
26
|
transform: OAuthAdapterTransformConfig;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CascadeState } from "./pool-types";
|
|
2
|
+
export declare class CascadeStateManager {
|
|
3
|
+
suppressNextStartTurn: boolean;
|
|
4
|
+
private cascadeState;
|
|
5
|
+
startTurn(prompt: string, currentAccountUuid?: string): CascadeState;
|
|
6
|
+
ensureCascadeState(prompt: string, currentAccountUuid?: string): CascadeState;
|
|
7
|
+
markAttempted(accountUuid: string): void;
|
|
8
|
+
markVisitedChainIndex(index: number): void;
|
|
9
|
+
clearCascadeState(): void;
|
|
10
|
+
getSnapshot(): CascadeState | null;
|
|
11
|
+
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ManagedAccount, type PluginClient, type TokenRefreshResult } from "./types";
|
|
2
2
|
export interface ExecutorAccountManager {
|
|
3
3
|
getAccountCount(): number;
|
|
4
4
|
refresh(): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,7 @@ export * from "./ui/ansi";
|
|
|
14
14
|
export * from "./ui/confirm";
|
|
15
15
|
export * from "./ui/select";
|
|
16
16
|
export * from "./adapters";
|
|
17
|
+
export * from "./pool-types";
|
|
18
|
+
export * from "./pool-config-store";
|
|
19
|
+
export * from "./pool-manager";
|
|
20
|
+
export * from "./cascade-state";
|
package/dist/index.js
CHANGED
|
@@ -81,6 +81,23 @@ var PluginConfigSchema = v.object({
|
|
|
81
81
|
quiet_mode: v.optional(v.boolean(), false),
|
|
82
82
|
debug: v.optional(v.boolean(), false)
|
|
83
83
|
});
|
|
84
|
+
var TokenRefreshError = class _TokenRefreshError extends Error {
|
|
85
|
+
status;
|
|
86
|
+
permanent;
|
|
87
|
+
constructor(permanent, status) {
|
|
88
|
+
super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
|
|
89
|
+
this.name = "TokenRefreshError";
|
|
90
|
+
this.status = status;
|
|
91
|
+
this.permanent = permanent;
|
|
92
|
+
Object.setPrototypeOf(this, _TokenRefreshError.prototype);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function isTokenRefreshError(error) {
|
|
96
|
+
if (error instanceof TokenRefreshError) return true;
|
|
97
|
+
if (!(error instanceof Error)) return false;
|
|
98
|
+
const candidate = error;
|
|
99
|
+
return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
|
|
100
|
+
}
|
|
84
101
|
|
|
85
102
|
// src/config.ts
|
|
86
103
|
var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
|
|
@@ -224,6 +241,14 @@ function createMinimalClient() {
|
|
|
224
241
|
}
|
|
225
242
|
};
|
|
226
243
|
}
|
|
244
|
+
function getClearedOAuthBody() {
|
|
245
|
+
return {
|
|
246
|
+
type: "oauth",
|
|
247
|
+
refresh: "",
|
|
248
|
+
access: "",
|
|
249
|
+
expires: 0
|
|
250
|
+
};
|
|
251
|
+
}
|
|
227
252
|
|
|
228
253
|
// src/claims.ts
|
|
229
254
|
var CLAIMS_FILENAME = "multiauth-claims.json";
|
|
@@ -661,13 +686,7 @@ function createAccountManagerForProvider(dependencies) {
|
|
|
661
686
|
});
|
|
662
687
|
}
|
|
663
688
|
async markRevoked(uuid) {
|
|
664
|
-
await this.
|
|
665
|
-
account.isAuthDisabled = true;
|
|
666
|
-
account.authDisabledReason = "OAuth token revoked (403)";
|
|
667
|
-
account.accessToken = void 0;
|
|
668
|
-
account.expiresAt = void 0;
|
|
669
|
-
});
|
|
670
|
-
this.runtimeFactory?.invalidate(uuid);
|
|
689
|
+
await this.removeAccountByUuid(uuid);
|
|
671
690
|
}
|
|
672
691
|
async markSuccess(uuid) {
|
|
673
692
|
this.last429Map.delete(uuid);
|
|
@@ -690,15 +709,32 @@ function createAccountManagerForProvider(dependencies) {
|
|
|
690
709
|
}).catch(() => {
|
|
691
710
|
});
|
|
692
711
|
}
|
|
712
|
+
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
713
|
+
if (!this.client) return;
|
|
714
|
+
const storage = await this.store.load();
|
|
715
|
+
if (storage.accounts.length > 0) return;
|
|
716
|
+
await this.client.auth.set({
|
|
717
|
+
path: { id: providerAuthId },
|
|
718
|
+
body: getClearedOAuthBody()
|
|
719
|
+
}).catch(() => {
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
async removeAccountByUuid(uuid) {
|
|
723
|
+
const removed = await this.store.removeAccount(uuid);
|
|
724
|
+
if (!removed) return;
|
|
725
|
+
this.last429Map.delete(uuid);
|
|
726
|
+
this.runtimeFactory?.invalidate(uuid);
|
|
727
|
+
await this.refresh();
|
|
728
|
+
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
729
|
+
}
|
|
693
730
|
async markAuthFailure(uuid, result) {
|
|
731
|
+
if (!result.ok && result.permanent) {
|
|
732
|
+
await this.removeAccountByUuid(uuid);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
694
735
|
await this.store.mutateStorage((storage) => {
|
|
695
736
|
const account = storage.accounts.find((entry) => entry.uuid === uuid);
|
|
696
737
|
if (!account) return;
|
|
697
|
-
if (!result.ok && result.permanent) {
|
|
698
|
-
account.isAuthDisabled = true;
|
|
699
|
-
account.authDisabledReason = "Token permanently rejected (400/401/403)";
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
738
|
account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
|
|
703
739
|
const maxFailures = getConfig().max_consecutive_auth_failures;
|
|
704
740
|
const usableCount = storage.accounts.filter(
|
|
@@ -795,11 +831,21 @@ function createAccountManagerForProvider(dependencies) {
|
|
|
795
831
|
this.cached = [];
|
|
796
832
|
this.activeAccountUuid = void 0;
|
|
797
833
|
}
|
|
798
|
-
async addAccount(auth) {
|
|
834
|
+
async addAccount(auth, email) {
|
|
799
835
|
if (!auth.refresh) return;
|
|
800
|
-
const
|
|
801
|
-
if (
|
|
836
|
+
const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
|
|
837
|
+
if (existingByToken) return;
|
|
838
|
+
if (email) {
|
|
839
|
+
const existingByEmail = this.cached.find(
|
|
840
|
+
(account) => account.email && account.email === email
|
|
841
|
+
);
|
|
842
|
+
if (existingByEmail?.uuid) {
|
|
843
|
+
await this.replaceAccountCredentials(existingByEmail.uuid, auth);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
802
847
|
const newAccount = this.createNewAccount(auth, Date.now());
|
|
848
|
+
if (email) newAccount.email = email;
|
|
803
849
|
await this.store.addAccount(newAccount);
|
|
804
850
|
this.activeAccountUuid = newAccount.uuid;
|
|
805
851
|
await this.store.setActiveUuid(newAccount.uuid);
|
|
@@ -1125,7 +1171,6 @@ var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
|
|
|
1125
1171
|
var MAX_RESOLVE_ATTEMPTS = 10;
|
|
1126
1172
|
var SERVER_RETRY_BASE_MS = 1e3;
|
|
1127
1173
|
var SERVER_RETRY_MAX_MS = 4e3;
|
|
1128
|
-
var PERMANENT_AUTH_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
|
|
1129
1174
|
function isAbortError(error) {
|
|
1130
1175
|
return error instanceof Error && error.name === "AbortError";
|
|
1131
1176
|
}
|
|
@@ -1139,80 +1184,49 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1139
1184
|
} = dependencies;
|
|
1140
1185
|
async function executeWithAccountRotation(manager, runtimeFactory, client, input, init) {
|
|
1141
1186
|
const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
|
|
1142
|
-
let retries = 0;
|
|
1143
1187
|
let previousAccountUuid;
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
);
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
let response;
|
|
1160
|
-
try {
|
|
1161
|
-
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1162
|
-
response = await runtime.fetch(input, init);
|
|
1163
|
-
} catch (error) {
|
|
1164
|
-
if (isAbortError(error)) throw error;
|
|
1165
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1166
|
-
continue;
|
|
1188
|
+
async function retryServerErrors(account, runtime) {
|
|
1189
|
+
for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
|
|
1190
|
+
const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
|
|
1191
|
+
const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
|
|
1192
|
+
await sleep2(jitteredBackoff);
|
|
1193
|
+
let retryResponse;
|
|
1194
|
+
try {
|
|
1195
|
+
retryResponse = await runtime.fetch(input, init);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
if (isAbortError(error)) throw error;
|
|
1198
|
+
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1202
|
+
return null;
|
|
1167
1203
|
}
|
|
1168
|
-
|
|
1169
|
-
continue;
|
|
1204
|
+
if (retryResponse.status < 500) return retryResponse;
|
|
1170
1205
|
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
|
|
1171
1209
|
if (response.status >= 500) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1210
|
+
const recovered = await retryServerErrors(account, runtime);
|
|
1211
|
+
if (recovered === null) {
|
|
1212
|
+
return { type: "retryOuter" };
|
|
1213
|
+
}
|
|
1214
|
+
response = recovered;
|
|
1215
|
+
}
|
|
1216
|
+
if (response.status === 401) {
|
|
1217
|
+
if (allow401Retry) {
|
|
1218
|
+
runtimeFactory.invalidate(accountUuid);
|
|
1179
1219
|
try {
|
|
1180
|
-
|
|
1220
|
+
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1221
|
+
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1222
|
+
return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
|
|
1181
1223
|
} catch (error) {
|
|
1182
1224
|
if (isAbortError(error)) throw error;
|
|
1183
1225
|
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1184
|
-
|
|
1185
|
-
break;
|
|
1226
|
+
return { type: "retryOuter" };
|
|
1186
1227
|
}
|
|
1187
|
-
|
|
1188
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1189
|
-
break;
|
|
1228
|
+
return { type: "retryOuter" };
|
|
1190
1229
|
}
|
|
1191
|
-
if (serverResponse.status < 500) break;
|
|
1192
|
-
}
|
|
1193
|
-
if (authFailureDuringServerRetry) {
|
|
1194
|
-
continue;
|
|
1195
|
-
}
|
|
1196
|
-
if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
|
|
1197
|
-
continue;
|
|
1198
|
-
}
|
|
1199
|
-
response = serverResponse;
|
|
1200
|
-
}
|
|
1201
|
-
if (response.status === 401) {
|
|
1202
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1203
|
-
try {
|
|
1204
|
-
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1205
|
-
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1206
|
-
if (retryResponse.status !== 401) {
|
|
1207
|
-
await manager.markSuccess(accountUuid);
|
|
1208
|
-
return retryResponse;
|
|
1209
|
-
}
|
|
1210
|
-
} catch (error) {
|
|
1211
|
-
if (isAbortError(error)) throw error;
|
|
1212
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
continue;
|
|
1216
1230
|
}
|
|
1217
1231
|
await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
|
|
1218
1232
|
await manager.refresh();
|
|
@@ -1223,7 +1237,7 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1223
1237
|
);
|
|
1224
1238
|
}
|
|
1225
1239
|
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1226
|
-
|
|
1240
|
+
return { type: "retryOuter" };
|
|
1227
1241
|
}
|
|
1228
1242
|
if (response.status === 403) {
|
|
1229
1243
|
const revoked = await isRevokedTokenResponse(response);
|
|
@@ -1240,26 +1254,62 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1240
1254
|
`All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1241
1255
|
);
|
|
1242
1256
|
}
|
|
1243
|
-
|
|
1257
|
+
return { type: "retryOuter" };
|
|
1258
|
+
}
|
|
1259
|
+
if (from401RefreshRetry) {
|
|
1260
|
+
return { type: "handled", response };
|
|
1244
1261
|
}
|
|
1245
1262
|
}
|
|
1246
1263
|
if (response.status === 429) {
|
|
1247
1264
|
await handleRateLimitResponse(manager, client, account, response);
|
|
1265
|
+
return { type: "handled" };
|
|
1266
|
+
}
|
|
1267
|
+
return { type: "success", response };
|
|
1268
|
+
};
|
|
1269
|
+
for (let retries = 1; retries <= maxRetries; retries++) {
|
|
1270
|
+
await manager.refresh();
|
|
1271
|
+
const account = await resolveAccount(manager, client);
|
|
1272
|
+
const accountUuid = account.uuid;
|
|
1273
|
+
if (!accountUuid) continue;
|
|
1274
|
+
if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
|
|
1275
|
+
void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
|
|
1276
|
+
}
|
|
1277
|
+
previousAccountUuid = accountUuid;
|
|
1278
|
+
let runtime;
|
|
1279
|
+
let response;
|
|
1280
|
+
try {
|
|
1281
|
+
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1282
|
+
response = await runtime.fetch(input, init);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
if (isAbortError(error)) throw error;
|
|
1285
|
+
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
|
|
1292
|
+
if (transition.type === "retryOuter" || transition.type === "handled") {
|
|
1293
|
+
if (transition.type === "handled" && transition.response) {
|
|
1294
|
+
return transition.response;
|
|
1295
|
+
}
|
|
1248
1296
|
continue;
|
|
1249
1297
|
}
|
|
1250
1298
|
await manager.markSuccess(accountUuid);
|
|
1251
|
-
return response;
|
|
1299
|
+
return transition.response;
|
|
1252
1300
|
}
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
|
|
1303
|
+
);
|
|
1253
1304
|
}
|
|
1254
1305
|
async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
|
|
1255
|
-
|
|
1256
|
-
if (refreshFailureStatus === void 0) return false;
|
|
1306
|
+
if (!isTokenRefreshError(error)) return false;
|
|
1257
1307
|
if (!account.uuid) return false;
|
|
1258
1308
|
const accountUuid = account.uuid;
|
|
1259
1309
|
runtimeFactory.invalidate(accountUuid);
|
|
1260
1310
|
await manager.markAuthFailure(accountUuid, {
|
|
1261
1311
|
ok: false,
|
|
1262
|
-
permanent:
|
|
1312
|
+
permanent: error.permanent
|
|
1263
1313
|
});
|
|
1264
1314
|
await manager.refresh();
|
|
1265
1315
|
if (!manager.hasAnyUsableAccount()) {
|
|
@@ -1304,13 +1354,6 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1304
1354
|
executeWithAccountRotation
|
|
1305
1355
|
};
|
|
1306
1356
|
}
|
|
1307
|
-
function getRefreshFailureStatus(error) {
|
|
1308
|
-
if (!(error instanceof Error)) return void 0;
|
|
1309
|
-
const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
|
|
1310
|
-
if (!matched) return void 0;
|
|
1311
|
-
const status = Number(matched[1]);
|
|
1312
|
-
return Number.isFinite(status) ? status : void 0;
|
|
1313
|
-
}
|
|
1314
1357
|
async function isRevokedTokenResponse(response) {
|
|
1315
1358
|
try {
|
|
1316
1359
|
const cloned = response.clone();
|
|
@@ -1325,6 +1368,7 @@ async function isRevokedTokenResponse(response) {
|
|
|
1325
1368
|
var INITIAL_DELAY_MS = 5e3;
|
|
1326
1369
|
function createProactiveRefreshQueueForProvider(dependencies) {
|
|
1327
1370
|
const {
|
|
1371
|
+
providerAuthId,
|
|
1328
1372
|
getConfig: getConfig2,
|
|
1329
1373
|
refreshToken,
|
|
1330
1374
|
isTokenExpired,
|
|
@@ -1343,6 +1387,10 @@ function createProactiveRefreshQueueForProvider(dependencies) {
|
|
|
1343
1387
|
const config = getConfig2();
|
|
1344
1388
|
if (!config.proactive_refresh) return;
|
|
1345
1389
|
this.runToken++;
|
|
1390
|
+
if (this.timeoutHandle) {
|
|
1391
|
+
clearTimeout(this.timeoutHandle);
|
|
1392
|
+
this.timeoutHandle = null;
|
|
1393
|
+
}
|
|
1346
1394
|
this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
|
|
1347
1395
|
debugLog2(this.client, "Proactive refresh started", {
|
|
1348
1396
|
intervalSeconds: config.proactive_refresh_interval_seconds,
|
|
@@ -1417,23 +1465,41 @@ function createProactiveRefreshQueueForProvider(dependencies) {
|
|
|
1417
1465
|
}
|
|
1418
1466
|
async persistFailure(account, permanent) {
|
|
1419
1467
|
try {
|
|
1420
|
-
|
|
1421
|
-
|
|
1468
|
+
const accountUuid = account.uuid;
|
|
1469
|
+
if (!accountUuid) return;
|
|
1470
|
+
if (permanent) {
|
|
1471
|
+
const removed = await this.store.removeAccount(accountUuid);
|
|
1472
|
+
if (!removed) return;
|
|
1473
|
+
this.onInvalidate?.(accountUuid);
|
|
1474
|
+
await this.clearOpenCodeAuthIfNoAccountsRemain();
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
await this.store.mutateStorage((storage) => {
|
|
1478
|
+
const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
|
|
1479
|
+
if (!target) return;
|
|
1480
|
+
target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
|
|
1481
|
+
const maxFailures = getConfig2().max_consecutive_auth_failures;
|
|
1482
|
+
const usableCount = storage.accounts.filter(
|
|
1483
|
+
(entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
|
|
1484
|
+
).length;
|
|
1485
|
+
if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
|
|
1422
1486
|
target.isAuthDisabled = true;
|
|
1423
|
-
target.authDisabledReason =
|
|
1424
|
-
} else {
|
|
1425
|
-
target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
|
|
1426
|
-
const maxFailures = getConfig2().max_consecutive_auth_failures;
|
|
1427
|
-
if (target.consecutiveAuthFailures >= maxFailures) {
|
|
1428
|
-
target.isAuthDisabled = true;
|
|
1429
|
-
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1430
|
-
}
|
|
1487
|
+
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1431
1488
|
}
|
|
1432
1489
|
});
|
|
1433
1490
|
} catch {
|
|
1434
1491
|
debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
|
|
1435
1492
|
}
|
|
1436
1493
|
}
|
|
1494
|
+
async clearOpenCodeAuthIfNoAccountsRemain() {
|
|
1495
|
+
const storage = await this.store.load();
|
|
1496
|
+
if (storage.accounts.length > 0) return;
|
|
1497
|
+
await this.client.auth.set({
|
|
1498
|
+
path: { id: providerAuthId },
|
|
1499
|
+
body: getClearedOAuthBody()
|
|
1500
|
+
}).catch(() => {
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1437
1503
|
};
|
|
1438
1504
|
}
|
|
1439
1505
|
|
|
@@ -1782,6 +1848,8 @@ var anthropicOAuthAdapter = {
|
|
|
1782
1848
|
oauthBetaHeader: "oauth-2025-04-20",
|
|
1783
1849
|
requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
|
1784
1850
|
cliUserAgent: "claude-cli/2.1.2 (external, cli)",
|
|
1851
|
+
cliVersion: "2.1.80",
|
|
1852
|
+
billingSalt: "59cf53e54c78",
|
|
1785
1853
|
toolPrefix: "mcp_",
|
|
1786
1854
|
accountStorageFilename: "anthropic-multi-account-accounts.json",
|
|
1787
1855
|
transform: {
|
|
@@ -1814,6 +1882,8 @@ var openAIOAuthAdapter = {
|
|
|
1814
1882
|
oauthBetaHeader: "",
|
|
1815
1883
|
requestBetaHeader: "",
|
|
1816
1884
|
cliUserAgent: "opencode/1.1.53",
|
|
1885
|
+
cliVersion: "",
|
|
1886
|
+
billingSalt: "",
|
|
1817
1887
|
toolPrefix: "mcp_",
|
|
1818
1888
|
accountStorageFilename: "openai-multi-account-accounts.json",
|
|
1819
1889
|
transform: {
|
|
@@ -1830,16 +1900,414 @@ var openAIOAuthAdapter = {
|
|
|
1830
1900
|
},
|
|
1831
1901
|
supported: true
|
|
1832
1902
|
};
|
|
1903
|
+
|
|
1904
|
+
// src/pool-types.ts
|
|
1905
|
+
import * as v5 from "valibot";
|
|
1906
|
+
var PoolConfigSchema = v5.object({
|
|
1907
|
+
name: v5.string(),
|
|
1908
|
+
baseProvider: v5.string(),
|
|
1909
|
+
members: v5.array(v5.string()),
|
|
1910
|
+
enabled: v5.boolean()
|
|
1911
|
+
});
|
|
1912
|
+
var ChainEntryConfigSchema = v5.object({
|
|
1913
|
+
pool: v5.string(),
|
|
1914
|
+
model: v5.optional(v5.string()),
|
|
1915
|
+
enabled: v5.boolean()
|
|
1916
|
+
});
|
|
1917
|
+
var ChainConfigSchema = v5.object({
|
|
1918
|
+
name: v5.string(),
|
|
1919
|
+
entries: v5.array(ChainEntryConfigSchema),
|
|
1920
|
+
enabled: v5.boolean()
|
|
1921
|
+
});
|
|
1922
|
+
var PoolChainConfigSchema = v5.object({
|
|
1923
|
+
pools: v5.optional(v5.array(PoolConfigSchema), []),
|
|
1924
|
+
chains: v5.optional(v5.array(ChainConfigSchema), [])
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/pool-config-store.ts
|
|
1928
|
+
import { promises as fs6 } from "node:fs";
|
|
1929
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
1930
|
+
import { dirname as dirname5, join as join7 } from "node:path";
|
|
1931
|
+
import lockfile2 from "proper-lockfile";
|
|
1932
|
+
import * as v6 from "valibot";
|
|
1933
|
+
var POOL_CONFIG_FILENAME = "multiauth-pools.json";
|
|
1934
|
+
var FILE_MODE2 = 384;
|
|
1935
|
+
var LOCK_OPTIONS2 = {
|
|
1936
|
+
stale: 1e4,
|
|
1937
|
+
retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
|
|
1938
|
+
};
|
|
1939
|
+
function createEmptyConfig() {
|
|
1940
|
+
return { pools: [], chains: [] };
|
|
1941
|
+
}
|
|
1942
|
+
function getGlobalConfigPath() {
|
|
1943
|
+
return join7(getConfigDir2(), POOL_CONFIG_FILENAME);
|
|
1944
|
+
}
|
|
1945
|
+
function buildTempPath2(targetPath) {
|
|
1946
|
+
return `${targetPath}.${randomBytes4(8).toString("hex")}.tmp`;
|
|
1947
|
+
}
|
|
1948
|
+
async function resolveConfigPath() {
|
|
1949
|
+
const projectPath = join7(process.cwd(), ".opencode", POOL_CONFIG_FILENAME);
|
|
1950
|
+
try {
|
|
1951
|
+
await fs6.access(projectPath);
|
|
1952
|
+
return projectPath;
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
return getGlobalConfigPath();
|
|
1956
|
+
}
|
|
1957
|
+
async function ensureConfigFileExists(targetPath) {
|
|
1958
|
+
await fs6.mkdir(dirname5(targetPath), { recursive: true });
|
|
1959
|
+
const emptyContent = `${JSON.stringify(createEmptyConfig(), null, 2)}
|
|
1960
|
+
`;
|
|
1961
|
+
try {
|
|
1962
|
+
await fs6.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE2 });
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
if (getErrorCode(error) !== "EEXIST") throw error;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function writeAtomicText2(targetPath, content) {
|
|
1968
|
+
await fs6.mkdir(dirname5(targetPath), { recursive: true });
|
|
1969
|
+
const tempPath = buildTempPath2(targetPath);
|
|
1970
|
+
try {
|
|
1971
|
+
await fs6.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE2 });
|
|
1972
|
+
await fs6.chmod(tempPath, FILE_MODE2);
|
|
1973
|
+
await fs6.rename(tempPath, targetPath);
|
|
1974
|
+
await fs6.chmod(targetPath, FILE_MODE2);
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
try {
|
|
1977
|
+
await fs6.unlink(tempPath);
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
throw error;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async function withConfigLock(fn) {
|
|
1984
|
+
const configPath = await resolveConfigPath();
|
|
1985
|
+
await ensureConfigFileExists(configPath);
|
|
1986
|
+
let release = null;
|
|
1987
|
+
try {
|
|
1988
|
+
release = await lockfile2.lock(configPath, LOCK_OPTIONS2);
|
|
1989
|
+
return await fn(configPath);
|
|
1990
|
+
} finally {
|
|
1991
|
+
if (release) {
|
|
1992
|
+
try {
|
|
1993
|
+
await release();
|
|
1994
|
+
} catch {
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function parsePoolChainConfig(content) {
|
|
2000
|
+
let parsed;
|
|
2001
|
+
try {
|
|
2002
|
+
parsed = JSON.parse(content);
|
|
2003
|
+
} catch {
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
const validation = v6.safeParse(PoolChainConfigSchema, parsed);
|
|
2007
|
+
return validation.success ? validation.output : null;
|
|
2008
|
+
}
|
|
2009
|
+
async function loadPoolChainConfig() {
|
|
2010
|
+
const path = await resolveConfigPath();
|
|
2011
|
+
try {
|
|
2012
|
+
const content = await fs6.readFile(path, "utf-8");
|
|
2013
|
+
return parsePoolChainConfig(content) ?? createEmptyConfig();
|
|
2014
|
+
} catch {
|
|
2015
|
+
return createEmptyConfig();
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
async function savePoolChainConfig(config) {
|
|
2019
|
+
await withConfigLock(async (configPath) => {
|
|
2020
|
+
const validation = v6.safeParse(PoolChainConfigSchema, config);
|
|
2021
|
+
if (!validation.success) {
|
|
2022
|
+
throw new Error("Invalid pool/chain config payload");
|
|
2023
|
+
}
|
|
2024
|
+
await writeAtomicText2(configPath, `${JSON.stringify(validation.output, null, 2)}
|
|
2025
|
+
`);
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/pool-manager.ts
|
|
2030
|
+
var DEFAULT_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
2031
|
+
var PoolManager = class {
|
|
2032
|
+
poolsByName = /* @__PURE__ */ new Map();
|
|
2033
|
+
exhaustedUntilByAccount = /* @__PURE__ */ new Map();
|
|
2034
|
+
exhaustedCooldownMs;
|
|
2035
|
+
constructor(options) {
|
|
2036
|
+
this.exhaustedCooldownMs = options?.exhaustedCooldownMs ?? DEFAULT_EXHAUSTED_COOLDOWN_MS;
|
|
2037
|
+
}
|
|
2038
|
+
loadPools(configs) {
|
|
2039
|
+
this.poolsByName.clear();
|
|
2040
|
+
for (const pool of configs) {
|
|
2041
|
+
this.poolsByName.set(pool.name, pool);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
getPoolForAccount(accountUuid) {
|
|
2045
|
+
for (const pool of this.poolsByName.values()) {
|
|
2046
|
+
if (!pool.enabled) continue;
|
|
2047
|
+
if (pool.members.includes(accountUuid)) return pool;
|
|
2048
|
+
}
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
getAvailableMembers(pool, accountManager) {
|
|
2052
|
+
if (!pool.enabled) return [];
|
|
2053
|
+
this.clearExpiredExhausted();
|
|
2054
|
+
const accountsByUuid = /* @__PURE__ */ new Map();
|
|
2055
|
+
for (const account of accountManager.getAccounts()) {
|
|
2056
|
+
if (!account.uuid) continue;
|
|
2057
|
+
accountsByUuid.set(account.uuid, account);
|
|
2058
|
+
}
|
|
2059
|
+
return pool.members.filter((accountUuid) => {
|
|
2060
|
+
const account = accountsByUuid.get(accountUuid);
|
|
2061
|
+
if (!account) return false;
|
|
2062
|
+
if (!account.enabled || account.isAuthDisabled) return false;
|
|
2063
|
+
if (this.isExhausted(accountUuid)) return false;
|
|
2064
|
+
if (accountManager.isRateLimited(account)) return false;
|
|
2065
|
+
return true;
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
markExhausted(accountUuid) {
|
|
2069
|
+
this.exhaustedUntilByAccount.set(accountUuid, Date.now() + this.exhaustedCooldownMs);
|
|
2070
|
+
}
|
|
2071
|
+
async getNextMember(pool, currentUuid, accountManager) {
|
|
2072
|
+
const availableMembers = this.getAvailableMembers(pool, accountManager);
|
|
2073
|
+
if (availableMembers.length === 0) return null;
|
|
2074
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
2075
|
+
if (currentUuid) excluded.add(currentUuid);
|
|
2076
|
+
const preferred = await this.selectPreferredMember(availableMembers, excluded, accountManager);
|
|
2077
|
+
if (preferred) return preferred;
|
|
2078
|
+
for (const candidate of availableMembers) {
|
|
2079
|
+
if (candidate !== currentUuid) return candidate;
|
|
2080
|
+
}
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
async buildFailoverPlan(currentAccount, config, accountManager, options) {
|
|
2084
|
+
this.loadPools(config.pools ?? []);
|
|
2085
|
+
if ((config.pools?.length ?? 0) === 0 && (config.chains?.length ?? 0) === 0) {
|
|
2086
|
+
return { candidates: [], skips: [] };
|
|
2087
|
+
}
|
|
2088
|
+
const attemptedAccounts = options?.attemptedAccounts ?? /* @__PURE__ */ new Set();
|
|
2089
|
+
const visitedChainIndexes = options?.visitedChainIndexes ?? /* @__PURE__ */ new Set();
|
|
2090
|
+
const currentUuid = currentAccount?.uuid;
|
|
2091
|
+
const candidates = [];
|
|
2092
|
+
const skips = [];
|
|
2093
|
+
const addedCandidateUuids = /* @__PURE__ */ new Set();
|
|
2094
|
+
const appendPoolCandidates = async (poolName, source, chainIndex) => {
|
|
2095
|
+
const pool = this.poolsByName.get(poolName);
|
|
2096
|
+
if (!pool || !pool.enabled) {
|
|
2097
|
+
skips.push({
|
|
2098
|
+
type: "chain_disabled",
|
|
2099
|
+
poolName,
|
|
2100
|
+
reason: "Pool is missing or disabled"
|
|
2101
|
+
});
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const available = this.getAvailableMembers(pool, accountManager);
|
|
2105
|
+
if (available.length === 0) {
|
|
2106
|
+
skips.push({
|
|
2107
|
+
type: "pool_exhausted",
|
|
2108
|
+
poolName,
|
|
2109
|
+
reason: "No available members"
|
|
2110
|
+
});
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
const poolExclusions = /* @__PURE__ */ new Set();
|
|
2114
|
+
if (currentUuid) poolExclusions.add(currentUuid);
|
|
2115
|
+
while (poolExclusions.size < available.length + (currentUuid ? 1 : 0)) {
|
|
2116
|
+
const nextMember = await this.selectPreferredMember(available, poolExclusions, accountManager);
|
|
2117
|
+
if (!nextMember) break;
|
|
2118
|
+
poolExclusions.add(nextMember);
|
|
2119
|
+
if (attemptedAccounts.has(nextMember)) {
|
|
2120
|
+
skips.push({
|
|
2121
|
+
type: "account_attempted",
|
|
2122
|
+
poolName,
|
|
2123
|
+
reason: "Already attempted in this cascade",
|
|
2124
|
+
detail: nextMember
|
|
2125
|
+
});
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (addedCandidateUuids.has(nextMember)) continue;
|
|
2129
|
+
candidates.push({
|
|
2130
|
+
poolName,
|
|
2131
|
+
accountUuid: nextMember,
|
|
2132
|
+
source,
|
|
2133
|
+
chainIndex
|
|
2134
|
+
});
|
|
2135
|
+
addedCandidateUuids.add(nextMember);
|
|
2136
|
+
}
|
|
2137
|
+
for (const memberUuid of available) {
|
|
2138
|
+
if (poolExclusions.has(memberUuid)) continue;
|
|
2139
|
+
if (attemptedAccounts.has(memberUuid)) {
|
|
2140
|
+
skips.push({
|
|
2141
|
+
type: "account_attempted",
|
|
2142
|
+
poolName,
|
|
2143
|
+
reason: "Already attempted in this cascade",
|
|
2144
|
+
detail: memberUuid
|
|
2145
|
+
});
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
if (addedCandidateUuids.has(memberUuid)) continue;
|
|
2149
|
+
candidates.push({
|
|
2150
|
+
poolName,
|
|
2151
|
+
accountUuid: memberUuid,
|
|
2152
|
+
source,
|
|
2153
|
+
chainIndex
|
|
2154
|
+
});
|
|
2155
|
+
addedCandidateUuids.add(memberUuid);
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
if (currentUuid) {
|
|
2159
|
+
const currentPool = this.getPoolForAccount(currentUuid);
|
|
2160
|
+
if (currentPool) {
|
|
2161
|
+
await appendPoolCandidates(currentPool.name, "pool");
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
let flattenedChainIndex = 0;
|
|
2165
|
+
for (const chain of config.chains ?? []) {
|
|
2166
|
+
if (!chain.enabled) {
|
|
2167
|
+
for (let i = 0; i < chain.entries.length; i++) {
|
|
2168
|
+
skips.push({
|
|
2169
|
+
type: "chain_disabled",
|
|
2170
|
+
poolName: chain.entries[i]?.pool ?? chain.name,
|
|
2171
|
+
reason: `Chain '${chain.name}' is disabled`
|
|
2172
|
+
});
|
|
2173
|
+
flattenedChainIndex += 1;
|
|
2174
|
+
}
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
for (const entry of chain.entries) {
|
|
2178
|
+
if (visitedChainIndexes.has(flattenedChainIndex)) {
|
|
2179
|
+
skips.push({
|
|
2180
|
+
type: "chain_disabled",
|
|
2181
|
+
poolName: entry.pool,
|
|
2182
|
+
reason: "Chain entry already visited in this cascade",
|
|
2183
|
+
detail: `${flattenedChainIndex}`
|
|
2184
|
+
});
|
|
2185
|
+
flattenedChainIndex += 1;
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
if (!entry.enabled) {
|
|
2189
|
+
skips.push({
|
|
2190
|
+
type: "chain_disabled",
|
|
2191
|
+
poolName: entry.pool,
|
|
2192
|
+
reason: "Chain entry is disabled",
|
|
2193
|
+
detail: `${flattenedChainIndex}`
|
|
2194
|
+
});
|
|
2195
|
+
flattenedChainIndex += 1;
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
await appendPoolCandidates(entry.pool, "chain", flattenedChainIndex);
|
|
2199
|
+
flattenedChainIndex += 1;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return { candidates, skips };
|
|
2203
|
+
}
|
|
2204
|
+
isExhausted(accountUuid) {
|
|
2205
|
+
const exhaustedUntil = this.exhaustedUntilByAccount.get(accountUuid);
|
|
2206
|
+
if (!exhaustedUntil) return false;
|
|
2207
|
+
if (Date.now() >= exhaustedUntil) {
|
|
2208
|
+
this.exhaustedUntilByAccount.delete(accountUuid);
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
clearExpiredExhausted() {
|
|
2214
|
+
const now = Date.now();
|
|
2215
|
+
for (const [accountUuid, exhaustedUntil] of this.exhaustedUntilByAccount.entries()) {
|
|
2216
|
+
if (now >= exhaustedUntil) this.exhaustedUntilByAccount.delete(accountUuid);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
async selectPreferredMember(availableMembers, excludedMembers, accountManager) {
|
|
2220
|
+
const availableSet = new Set(availableMembers);
|
|
2221
|
+
const maxAttempts = Math.max(availableMembers.length * 2, 6);
|
|
2222
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2223
|
+
const selected = await accountManager.selectAccount();
|
|
2224
|
+
if (!selected?.uuid) continue;
|
|
2225
|
+
if (!availableSet.has(selected.uuid)) continue;
|
|
2226
|
+
if (excludedMembers.has(selected.uuid)) continue;
|
|
2227
|
+
return selected.uuid;
|
|
2228
|
+
}
|
|
2229
|
+
for (const memberUuid of availableMembers) {
|
|
2230
|
+
if (!excludedMembers.has(memberUuid)) return memberUuid;
|
|
2231
|
+
}
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
|
|
2236
|
+
// src/cascade-state.ts
|
|
2237
|
+
function createCascadeState(prompt, currentAccountUuid) {
|
|
2238
|
+
const attemptedAccounts = /* @__PURE__ */ new Set();
|
|
2239
|
+
if (currentAccountUuid) {
|
|
2240
|
+
attemptedAccounts.add(currentAccountUuid);
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
prompt,
|
|
2244
|
+
attemptedAccounts,
|
|
2245
|
+
visitedChainIndexes: /* @__PURE__ */ new Set()
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
var CascadeStateManager = class {
|
|
2249
|
+
suppressNextStartTurn = false;
|
|
2250
|
+
cascadeState = null;
|
|
2251
|
+
startTurn(prompt, currentAccountUuid) {
|
|
2252
|
+
if (this.suppressNextStartTurn) {
|
|
2253
|
+
this.suppressNextStartTurn = false;
|
|
2254
|
+
return this.ensureCascadeState(prompt, currentAccountUuid);
|
|
2255
|
+
}
|
|
2256
|
+
const shouldReset = !this.cascadeState || this.cascadeState.prompt !== prompt;
|
|
2257
|
+
if (shouldReset) {
|
|
2258
|
+
this.cascadeState = createCascadeState(prompt, currentAccountUuid);
|
|
2259
|
+
return this.cascadeState;
|
|
2260
|
+
}
|
|
2261
|
+
return this.ensureCascadeState(prompt, currentAccountUuid);
|
|
2262
|
+
}
|
|
2263
|
+
ensureCascadeState(prompt, currentAccountUuid) {
|
|
2264
|
+
if (!this.cascadeState || this.cascadeState.prompt !== prompt) {
|
|
2265
|
+
this.cascadeState = createCascadeState(prompt, currentAccountUuid);
|
|
2266
|
+
return this.cascadeState;
|
|
2267
|
+
}
|
|
2268
|
+
if (currentAccountUuid) {
|
|
2269
|
+
this.cascadeState.attemptedAccounts.add(currentAccountUuid);
|
|
2270
|
+
}
|
|
2271
|
+
return this.cascadeState;
|
|
2272
|
+
}
|
|
2273
|
+
markAttempted(accountUuid) {
|
|
2274
|
+
if (!this.cascadeState) return;
|
|
2275
|
+
this.cascadeState.attemptedAccounts.add(accountUuid);
|
|
2276
|
+
}
|
|
2277
|
+
markVisitedChainIndex(index) {
|
|
2278
|
+
if (!this.cascadeState) return;
|
|
2279
|
+
this.cascadeState.visitedChainIndexes.add(index);
|
|
2280
|
+
}
|
|
2281
|
+
clearCascadeState() {
|
|
2282
|
+
this.cascadeState = null;
|
|
2283
|
+
this.suppressNextStartTurn = false;
|
|
2284
|
+
}
|
|
2285
|
+
getSnapshot() {
|
|
2286
|
+
if (!this.cascadeState) return null;
|
|
2287
|
+
return {
|
|
2288
|
+
prompt: this.cascadeState.prompt,
|
|
2289
|
+
attemptedAccounts: new Set(this.cascadeState.attemptedAccounts),
|
|
2290
|
+
visitedChainIndexes: new Set(this.cascadeState.visitedChainIndexes)
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
1833
2294
|
export {
|
|
1834
2295
|
ACCOUNTS_FILENAME,
|
|
1835
2296
|
ANSI,
|
|
1836
2297
|
AccountSelectionStrategySchema,
|
|
1837
2298
|
AccountStorageSchema,
|
|
1838
2299
|
AccountStore,
|
|
2300
|
+
CascadeStateManager,
|
|
2301
|
+
ChainConfigSchema,
|
|
2302
|
+
ChainEntryConfigSchema,
|
|
1839
2303
|
CredentialRefreshPatchSchema,
|
|
1840
2304
|
OAuthCredentialsSchema,
|
|
1841
2305
|
PluginConfigSchema,
|
|
2306
|
+
PoolChainConfigSchema,
|
|
2307
|
+
PoolConfigSchema,
|
|
2308
|
+
PoolManager,
|
|
1842
2309
|
StoredAccountSchema,
|
|
2310
|
+
TokenRefreshError,
|
|
1843
2311
|
UsageLimitEntrySchema,
|
|
1844
2312
|
UsageLimitsSchema,
|
|
1845
2313
|
anthropicOAuthAdapter,
|
|
@@ -1853,14 +2321,17 @@ export {
|
|
|
1853
2321
|
deduplicateAccounts,
|
|
1854
2322
|
formatWaitTime,
|
|
1855
2323
|
getAccountLabel,
|
|
2324
|
+
getClearedOAuthBody,
|
|
1856
2325
|
getConfig,
|
|
1857
2326
|
getConfigDir2 as getConfigDir,
|
|
1858
2327
|
getErrorCode,
|
|
1859
2328
|
initCoreConfig,
|
|
1860
2329
|
isClaimedByOther,
|
|
1861
2330
|
isTTY,
|
|
2331
|
+
isTokenRefreshError,
|
|
1862
2332
|
loadAccounts,
|
|
1863
2333
|
loadConfig,
|
|
2334
|
+
loadPoolChainConfig,
|
|
1864
2335
|
migrateFromAuthJson,
|
|
1865
2336
|
openAIOAuthAdapter,
|
|
1866
2337
|
parseKey,
|
|
@@ -1868,6 +2339,7 @@ export {
|
|
|
1868
2339
|
readStorageFromDisk,
|
|
1869
2340
|
releaseClaim,
|
|
1870
2341
|
resetConfigCache,
|
|
2342
|
+
savePoolChainConfig,
|
|
1871
2343
|
select,
|
|
1872
2344
|
setAccountsFilename,
|
|
1873
2345
|
setConfigGetter,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ManagedAccount } from "./types";
|
|
2
|
+
import type { FailoverCandidate, FailoverSkip, PoolChainConfig, PoolConfig } from "./pool-types";
|
|
3
|
+
interface PoolAwareAccountManager {
|
|
4
|
+
getAccounts(): ManagedAccount[];
|
|
5
|
+
isRateLimited(account: ManagedAccount): boolean;
|
|
6
|
+
selectAccount(): Promise<ManagedAccount | null>;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildFailoverPlanOptions {
|
|
9
|
+
attemptedAccounts?: Set<string>;
|
|
10
|
+
visitedChainIndexes?: Set<number>;
|
|
11
|
+
}
|
|
12
|
+
export interface FailoverPlan {
|
|
13
|
+
candidates: FailoverCandidate[];
|
|
14
|
+
skips: FailoverSkip[];
|
|
15
|
+
}
|
|
16
|
+
export declare class PoolManager {
|
|
17
|
+
private poolsByName;
|
|
18
|
+
private exhaustedUntilByAccount;
|
|
19
|
+
private exhaustedCooldownMs;
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
exhaustedCooldownMs?: number;
|
|
22
|
+
});
|
|
23
|
+
loadPools(configs: PoolConfig[]): void;
|
|
24
|
+
getPoolForAccount(accountUuid: string): PoolConfig | null;
|
|
25
|
+
getAvailableMembers(pool: PoolConfig, accountManager: PoolAwareAccountManager): string[];
|
|
26
|
+
markExhausted(accountUuid: string): void;
|
|
27
|
+
getNextMember(pool: PoolConfig, currentUuid: string | undefined, accountManager: PoolAwareAccountManager): Promise<string | null>;
|
|
28
|
+
buildFailoverPlan(currentAccount: Pick<ManagedAccount, "uuid" | "accountId"> | null, config: PoolChainConfig, accountManager: PoolAwareAccountManager, options?: BuildFailoverPlanOptions): Promise<FailoverPlan>;
|
|
29
|
+
private isExhausted;
|
|
30
|
+
private clearExpiredExhausted;
|
|
31
|
+
private selectPreferredMember;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
export declare const PoolConfigSchema: v.ObjectSchema<{
|
|
3
|
+
readonly name: v.StringSchema<undefined>;
|
|
4
|
+
readonly baseProvider: v.StringSchema<undefined>;
|
|
5
|
+
readonly members: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
6
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
7
|
+
}, undefined>;
|
|
8
|
+
export declare const ChainEntryConfigSchema: v.ObjectSchema<{
|
|
9
|
+
readonly pool: v.StringSchema<undefined>;
|
|
10
|
+
readonly model: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
11
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
12
|
+
}, undefined>;
|
|
13
|
+
export declare const ChainConfigSchema: v.ObjectSchema<{
|
|
14
|
+
readonly name: v.StringSchema<undefined>;
|
|
15
|
+
readonly entries: v.ArraySchema<v.ObjectSchema<{
|
|
16
|
+
readonly pool: v.StringSchema<undefined>;
|
|
17
|
+
readonly model: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
18
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
19
|
+
}, undefined>, undefined>;
|
|
20
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
21
|
+
}, undefined>;
|
|
22
|
+
export declare const PoolChainConfigSchema: v.ObjectSchema<{
|
|
23
|
+
readonly pools: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
24
|
+
readonly name: v.StringSchema<undefined>;
|
|
25
|
+
readonly baseProvider: v.StringSchema<undefined>;
|
|
26
|
+
readonly members: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
27
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
28
|
+
}, undefined>, undefined>, readonly []>;
|
|
29
|
+
readonly chains: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
30
|
+
readonly name: v.StringSchema<undefined>;
|
|
31
|
+
readonly entries: v.ArraySchema<v.ObjectSchema<{
|
|
32
|
+
readonly pool: v.StringSchema<undefined>;
|
|
33
|
+
readonly model: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
34
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
35
|
+
}, undefined>, undefined>;
|
|
36
|
+
readonly enabled: v.BooleanSchema<undefined>;
|
|
37
|
+
}, undefined>, undefined>, readonly []>;
|
|
38
|
+
}, undefined>;
|
|
39
|
+
export type PoolConfig = v.InferOutput<typeof PoolConfigSchema>;
|
|
40
|
+
export type ChainEntryConfig = v.InferOutput<typeof ChainEntryConfigSchema>;
|
|
41
|
+
export type ChainConfig = v.InferOutput<typeof ChainConfigSchema>;
|
|
42
|
+
export type PoolChainConfig = v.InferOutput<typeof PoolChainConfigSchema>;
|
|
43
|
+
export interface CascadeState {
|
|
44
|
+
prompt: string;
|
|
45
|
+
attemptedAccounts: Set<string>;
|
|
46
|
+
visitedChainIndexes: Set<number>;
|
|
47
|
+
}
|
|
48
|
+
export interface FailoverCandidate {
|
|
49
|
+
poolName: string;
|
|
50
|
+
accountUuid: string;
|
|
51
|
+
source: "pool" | "chain";
|
|
52
|
+
chainIndex?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface FailoverSkip {
|
|
55
|
+
type: "pool_exhausted" | "chain_disabled" | "account_attempted" | "account_unavailable";
|
|
56
|
+
poolName: string;
|
|
57
|
+
reason: string;
|
|
58
|
+
detail?: string;
|
|
59
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AccountStore } from "./account-store";
|
|
2
2
|
import type { PluginClient, PluginConfig, StoredAccount, TokenRefreshResult } from "./types";
|
|
3
3
|
export interface ProactiveRefreshDependencies {
|
|
4
|
+
providerAuthId: string;
|
|
4
5
|
getConfig: () => PluginConfig;
|
|
5
6
|
refreshToken: (currentRefreshToken: string, accountId: string, client: PluginClient) => Promise<TokenRefreshResult>;
|
|
6
7
|
isTokenExpired: (account: Pick<StoredAccount, "accessToken" | "expiresAt">) => boolean;
|
package/dist/types.d.ts
CHANGED
|
@@ -130,6 +130,12 @@ export type TokenRefreshResult = {
|
|
|
130
130
|
permanent: boolean;
|
|
131
131
|
status?: number;
|
|
132
132
|
};
|
|
133
|
+
export declare class TokenRefreshError extends Error {
|
|
134
|
+
readonly status?: number;
|
|
135
|
+
readonly permanent: boolean;
|
|
136
|
+
constructor(permanent: boolean, status?: number);
|
|
137
|
+
}
|
|
138
|
+
export declare function isTokenRefreshError(error: unknown): error is TokenRefreshError;
|
|
133
139
|
export interface ManagedAccount {
|
|
134
140
|
index: number;
|
|
135
141
|
uuid?: string;
|
package/dist/utils.d.ts
CHANGED
|
@@ -7,3 +7,9 @@ export declare function sleep(ms: number): Promise<void>;
|
|
|
7
7
|
export declare function showToast(client: PluginClient, message: string, variant: "info" | "warning" | "success" | "error"): Promise<void>;
|
|
8
8
|
export declare function debugLog(client: PluginClient, message: string, extra?: Record<string, unknown>): void;
|
|
9
9
|
export declare function createMinimalClient(): PluginClient;
|
|
10
|
+
export declare function getClearedOAuthBody(): {
|
|
11
|
+
type: "oauth";
|
|
12
|
+
refresh: string;
|
|
13
|
+
access: string;
|
|
14
|
+
expires: number;
|
|
15
|
+
};
|