oc-auth-switcher 0.7.0 → 0.7.2
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/cli.js +99 -47
- package/dist/index.js +77 -30
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// src/accounts.ts
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path2 from "path";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
7
8
|
|
|
8
9
|
// src/constants.ts
|
|
9
10
|
import path from "path";
|
|
@@ -20,6 +21,14 @@ var METRIC_MODEL_FAMILY = {
|
|
|
20
21
|
weekly7dSonnet: "sonnet",
|
|
21
22
|
weekly7dFable: "fable"
|
|
22
23
|
};
|
|
24
|
+
var UNIFIED_RATE_LIMIT_PREFIX = "anthropic-ratelimit-unified-";
|
|
25
|
+
var QUOTA_WINDOW_SCOPE_PATTERN = /^\d+[hmd](?:_[a-z0-9.\-]+)?$/i;
|
|
26
|
+
function quotaWindowScope(prefix) {
|
|
27
|
+
if (!prefix)
|
|
28
|
+
return;
|
|
29
|
+
const scope = prefix.toLowerCase().startsWith(UNIFIED_RATE_LIMIT_PREFIX) ? prefix.slice(UNIFIED_RATE_LIMIT_PREFIX.length) : prefix;
|
|
30
|
+
return QUOTA_WINDOW_SCOPE_PATTERN.test(scope) ? scope : undefined;
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
25
34
|
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
@@ -69,18 +78,52 @@ function safeReadJSON(filePath, fallback) {
|
|
|
69
78
|
}
|
|
70
79
|
return fallback;
|
|
71
80
|
}
|
|
72
|
-
|
|
81
|
+
var writeQueues = new Map;
|
|
82
|
+
var MAX_RENAME_ATTEMPTS = 3;
|
|
83
|
+
var RENAME_RETRY_CODES = new Set(["ENOENT", "EEXIST", "EPERM"]);
|
|
84
|
+
function errorCode(error) {
|
|
85
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
86
|
+
}
|
|
87
|
+
async function writeJSON(filePath, data, options) {
|
|
73
88
|
ensureDir(filePath);
|
|
74
89
|
const content = JSON.stringify(data, null, 2);
|
|
75
|
-
const tmpPath = filePath + ".tmp";
|
|
76
90
|
const bakPath = filePath + ".bak";
|
|
91
|
+
const rename = options.rename ?? fs.promises.rename;
|
|
92
|
+
const retryDelayMs = options.retryDelayMs ?? 5;
|
|
77
93
|
if (fs.existsSync(filePath)) {
|
|
78
94
|
try {
|
|
79
|
-
fs.
|
|
95
|
+
await fs.promises.copyFile(filePath, bakPath);
|
|
80
96
|
} catch {}
|
|
81
97
|
}
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
let lastError;
|
|
99
|
+
for (let attempt = 1;attempt <= MAX_RENAME_ATTEMPTS; attempt++) {
|
|
100
|
+
const tmpPath = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
|
|
101
|
+
try {
|
|
102
|
+
await fs.promises.writeFile(tmpPath, content, { mode: 384 });
|
|
103
|
+
await rename(tmpPath, filePath);
|
|
104
|
+
return;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
lastError = error;
|
|
107
|
+
await fs.promises.rm(tmpPath, { force: true }).catch(() => {});
|
|
108
|
+
if (!RENAME_RETRY_CODES.has(errorCode(error) ?? "") || attempt === MAX_RENAME_ATTEMPTS) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
function safeWriteJSON(filePath, data, options = {}) {
|
|
117
|
+
const previous = writeQueues.get(filePath) ?? Promise.resolve();
|
|
118
|
+
const current = previous.catch(() => {}).then(() => writeJSON(filePath, data, options)).catch((error) => {
|
|
119
|
+
console.warn(`[oc-auth-switcher] Could not safely write ${path2.basename(filePath)}:`, error instanceof Error ? error.message : String(error));
|
|
120
|
+
});
|
|
121
|
+
writeQueues.set(filePath, current);
|
|
122
|
+
current.finally(() => {
|
|
123
|
+
if (writeQueues.get(filePath) === current)
|
|
124
|
+
writeQueues.delete(filePath);
|
|
125
|
+
});
|
|
126
|
+
return current;
|
|
84
127
|
}
|
|
85
128
|
function loadAccounts() {
|
|
86
129
|
const raw = safeReadJSON(ACCOUNTS_FILE, {
|
|
@@ -90,9 +133,9 @@ function loadAccounts() {
|
|
|
90
133
|
return { accounts };
|
|
91
134
|
}
|
|
92
135
|
function saveAccounts(data) {
|
|
93
|
-
safeWriteJSON(ACCOUNTS_FILE, data);
|
|
136
|
+
return safeWriteJSON(ACCOUNTS_FILE, data);
|
|
94
137
|
}
|
|
95
|
-
function addAccount(account) {
|
|
138
|
+
async function addAccount(account) {
|
|
96
139
|
const data = loadAccounts();
|
|
97
140
|
const idx = data.accounts.findIndex((a) => a.name === account.name);
|
|
98
141
|
if (idx >= 0) {
|
|
@@ -100,23 +143,23 @@ function addAccount(account) {
|
|
|
100
143
|
} else {
|
|
101
144
|
data.accounts.push(account);
|
|
102
145
|
}
|
|
103
|
-
saveAccounts(data);
|
|
146
|
+
await saveAccounts(data);
|
|
104
147
|
return data;
|
|
105
148
|
}
|
|
106
|
-
function removeAccount(name) {
|
|
149
|
+
async function removeAccount(name) {
|
|
107
150
|
const data = loadAccounts();
|
|
108
151
|
data.accounts = data.accounts.filter((a) => a.name !== name);
|
|
109
|
-
saveAccounts(data);
|
|
152
|
+
await saveAccounts(data);
|
|
110
153
|
return data;
|
|
111
154
|
}
|
|
112
|
-
function updateAccountTokens(name, access, refresh, expires) {
|
|
155
|
+
async function updateAccountTokens(name, access, refresh, expires) {
|
|
113
156
|
const data = loadAccounts();
|
|
114
157
|
const account = data.accounts.find((a) => a.name === name);
|
|
115
158
|
if (account) {
|
|
116
159
|
account.access = access;
|
|
117
160
|
account.refresh = refresh;
|
|
118
161
|
account.expires = expires;
|
|
119
|
-
saveAccounts(data);
|
|
162
|
+
await saveAccounts(data);
|
|
120
163
|
}
|
|
121
164
|
}
|
|
122
165
|
async function refreshAccountToken(refreshTokenValue) {
|
|
@@ -182,17 +225,20 @@ function normalizeState(raw) {
|
|
|
182
225
|
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
183
226
|
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
184
227
|
} : threshold ?? defaults.config.threshold;
|
|
185
|
-
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
228
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => {
|
|
229
|
+
const rejected = { ...EMPTY_METRIC, ...accountUsage?.rejected };
|
|
230
|
+
return [
|
|
231
|
+
name,
|
|
232
|
+
{
|
|
233
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
234
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
235
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
236
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
237
|
+
rejected: rejected.status?.toLowerCase() === "rejected" && !quotaWindowScope(rejected.prefix) ? { ...EMPTY_METRIC } : rejected,
|
|
238
|
+
timestamp: accountUsage?.timestamp
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
}));
|
|
196
242
|
return {
|
|
197
243
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
198
244
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
@@ -203,11 +249,11 @@ function normalizeState(raw) {
|
|
|
203
249
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
204
250
|
};
|
|
205
251
|
}
|
|
206
|
-
function loadState() {
|
|
207
|
-
return normalizeState(safeReadJSON(
|
|
252
|
+
function loadState(stateFile = STATE_FILE) {
|
|
253
|
+
return normalizeState(safeReadJSON(stateFile, {}));
|
|
208
254
|
}
|
|
209
255
|
function saveState(state) {
|
|
210
|
-
safeWriteJSON(STATE_FILE, state);
|
|
256
|
+
return safeWriteJSON(STATE_FILE, state);
|
|
211
257
|
}
|
|
212
258
|
function getThresholds(config) {
|
|
213
259
|
if (typeof config.threshold === "number") {
|
|
@@ -426,6 +472,10 @@ function formatRelativeDuration(targetMs, nowMs = Date.now()) {
|
|
|
426
472
|
parts.push(`${Math.max(1, Math.round(remaining))}s`);
|
|
427
473
|
return difference >= 0 ? `in ${parts.join(" ")}` : `${parts.join(" ")} ago`;
|
|
428
474
|
}
|
|
475
|
+
function formatResetTime(reset, nowMs = Date.now()) {
|
|
476
|
+
const resetMs = reset * 1000;
|
|
477
|
+
return `${new Date(resetMs).toLocaleString()} (${formatRelativeDuration(resetMs, nowMs)})`;
|
|
478
|
+
}
|
|
429
479
|
function displayRejectionPrefix(prefix) {
|
|
430
480
|
return prefix?.replace(/^anthropic-ratelimit-unified-/i, "");
|
|
431
481
|
}
|
|
@@ -720,7 +770,7 @@ ${BOLD}Starting OAuth flow for account: ${CYAN}${name}${RESET}
|
|
|
720
770
|
${RED}Authentication failed${RESET}`);
|
|
721
771
|
process.exit(1);
|
|
722
772
|
}
|
|
723
|
-
addAccount({
|
|
773
|
+
await addAccount({
|
|
724
774
|
name,
|
|
725
775
|
access: exchangeResult.access,
|
|
726
776
|
refresh: exchangeResult.refresh,
|
|
@@ -733,7 +783,7 @@ ${GREEN}Account "${name}" added successfully.${RESET}`);
|
|
|
733
783
|
if (data.accounts.length === 1) {
|
|
734
784
|
const state = loadState();
|
|
735
785
|
state.currentAccount = name;
|
|
736
|
-
saveState(state);
|
|
786
|
+
await saveState(state);
|
|
737
787
|
console.log(`
|
|
738
788
|
${YELLOW}This is the only account \u2014 set as active.${RESET}`);
|
|
739
789
|
}
|
|
@@ -782,10 +832,10 @@ ${RED}Re-authentication failed${RESET}`);
|
|
|
782
832
|
account.access = exchangeResult.access;
|
|
783
833
|
account.refresh = exchangeResult.refresh;
|
|
784
834
|
account.expires = exchangeResult.expires;
|
|
785
|
-
saveAccounts(data);
|
|
835
|
+
await saveAccounts(data);
|
|
786
836
|
const state = loadState();
|
|
787
837
|
clearAuthFailure(state, name);
|
|
788
|
-
saveState(state);
|
|
838
|
+
await saveState(state);
|
|
789
839
|
console.log(`
|
|
790
840
|
${GREEN}Account "${name}" re-authenticated successfully.${RESET}`);
|
|
791
841
|
}
|
|
@@ -798,7 +848,7 @@ async function refreshExpiredAccounts(data, state) {
|
|
|
798
848
|
account.access = result.access;
|
|
799
849
|
account.refresh = result.refresh;
|
|
800
850
|
account.expires = result.expires;
|
|
801
|
-
updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
851
|
+
await updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
802
852
|
clearAuthFailure(state, account.name);
|
|
803
853
|
}
|
|
804
854
|
}
|
|
@@ -809,7 +859,7 @@ async function cmdUsage(args) {
|
|
|
809
859
|
const data = loadAccounts();
|
|
810
860
|
const state = loadState();
|
|
811
861
|
await refreshExpiredAccounts(data, state);
|
|
812
|
-
saveState(state);
|
|
862
|
+
await saveState(state);
|
|
813
863
|
resolveStaleMetrics(state);
|
|
814
864
|
ensureAccountsInState(state, data.accounts.map((a) => a.name));
|
|
815
865
|
const thresholds = getThresholds(state.config);
|
|
@@ -830,8 +880,9 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
830
880
|
}
|
|
831
881
|
for (const account of data.accounts) {
|
|
832
882
|
const usage = state.usage[account.name];
|
|
883
|
+
const now = Date.now();
|
|
833
884
|
const isActive = account.name === state.currentAccount;
|
|
834
|
-
const isCooling = !!state.authFailures[account.name] && state.authFailures[account.name] >
|
|
885
|
+
const isCooling = !!state.authFailures[account.name] && state.authFailures[account.name] > now;
|
|
835
886
|
const tag = isActive ? `${GREEN} [ACTIVE]${RESET}` : isCooling ? `${RED} [COOLDOWN]${RESET}` : "";
|
|
836
887
|
console.log(` ${BOLD}${account.name}${RESET}${tag}`);
|
|
837
888
|
if (usage) {
|
|
@@ -839,8 +890,10 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
839
890
|
console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}`);
|
|
840
891
|
console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}`);
|
|
841
892
|
console.log(` 7d fable: ${progressBar(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}`);
|
|
842
|
-
|
|
843
|
-
|
|
893
|
+
const rejection = deriveAccountHealth(account, usage, thresholds, state.authFailures[account.name], now).reasons.find((reason) => reason.kind === "rejection");
|
|
894
|
+
if (rejection) {
|
|
895
|
+
const reset = rejection.reset ? ` \u2014 resets ${formatResetTime(rejection.reset, now)}` : "";
|
|
896
|
+
console.log(` ${RED}Rate limit status: ${rejection.message}${reset}${RESET}`);
|
|
844
897
|
}
|
|
845
898
|
if (usage.timestamp) {
|
|
846
899
|
console.log(` ${DIM}Last updated: ${usage.timestamp}${RESET}`);
|
|
@@ -871,7 +924,7 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
871
924
|
new Promise(() => {});
|
|
872
925
|
}
|
|
873
926
|
}
|
|
874
|
-
function cmdConfig(args) {
|
|
927
|
+
async function cmdConfig(args) {
|
|
875
928
|
const state = loadState();
|
|
876
929
|
if (args.length === 0) {
|
|
877
930
|
const thresholds = getThresholds(state.config);
|
|
@@ -921,9 +974,9 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
921
974
|
console.log(`${GREEN}Reset to default threshold: ${(DEFAULT_THRESHOLD * 100).toFixed(0)}%${RESET}`);
|
|
922
975
|
}
|
|
923
976
|
}
|
|
924
|
-
saveState(state);
|
|
977
|
+
await saveState(state);
|
|
925
978
|
}
|
|
926
|
-
function cmdSwitch(args) {
|
|
979
|
+
async function cmdSwitch(args) {
|
|
927
980
|
const data = loadAccounts();
|
|
928
981
|
if (data.accounts.length === 0) {
|
|
929
982
|
console.error(`${RED}No accounts configured.${RESET}`);
|
|
@@ -950,7 +1003,7 @@ ${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
|
950
1003
|
console.log(`${CYAN}Switching to account: ${name}...${RESET}`);
|
|
951
1004
|
const state = loadState();
|
|
952
1005
|
state.currentAccount = name;
|
|
953
|
-
saveState(state);
|
|
1006
|
+
await saveState(state);
|
|
954
1007
|
console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
|
|
955
1008
|
}
|
|
956
1009
|
function healthLabel(health) {
|
|
@@ -1032,8 +1085,7 @@ ${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
|
1032
1085
|
console.log(` ${RED}Reason: ${reason.message}${RESET}`);
|
|
1033
1086
|
}
|
|
1034
1087
|
if (health.earliestReset) {
|
|
1035
|
-
|
|
1036
|
-
console.log(` Reset: ${new Date(resetMs).toLocaleString()} (${formatRelativeDuration(resetMs, now)})`);
|
|
1088
|
+
console.log(` Reset: ${formatResetTime(health.earliestReset, now)}`);
|
|
1037
1089
|
}
|
|
1038
1090
|
const tokenColor = health.reasons.some((reason) => reason.kind === "token") ? RED : DIM;
|
|
1039
1091
|
console.log(` ${tokenColor}Token: ${health.tokenExpiry}${RESET}`);
|
|
@@ -1044,7 +1096,7 @@ ${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
|
1044
1096
|
console.log(` ${DIM}State file: ${STATE_FILE}${RESET}`);
|
|
1045
1097
|
console.log();
|
|
1046
1098
|
}
|
|
1047
|
-
function cmdRemove(args) {
|
|
1099
|
+
async function cmdRemove(args) {
|
|
1048
1100
|
const name = args[0];
|
|
1049
1101
|
if (!name) {
|
|
1050
1102
|
console.error(`${RED}Usage: oc-auth-switcher remove <account-name>${RESET}`);
|
|
@@ -1056,12 +1108,12 @@ function cmdRemove(args) {
|
|
|
1056
1108
|
console.error(`${RED}Account "${name}" not found${RESET}`);
|
|
1057
1109
|
process.exit(1);
|
|
1058
1110
|
}
|
|
1059
|
-
removeAccount(name);
|
|
1111
|
+
await removeAccount(name);
|
|
1060
1112
|
console.log(`${GREEN}Account "${name}" removed.${RESET}`);
|
|
1061
1113
|
const state = loadState();
|
|
1062
1114
|
if (state.currentAccount === name) {
|
|
1063
1115
|
state.currentAccount = null;
|
|
1064
|
-
saveState(state);
|
|
1116
|
+
await saveState(state);
|
|
1065
1117
|
console.log(`${YELLOW}This was the active account. Rotation will pick a new one automatically.${RESET}`);
|
|
1066
1118
|
}
|
|
1067
1119
|
}
|
|
@@ -1112,18 +1164,18 @@ async function main() {
|
|
|
1112
1164
|
break;
|
|
1113
1165
|
case "config":
|
|
1114
1166
|
case "c":
|
|
1115
|
-
cmdConfig(commandArgs);
|
|
1167
|
+
await cmdConfig(commandArgs);
|
|
1116
1168
|
break;
|
|
1117
1169
|
case "switch":
|
|
1118
1170
|
case "s":
|
|
1119
|
-
cmdSwitch(commandArgs);
|
|
1171
|
+
await cmdSwitch(commandArgs);
|
|
1120
1172
|
break;
|
|
1121
1173
|
case "status":
|
|
1122
1174
|
cmdStatus();
|
|
1123
1175
|
break;
|
|
1124
1176
|
case "remove":
|
|
1125
1177
|
case "rm":
|
|
1126
|
-
cmdRemove(commandArgs);
|
|
1178
|
+
await cmdRemove(commandArgs);
|
|
1127
1179
|
break;
|
|
1128
1180
|
case "help":
|
|
1129
1181
|
case "--help":
|
package/dist/index.js
CHANGED
|
@@ -406,6 +406,7 @@ async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
406
406
|
// src/accounts.ts
|
|
407
407
|
import fs from "node:fs";
|
|
408
408
|
import path2 from "node:path";
|
|
409
|
+
import { randomUUID } from "node:crypto";
|
|
409
410
|
|
|
410
411
|
// src/constants.ts
|
|
411
412
|
import path from "node:path";
|
|
@@ -422,6 +423,14 @@ var METRIC_MODEL_FAMILY = {
|
|
|
422
423
|
weekly7dSonnet: "sonnet",
|
|
423
424
|
weekly7dFable: "fable"
|
|
424
425
|
};
|
|
426
|
+
var UNIFIED_RATE_LIMIT_PREFIX = "anthropic-ratelimit-unified-";
|
|
427
|
+
var QUOTA_WINDOW_SCOPE_PATTERN = /^\d+[hmd](?:_[a-z0-9.\-]+)?$/i;
|
|
428
|
+
function quotaWindowScope(prefix) {
|
|
429
|
+
if (!prefix)
|
|
430
|
+
return;
|
|
431
|
+
const scope = prefix.toLowerCase().startsWith(UNIFIED_RATE_LIMIT_PREFIX) ? prefix.slice(UNIFIED_RATE_LIMIT_PREFIX.length) : prefix;
|
|
432
|
+
return QUOTA_WINDOW_SCOPE_PATTERN.test(scope) ? scope : undefined;
|
|
433
|
+
}
|
|
425
434
|
|
|
426
435
|
// src/accounts.ts
|
|
427
436
|
function normalizeAccount(raw) {
|
|
@@ -454,18 +463,52 @@ function safeReadJSON(filePath, fallback) {
|
|
|
454
463
|
}
|
|
455
464
|
return fallback;
|
|
456
465
|
}
|
|
457
|
-
|
|
466
|
+
var writeQueues = new Map;
|
|
467
|
+
var MAX_RENAME_ATTEMPTS = 3;
|
|
468
|
+
var RENAME_RETRY_CODES = new Set(["ENOENT", "EEXIST", "EPERM"]);
|
|
469
|
+
function errorCode(error) {
|
|
470
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
471
|
+
}
|
|
472
|
+
async function writeJSON(filePath, data, options) {
|
|
458
473
|
ensureDir(filePath);
|
|
459
474
|
const content = JSON.stringify(data, null, 2);
|
|
460
|
-
const tmpPath = filePath + ".tmp";
|
|
461
475
|
const bakPath = filePath + ".bak";
|
|
476
|
+
const rename = options.rename ?? fs.promises.rename;
|
|
477
|
+
const retryDelayMs = options.retryDelayMs ?? 5;
|
|
462
478
|
if (fs.existsSync(filePath)) {
|
|
463
479
|
try {
|
|
464
|
-
fs.
|
|
480
|
+
await fs.promises.copyFile(filePath, bakPath);
|
|
465
481
|
} catch {}
|
|
466
482
|
}
|
|
467
|
-
|
|
468
|
-
|
|
483
|
+
let lastError;
|
|
484
|
+
for (let attempt = 1;attempt <= MAX_RENAME_ATTEMPTS; attempt++) {
|
|
485
|
+
const tmpPath = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
|
|
486
|
+
try {
|
|
487
|
+
await fs.promises.writeFile(tmpPath, content, { mode: 384 });
|
|
488
|
+
await rename(tmpPath, filePath);
|
|
489
|
+
return;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
lastError = error;
|
|
492
|
+
await fs.promises.rm(tmpPath, { force: true }).catch(() => {});
|
|
493
|
+
if (!RENAME_RETRY_CODES.has(errorCode(error) ?? "") || attempt === MAX_RENAME_ATTEMPTS) {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
throw lastError;
|
|
500
|
+
}
|
|
501
|
+
function safeWriteJSON(filePath, data, options = {}) {
|
|
502
|
+
const previous = writeQueues.get(filePath) ?? Promise.resolve();
|
|
503
|
+
const current = previous.catch(() => {}).then(() => writeJSON(filePath, data, options)).catch((error) => {
|
|
504
|
+
console.warn(`[oc-auth-switcher] Could not safely write ${path2.basename(filePath)}:`, error instanceof Error ? error.message : String(error));
|
|
505
|
+
});
|
|
506
|
+
writeQueues.set(filePath, current);
|
|
507
|
+
current.finally(() => {
|
|
508
|
+
if (writeQueues.get(filePath) === current)
|
|
509
|
+
writeQueues.delete(filePath);
|
|
510
|
+
});
|
|
511
|
+
return current;
|
|
469
512
|
}
|
|
470
513
|
function loadAccounts() {
|
|
471
514
|
const raw = safeReadJSON(ACCOUNTS_FILE, {
|
|
@@ -475,16 +518,16 @@ function loadAccounts() {
|
|
|
475
518
|
return { accounts };
|
|
476
519
|
}
|
|
477
520
|
function saveAccounts(data) {
|
|
478
|
-
safeWriteJSON(ACCOUNTS_FILE, data);
|
|
521
|
+
return safeWriteJSON(ACCOUNTS_FILE, data);
|
|
479
522
|
}
|
|
480
|
-
function updateAccountTokens(name, access, refresh, expires) {
|
|
523
|
+
async function updateAccountTokens(name, access, refresh, expires) {
|
|
481
524
|
const data = loadAccounts();
|
|
482
525
|
const account = data.accounts.find((a) => a.name === name);
|
|
483
526
|
if (account) {
|
|
484
527
|
account.access = access;
|
|
485
528
|
account.refresh = refresh;
|
|
486
529
|
account.expires = expires;
|
|
487
|
-
saveAccounts(data);
|
|
530
|
+
await saveAccounts(data);
|
|
488
531
|
}
|
|
489
532
|
}
|
|
490
533
|
async function refreshAccountToken(refreshTokenValue) {
|
|
@@ -550,17 +593,20 @@ function normalizeState(raw) {
|
|
|
550
593
|
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
551
594
|
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
552
595
|
} : threshold ?? defaults.config.threshold;
|
|
553
|
-
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) =>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
596
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => {
|
|
597
|
+
const rejected = { ...EMPTY_METRIC, ...accountUsage?.rejected };
|
|
598
|
+
return [
|
|
599
|
+
name,
|
|
600
|
+
{
|
|
601
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
602
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
603
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
604
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
605
|
+
rejected: rejected.status?.toLowerCase() === "rejected" && !quotaWindowScope(rejected.prefix) ? { ...EMPTY_METRIC } : rejected,
|
|
606
|
+
timestamp: accountUsage?.timestamp
|
|
607
|
+
}
|
|
608
|
+
];
|
|
609
|
+
}));
|
|
564
610
|
return {
|
|
565
611
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
566
612
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
@@ -571,11 +617,11 @@ function normalizeState(raw) {
|
|
|
571
617
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
572
618
|
};
|
|
573
619
|
}
|
|
574
|
-
function loadState() {
|
|
575
|
-
return normalizeState(safeReadJSON(
|
|
620
|
+
function loadState(stateFile = STATE_FILE) {
|
|
621
|
+
return normalizeState(safeReadJSON(stateFile, {}));
|
|
576
622
|
}
|
|
577
623
|
function saveState(state) {
|
|
578
|
-
safeWriteJSON(STATE_FILE, state);
|
|
624
|
+
return safeWriteJSON(STATE_FILE, state);
|
|
579
625
|
}
|
|
580
626
|
function getThresholds(config) {
|
|
581
627
|
if (typeof config.threshold === "number") {
|
|
@@ -692,7 +738,8 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
692
738
|
}
|
|
693
739
|
for (const [headerName, headerValue] of headers.entries()) {
|
|
694
740
|
const normalizedName = headerName.toLowerCase();
|
|
695
|
-
|
|
741
|
+
const statusMatch = normalizedName.match(/^anthropic-ratelimit-unified-(.+)-status$/);
|
|
742
|
+
if (!statusMatch || !quotaWindowScope(statusMatch[1]) || headerValue.toLowerCase() !== "rejected") {
|
|
696
743
|
continue;
|
|
697
744
|
}
|
|
698
745
|
const prefix = normalizedName.slice(0, -"-status".length);
|
|
@@ -865,13 +912,13 @@ function selectionSnapshot(state) {
|
|
|
865
912
|
currentAccount: state.currentAccount
|
|
866
913
|
};
|
|
867
914
|
}
|
|
868
|
-
function saveRequestState(state, initiallyLoaded) {
|
|
915
|
+
async function saveRequestState(state, initiallyLoaded) {
|
|
869
916
|
const onDisk = loadState();
|
|
870
917
|
const selectionChangedSinceLoad = onDisk.currentAccount !== initiallyLoaded.currentAccount;
|
|
871
918
|
if (selectionChangedSinceLoad) {
|
|
872
919
|
state.currentAccount = onDisk.currentAccount;
|
|
873
920
|
}
|
|
874
|
-
saveState(state);
|
|
921
|
+
await saveState(state);
|
|
875
922
|
}
|
|
876
923
|
function getRequestModel(body) {
|
|
877
924
|
if (typeof body !== "string")
|
|
@@ -973,13 +1020,13 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
973
1020
|
account.access = result.access;
|
|
974
1021
|
account.refresh = result.refresh;
|
|
975
1022
|
account.expires = result.expires;
|
|
976
|
-
updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
1023
|
+
await updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
977
1024
|
} else {
|
|
978
1025
|
markAuthFailure(state, account.name);
|
|
979
1026
|
const available = accounts.filter((candidate) => !attemptedAccounts.has(candidate.name) && (!state.authFailures[candidate.name] || state.authFailures[candidate.name] <= Date.now()));
|
|
980
1027
|
const next = available.length > 0 ? selectAccount(available, state, model).account : undefined;
|
|
981
1028
|
if (!next) {
|
|
982
|
-
saveRequestState(state, initiallyLoadedSelection);
|
|
1029
|
+
await saveRequestState(state, initiallyLoadedSelection);
|
|
983
1030
|
throw new Error(`[oc-auth-switcher] All accounts failed token refresh`);
|
|
984
1031
|
}
|
|
985
1032
|
account = next;
|
|
@@ -1021,7 +1068,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1021
1068
|
next.access = result.access;
|
|
1022
1069
|
next.refresh = result.refresh;
|
|
1023
1070
|
next.expires = result.expires;
|
|
1024
|
-
updateAccountTokens(next.name, result.access, result.refresh, result.expires);
|
|
1071
|
+
await updateAccountTokens(next.name, result.access, result.refresh, result.expires);
|
|
1025
1072
|
}
|
|
1026
1073
|
}
|
|
1027
1074
|
const retryHeaders = mergeHeaders(input, init);
|
|
@@ -1036,7 +1083,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1036
1083
|
});
|
|
1037
1084
|
updateUsageFromHeaders(state, next.name, retryResponse.headers);
|
|
1038
1085
|
clearAuthFailure(state, next.name);
|
|
1039
|
-
saveRequestState(state, initiallyLoadedSelection);
|
|
1086
|
+
await saveRequestState(state, initiallyLoadedSelection);
|
|
1040
1087
|
return createStrippedStream(retryResponse);
|
|
1041
1088
|
}
|
|
1042
1089
|
}
|
|
@@ -1044,7 +1091,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1044
1091
|
updateUsageFromHeaders(state, account.name, response.headers);
|
|
1045
1092
|
clearAuthFailure(state, account.name);
|
|
1046
1093
|
state.requestCount = (state.requestCount || 0) + 1;
|
|
1047
|
-
saveRequestState(state, initiallyLoadedSelection);
|
|
1094
|
+
await saveRequestState(state, initiallyLoadedSelection);
|
|
1048
1095
|
return createStrippedStream(response);
|
|
1049
1096
|
}
|
|
1050
1097
|
};
|