opencode-multi-account-core 0.2.2 → 0.2.4
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 +579 -101
- 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,9 @@ 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
|
-
|
|
1174
|
+
function isAbortError(error) {
|
|
1175
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1176
|
+
}
|
|
1129
1177
|
function createExecutorForProvider(providerName, dependencies) {
|
|
1130
1178
|
const {
|
|
1131
1179
|
handleRateLimitResponse,
|
|
@@ -1136,77 +1184,49 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1136
1184
|
} = dependencies;
|
|
1137
1185
|
async function executeWithAccountRotation(manager, runtimeFactory, client, input, init) {
|
|
1138
1186
|
const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
|
|
1139
|
-
let retries = 0;
|
|
1140
1187
|
let previousAccountUuid;
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
);
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
let response;
|
|
1157
|
-
try {
|
|
1158
|
-
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1159
|
-
response = await runtime.fetch(input, init);
|
|
1160
|
-
} catch (error) {
|
|
1161
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1162
|
-
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;
|
|
1163
1203
|
}
|
|
1164
|
-
|
|
1165
|
-
continue;
|
|
1204
|
+
if (retryResponse.status < 500) return retryResponse;
|
|
1166
1205
|
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
|
|
1167
1209
|
if (response.status >= 500) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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);
|
|
1175
1219
|
try {
|
|
1176
|
-
|
|
1220
|
+
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1221
|
+
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1222
|
+
return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
|
|
1177
1223
|
} catch (error) {
|
|
1224
|
+
if (isAbortError(error)) throw error;
|
|
1178
1225
|
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1179
|
-
|
|
1180
|
-
break;
|
|
1226
|
+
return { type: "retryOuter" };
|
|
1181
1227
|
}
|
|
1182
|
-
|
|
1183
|
-
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1184
|
-
break;
|
|
1228
|
+
return { type: "retryOuter" };
|
|
1185
1229
|
}
|
|
1186
|
-
if (serverResponse.status < 500) break;
|
|
1187
|
-
}
|
|
1188
|
-
if (authFailureDuringServerRetry) {
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
|
|
1192
|
-
continue;
|
|
1193
|
-
}
|
|
1194
|
-
response = serverResponse;
|
|
1195
|
-
}
|
|
1196
|
-
if (response.status === 401) {
|
|
1197
|
-
runtimeFactory.invalidate(accountUuid);
|
|
1198
|
-
try {
|
|
1199
|
-
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1200
|
-
const retryResponse = await retryRuntime.fetch(input, init);
|
|
1201
|
-
if (retryResponse.status !== 401) {
|
|
1202
|
-
await manager.markSuccess(accountUuid);
|
|
1203
|
-
return retryResponse;
|
|
1204
|
-
}
|
|
1205
|
-
} catch (error) {
|
|
1206
|
-
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
1207
|
-
continue;
|
|
1208
|
-
}
|
|
1209
|
-
continue;
|
|
1210
1230
|
}
|
|
1211
1231
|
await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
|
|
1212
1232
|
await manager.refresh();
|
|
@@ -1217,7 +1237,7 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1217
1237
|
);
|
|
1218
1238
|
}
|
|
1219
1239
|
void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
|
|
1220
|
-
|
|
1240
|
+
return { type: "retryOuter" };
|
|
1221
1241
|
}
|
|
1222
1242
|
if (response.status === 403) {
|
|
1223
1243
|
const revoked = await isRevokedTokenResponse(response);
|
|
@@ -1234,26 +1254,62 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1234
1254
|
`All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
|
|
1235
1255
|
);
|
|
1236
1256
|
}
|
|
1237
|
-
|
|
1257
|
+
return { type: "retryOuter" };
|
|
1258
|
+
}
|
|
1259
|
+
if (from401RefreshRetry) {
|
|
1260
|
+
return { type: "handled", response };
|
|
1238
1261
|
}
|
|
1239
1262
|
}
|
|
1240
1263
|
if (response.status === 429) {
|
|
1241
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
|
+
}
|
|
1242
1296
|
continue;
|
|
1243
1297
|
}
|
|
1244
1298
|
await manager.markSuccess(accountUuid);
|
|
1245
|
-
return response;
|
|
1299
|
+
return transition.response;
|
|
1246
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
|
+
);
|
|
1247
1304
|
}
|
|
1248
1305
|
async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
|
|
1249
|
-
|
|
1250
|
-
if (refreshFailureStatus === void 0) return false;
|
|
1306
|
+
if (!isTokenRefreshError(error)) return false;
|
|
1251
1307
|
if (!account.uuid) return false;
|
|
1252
1308
|
const accountUuid = account.uuid;
|
|
1253
1309
|
runtimeFactory.invalidate(accountUuid);
|
|
1254
1310
|
await manager.markAuthFailure(accountUuid, {
|
|
1255
1311
|
ok: false,
|
|
1256
|
-
permanent:
|
|
1312
|
+
permanent: error.permanent
|
|
1257
1313
|
});
|
|
1258
1314
|
await manager.refresh();
|
|
1259
1315
|
if (!manager.hasAnyUsableAccount()) {
|
|
@@ -1298,13 +1354,6 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1298
1354
|
executeWithAccountRotation
|
|
1299
1355
|
};
|
|
1300
1356
|
}
|
|
1301
|
-
function getRefreshFailureStatus(error) {
|
|
1302
|
-
if (!(error instanceof Error)) return void 0;
|
|
1303
|
-
const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
|
|
1304
|
-
if (!matched) return void 0;
|
|
1305
|
-
const status = Number(matched[1]);
|
|
1306
|
-
return Number.isFinite(status) ? status : void 0;
|
|
1307
|
-
}
|
|
1308
1357
|
async function isRevokedTokenResponse(response) {
|
|
1309
1358
|
try {
|
|
1310
1359
|
const cloned = response.clone();
|
|
@@ -1319,6 +1368,7 @@ async function isRevokedTokenResponse(response) {
|
|
|
1319
1368
|
var INITIAL_DELAY_MS = 5e3;
|
|
1320
1369
|
function createProactiveRefreshQueueForProvider(dependencies) {
|
|
1321
1370
|
const {
|
|
1371
|
+
providerAuthId,
|
|
1322
1372
|
getConfig: getConfig2,
|
|
1323
1373
|
refreshToken,
|
|
1324
1374
|
isTokenExpired,
|
|
@@ -1337,6 +1387,10 @@ function createProactiveRefreshQueueForProvider(dependencies) {
|
|
|
1337
1387
|
const config = getConfig2();
|
|
1338
1388
|
if (!config.proactive_refresh) return;
|
|
1339
1389
|
this.runToken++;
|
|
1390
|
+
if (this.timeoutHandle) {
|
|
1391
|
+
clearTimeout(this.timeoutHandle);
|
|
1392
|
+
this.timeoutHandle = null;
|
|
1393
|
+
}
|
|
1340
1394
|
this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
|
|
1341
1395
|
debugLog2(this.client, "Proactive refresh started", {
|
|
1342
1396
|
intervalSeconds: config.proactive_refresh_interval_seconds,
|
|
@@ -1411,23 +1465,41 @@ function createProactiveRefreshQueueForProvider(dependencies) {
|
|
|
1411
1465
|
}
|
|
1412
1466
|
async persistFailure(account, permanent) {
|
|
1413
1467
|
try {
|
|
1414
|
-
|
|
1415
|
-
|
|
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) {
|
|
1416
1486
|
target.isAuthDisabled = true;
|
|
1417
|
-
target.authDisabledReason =
|
|
1418
|
-
} else {
|
|
1419
|
-
target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
|
|
1420
|
-
const maxFailures = getConfig2().max_consecutive_auth_failures;
|
|
1421
|
-
if (target.consecutiveAuthFailures >= maxFailures) {
|
|
1422
|
-
target.isAuthDisabled = true;
|
|
1423
|
-
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1424
|
-
}
|
|
1487
|
+
target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
|
|
1425
1488
|
}
|
|
1426
1489
|
});
|
|
1427
1490
|
} catch {
|
|
1428
1491
|
debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
|
|
1429
1492
|
}
|
|
1430
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
|
+
}
|
|
1431
1503
|
};
|
|
1432
1504
|
}
|
|
1433
1505
|
|
|
@@ -1776,6 +1848,8 @@ var anthropicOAuthAdapter = {
|
|
|
1776
1848
|
oauthBetaHeader: "oauth-2025-04-20",
|
|
1777
1849
|
requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
|
1778
1850
|
cliUserAgent: "claude-cli/2.1.2 (external, cli)",
|
|
1851
|
+
cliVersion: "2.1.80",
|
|
1852
|
+
billingSalt: "59cf53e54c78",
|
|
1779
1853
|
toolPrefix: "mcp_",
|
|
1780
1854
|
accountStorageFilename: "anthropic-multi-account-accounts.json",
|
|
1781
1855
|
transform: {
|
|
@@ -1808,6 +1882,8 @@ var openAIOAuthAdapter = {
|
|
|
1808
1882
|
oauthBetaHeader: "",
|
|
1809
1883
|
requestBetaHeader: "",
|
|
1810
1884
|
cliUserAgent: "opencode/1.1.53",
|
|
1885
|
+
cliVersion: "",
|
|
1886
|
+
billingSalt: "",
|
|
1811
1887
|
toolPrefix: "mcp_",
|
|
1812
1888
|
accountStorageFilename: "openai-multi-account-accounts.json",
|
|
1813
1889
|
transform: {
|
|
@@ -1824,16 +1900,414 @@ var openAIOAuthAdapter = {
|
|
|
1824
1900
|
},
|
|
1825
1901
|
supported: true
|
|
1826
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
|
+
};
|
|
1827
2294
|
export {
|
|
1828
2295
|
ACCOUNTS_FILENAME,
|
|
1829
2296
|
ANSI,
|
|
1830
2297
|
AccountSelectionStrategySchema,
|
|
1831
2298
|
AccountStorageSchema,
|
|
1832
2299
|
AccountStore,
|
|
2300
|
+
CascadeStateManager,
|
|
2301
|
+
ChainConfigSchema,
|
|
2302
|
+
ChainEntryConfigSchema,
|
|
1833
2303
|
CredentialRefreshPatchSchema,
|
|
1834
2304
|
OAuthCredentialsSchema,
|
|
1835
2305
|
PluginConfigSchema,
|
|
2306
|
+
PoolChainConfigSchema,
|
|
2307
|
+
PoolConfigSchema,
|
|
2308
|
+
PoolManager,
|
|
1836
2309
|
StoredAccountSchema,
|
|
2310
|
+
TokenRefreshError,
|
|
1837
2311
|
UsageLimitEntrySchema,
|
|
1838
2312
|
UsageLimitsSchema,
|
|
1839
2313
|
anthropicOAuthAdapter,
|
|
@@ -1847,14 +2321,17 @@ export {
|
|
|
1847
2321
|
deduplicateAccounts,
|
|
1848
2322
|
formatWaitTime,
|
|
1849
2323
|
getAccountLabel,
|
|
2324
|
+
getClearedOAuthBody,
|
|
1850
2325
|
getConfig,
|
|
1851
2326
|
getConfigDir2 as getConfigDir,
|
|
1852
2327
|
getErrorCode,
|
|
1853
2328
|
initCoreConfig,
|
|
1854
2329
|
isClaimedByOther,
|
|
1855
2330
|
isTTY,
|
|
2331
|
+
isTokenRefreshError,
|
|
1856
2332
|
loadAccounts,
|
|
1857
2333
|
loadConfig,
|
|
2334
|
+
loadPoolChainConfig,
|
|
1858
2335
|
migrateFromAuthJson,
|
|
1859
2336
|
openAIOAuthAdapter,
|
|
1860
2337
|
parseKey,
|
|
@@ -1862,6 +2339,7 @@ export {
|
|
|
1862
2339
|
readStorageFromDisk,
|
|
1863
2340
|
releaseClaim,
|
|
1864
2341
|
resetConfigCache,
|
|
2342
|
+
savePoolChainConfig,
|
|
1865
2343
|
select,
|
|
1866
2344
|
setAccountsFilename,
|
|
1867
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
|
+
};
|