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.
Files changed (3) hide show
  1. package/dist/cli.js +99 -47
  2. package/dist/index.js +77 -30
  3. 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
- function safeWriteJSON(filePath, data) {
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.copyFileSync(filePath, bakPath);
95
+ await fs.promises.copyFile(filePath, bakPath);
80
96
  } catch {}
81
97
  }
82
- fs.writeFileSync(tmpPath, content, { mode: 384 });
83
- fs.renameSync(tmpPath, filePath);
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
- name,
187
- {
188
- session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
189
- weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
190
- weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
191
- weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
192
- rejected: { ...EMPTY_METRIC, ...accountUsage?.rejected },
193
- timestamp: accountUsage?.timestamp
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(STATE_FILE, {}));
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] > Date.now();
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
- if (usage.rejected.status === "rejected") {
843
- console.log(` ${RED}Rate limit status: REJECTED${RESET}`);
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
- const resetMs = health.earliestReset * 1000;
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
- function safeWriteJSON(filePath, data) {
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.copyFileSync(filePath, bakPath);
480
+ await fs.promises.copyFile(filePath, bakPath);
465
481
  } catch {}
466
482
  }
467
- fs.writeFileSync(tmpPath, content, { mode: 384 });
468
- fs.renameSync(tmpPath, filePath);
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
- name,
555
- {
556
- session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
557
- weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
558
- weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
559
- weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
560
- rejected: { ...EMPTY_METRIC, ...accountUsage?.rejected },
561
- timestamp: accountUsage?.timestamp
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(STATE_FILE, {}));
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
- if (!/^anthropic-ratelimit-unified-(?:.*-)?status$/.test(normalizedName) || headerValue.toLowerCase() !== "rejected") {
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "OpenCode auth plugin for multi-account Anthropic Claude Max rotation with automatic failover.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",