oc-auth-switcher 0.2.1 → 0.4.0
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 +90 -19
- package/dist/index.js +151 -18
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -152,11 +152,15 @@ var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
|
|
|
152
152
|
var EMPTY_USAGE = {
|
|
153
153
|
session5h: { ...EMPTY_METRIC },
|
|
154
154
|
weekly7d: { ...EMPTY_METRIC },
|
|
155
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
155
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
156
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
157
|
+
rejected: { ...EMPTY_METRIC }
|
|
156
158
|
};
|
|
157
159
|
function defaultState() {
|
|
158
160
|
return {
|
|
159
161
|
currentAccount: null,
|
|
162
|
+
selectionMode: "auto",
|
|
163
|
+
manualAccount: null,
|
|
160
164
|
lastRotationCheck: 0,
|
|
161
165
|
requestCount: 0,
|
|
162
166
|
config: {
|
|
@@ -167,21 +171,43 @@ function defaultState() {
|
|
|
167
171
|
authFailures: {}
|
|
168
172
|
};
|
|
169
173
|
}
|
|
170
|
-
function
|
|
171
|
-
const raw = safeReadJSON(STATE_FILE, {});
|
|
174
|
+
function normalizeState(raw) {
|
|
172
175
|
const defaults = defaultState();
|
|
176
|
+
const threshold = raw.config?.threshold;
|
|
177
|
+
const migratedThreshold = typeof threshold === "object" && threshold !== null ? {
|
|
178
|
+
session5h: threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
179
|
+
weekly7d: threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
180
|
+
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
181
|
+
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
182
|
+
} : threshold ?? defaults.config.threshold;
|
|
183
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => [
|
|
184
|
+
name,
|
|
185
|
+
{
|
|
186
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
187
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
188
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
189
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
190
|
+
rejected: { ...EMPTY_METRIC, ...accountUsage?.rejected },
|
|
191
|
+
timestamp: accountUsage?.timestamp
|
|
192
|
+
}
|
|
193
|
+
]));
|
|
173
194
|
return {
|
|
174
195
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
196
|
+
selectionMode: raw.selectionMode === "manual" && raw.manualAccount ? "manual" : "auto",
|
|
197
|
+
manualAccount: raw.manualAccount ?? defaults.manualAccount,
|
|
175
198
|
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
176
199
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
177
200
|
config: {
|
|
178
|
-
threshold:
|
|
201
|
+
threshold: migratedThreshold,
|
|
179
202
|
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
180
203
|
},
|
|
181
|
-
usage
|
|
204
|
+
usage,
|
|
182
205
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
183
206
|
};
|
|
184
207
|
}
|
|
208
|
+
function loadState() {
|
|
209
|
+
return normalizeState(safeReadJSON(STATE_FILE, {}));
|
|
210
|
+
}
|
|
185
211
|
function saveState(state) {
|
|
186
212
|
safeWriteJSON(STATE_FILE, state);
|
|
187
213
|
}
|
|
@@ -190,16 +216,27 @@ function getThresholds(config) {
|
|
|
190
216
|
return {
|
|
191
217
|
session5h: config.threshold,
|
|
192
218
|
weekly7d: config.threshold,
|
|
193
|
-
weekly7dSonnet: config.threshold
|
|
219
|
+
weekly7dSonnet: config.threshold,
|
|
220
|
+
weekly7dFable: config.threshold
|
|
194
221
|
};
|
|
195
222
|
}
|
|
196
|
-
return
|
|
223
|
+
return {
|
|
224
|
+
session5h: config.threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
225
|
+
weekly7d: config.threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
226
|
+
weekly7dSonnet: config.threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
227
|
+
weekly7dFable: config.threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
228
|
+
};
|
|
197
229
|
}
|
|
198
230
|
function resolveStaleMetrics(state) {
|
|
199
231
|
const now = Date.now() / 1000;
|
|
200
232
|
for (const accountName of Object.keys(state.usage)) {
|
|
201
233
|
const usage = state.usage[accountName];
|
|
202
|
-
const metrics = [
|
|
234
|
+
const metrics = [
|
|
235
|
+
"session5h",
|
|
236
|
+
"weekly7d",
|
|
237
|
+
"weekly7dSonnet",
|
|
238
|
+
"weekly7dFable"
|
|
239
|
+
];
|
|
203
240
|
for (const key of metrics) {
|
|
204
241
|
const metric = usage[key];
|
|
205
242
|
if (metric.reset > 0 && metric.reset <= now) {
|
|
@@ -208,6 +245,9 @@ function resolveStaleMetrics(state) {
|
|
|
208
245
|
metric.status = "";
|
|
209
246
|
}
|
|
210
247
|
}
|
|
248
|
+
if (usage.rejected.reset > 0 && usage.rejected.reset <= now) {
|
|
249
|
+
usage.rejected = { ...EMPTY_METRIC };
|
|
250
|
+
}
|
|
211
251
|
}
|
|
212
252
|
}
|
|
213
253
|
function ensureAccountsInState(state, accountNames) {
|
|
@@ -216,7 +256,9 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
216
256
|
state.usage[name] = {
|
|
217
257
|
session5h: { ...EMPTY_METRIC },
|
|
218
258
|
weekly7d: { ...EMPTY_METRIC },
|
|
219
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
259
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
260
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
261
|
+
rejected: { ...EMPTY_METRIC }
|
|
220
262
|
};
|
|
221
263
|
}
|
|
222
264
|
}
|
|
@@ -538,7 +580,7 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
538
580
|
`);
|
|
539
581
|
console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
|
|
540
582
|
console.log(` Total accounts: ${data.accounts.length}`);
|
|
541
|
-
console.log(` Thresholds: 5h=${(thresholds.session5h * 100).toFixed(0)}% 7d=${(thresholds.weekly7d * 100).toFixed(0)}% 7d-sonnet=${(thresholds.weekly7dSonnet * 100).toFixed(0)}%`);
|
|
583
|
+
console.log(` Thresholds: 5h=${(thresholds.session5h * 100).toFixed(0)}% 7d=${(thresholds.weekly7d * 100).toFixed(0)}% 7d-sonnet=${(thresholds.weekly7dSonnet * 100).toFixed(0)}% 7d-fable=${(thresholds.weekly7dFable * 100).toFixed(0)}%`);
|
|
542
584
|
console.log();
|
|
543
585
|
if (data.accounts.length === 0) {
|
|
544
586
|
console.log(` ${DIM}No accounts configured. Run 'oc-auth-switcher add' to add one.${RESET}
|
|
@@ -555,6 +597,10 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
555
597
|
console.log(` 5h session: ${progressBar(usage.session5h.utilization, thresholds.session5h)}`);
|
|
556
598
|
console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}`);
|
|
557
599
|
console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}`);
|
|
600
|
+
console.log(` 7d fable: ${progressBar(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}`);
|
|
601
|
+
if (usage.rejected.status === "rejected") {
|
|
602
|
+
console.log(` ${RED}Rate limit status: REJECTED${RESET}`);
|
|
603
|
+
}
|
|
558
604
|
if (usage.timestamp) {
|
|
559
605
|
console.log(` ${DIM}Last updated: ${usage.timestamp}${RESET}`);
|
|
560
606
|
}
|
|
@@ -594,6 +640,7 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
594
640
|
console.log(` Threshold (5h): ${(thresholds.session5h * 100).toFixed(0)}%`);
|
|
595
641
|
console.log(` Threshold (7d): ${(thresholds.weekly7d * 100).toFixed(0)}%`);
|
|
596
642
|
console.log(` Threshold (7d sonnet): ${(thresholds.weekly7dSonnet * 100).toFixed(0)}%`);
|
|
643
|
+
console.log(` Threshold (7d fable): ${(thresholds.weekly7dFable * 100).toFixed(0)}%`);
|
|
597
644
|
console.log(` Check interval: ${state.config.checkInterval / 60000} min`);
|
|
598
645
|
console.log();
|
|
599
646
|
console.log(`${DIM} Config file: ${STATE_FILE}${RESET}`);
|
|
@@ -604,25 +651,31 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
604
651
|
const arg = args[i];
|
|
605
652
|
if (arg === "--threshold" && args[i + 1]) {
|
|
606
653
|
const val = parseFloat(args[++i]);
|
|
607
|
-
if (isNaN(val) || val
|
|
608
|
-
console.error(`${RED}Threshold must be
|
|
654
|
+
if (isNaN(val) || val <= 0 || val > 1) {
|
|
655
|
+
console.error(`${RED}Threshold must be greater than 0 and at most 1 (e.g., 0.90)${RESET}`);
|
|
609
656
|
process.exit(1);
|
|
610
657
|
}
|
|
611
658
|
state.config.threshold = val;
|
|
612
659
|
console.log(`${GREEN}Set uniform threshold to ${(val * 100).toFixed(0)}%${RESET}`);
|
|
613
660
|
} else if (arg === "--thresholds" && args[i + 1]) {
|
|
614
661
|
const parts = args[++i].split(",").map((s) => parseFloat(s.trim()));
|
|
615
|
-
if (parts.length !== 3 || parts.some(isNaN)) {
|
|
616
|
-
console.error(`${RED}--thresholds requires 3 comma-separated values (e.g., 90,80,70)${RESET}`);
|
|
662
|
+
if (parts.length !== 3 && parts.length !== 4 || parts.some(isNaN)) {
|
|
663
|
+
console.error(`${RED}--thresholds requires 3 or 4 comma-separated values (e.g., 90,80,70,70)${RESET}`);
|
|
617
664
|
process.exit(1);
|
|
618
665
|
}
|
|
619
666
|
const normalized = parts.map((v) => v > 1 ? v / 100 : v);
|
|
667
|
+
if (normalized.some((v) => v <= 0 || v > 1)) {
|
|
668
|
+
console.error(`${RED}Thresholds must each be greater than 0 and at most 100%${RESET}`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
const currentFableThreshold = getThresholds(state.config).weekly7dFable;
|
|
620
672
|
state.config.threshold = {
|
|
621
673
|
session5h: normalized[0],
|
|
622
674
|
weekly7d: normalized[1],
|
|
623
|
-
weekly7dSonnet: normalized[2]
|
|
675
|
+
weekly7dSonnet: normalized[2],
|
|
676
|
+
weekly7dFable: normalized[3] ?? currentFableThreshold
|
|
624
677
|
};
|
|
625
|
-
console.log(`${GREEN}Set per-metric thresholds: 5h=${(normalized[0] * 100).toFixed(0)}% 7d=${(normalized[1] * 100).toFixed(0)}% 7d-sonnet=${(normalized[2] * 100).toFixed(0)}%${RESET}`);
|
|
678
|
+
console.log(`${GREEN}Set per-metric thresholds: 5h=${(normalized[0] * 100).toFixed(0)}% 7d=${(normalized[1] * 100).toFixed(0)}% 7d-sonnet=${(normalized[2] * 100).toFixed(0)}% 7d-fable=${((normalized[3] ?? currentFableThreshold) * 100).toFixed(0)}%${RESET}`);
|
|
626
679
|
} else if (arg === "--interval" && args[i + 1]) {
|
|
627
680
|
const minutes = parseInt(args[++i], 10);
|
|
628
681
|
if (isNaN(minutes) || minutes < 1) {
|
|
@@ -654,10 +707,20 @@ ${BOLD}Available accounts:${RESET}`);
|
|
|
654
707
|
const tag = a.name === state2.currentAccount ? ` ${GREEN}[ACTIVE]${RESET}` : "";
|
|
655
708
|
console.log(` - ${a.name}${tag}`);
|
|
656
709
|
}
|
|
710
|
+
console.log(` - auto ${DIM}(resume automatic selection)${RESET}`);
|
|
657
711
|
console.error(`
|
|
658
|
-
${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
712
|
+
${RED}Usage: oc-auth-switcher switch <account-name|auto>${RESET}`);
|
|
659
713
|
process.exit(1);
|
|
660
714
|
}
|
|
715
|
+
if (name === "auto") {
|
|
716
|
+
const state2 = loadState();
|
|
717
|
+
state2.selectionMode = "auto";
|
|
718
|
+
state2.manualAccount = null;
|
|
719
|
+
state2.lastRotationCheck = Date.now();
|
|
720
|
+
saveState(state2);
|
|
721
|
+
console.log(`${GREEN}Automatic account selection resumed.${RESET}`);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
661
724
|
const account = data.accounts.find((a) => a.name === name);
|
|
662
725
|
if (!account) {
|
|
663
726
|
console.error(`${RED}Account "${name}" not found${RESET}`);
|
|
@@ -666,6 +729,8 @@ ${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
|
666
729
|
console.log(`${CYAN}Switching to account: ${name}...${RESET}`);
|
|
667
730
|
const state = loadState();
|
|
668
731
|
state.currentAccount = name;
|
|
732
|
+
state.selectionMode = "manual";
|
|
733
|
+
state.manualAccount = name;
|
|
669
734
|
state.lastRotationCheck = Date.now();
|
|
670
735
|
saveState(state);
|
|
671
736
|
console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
|
|
@@ -678,6 +743,7 @@ function cmdStatus() {
|
|
|
678
743
|
${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
679
744
|
`);
|
|
680
745
|
console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
|
|
746
|
+
console.log(` Selection mode: ${state.selectionMode}${state.manualAccount ? ` (${state.manualAccount})` : ""}`);
|
|
681
747
|
console.log(` Total accounts: ${data.accounts.length}`);
|
|
682
748
|
console.log(` Request count: ${state.requestCount}`);
|
|
683
749
|
console.log(` Last rotation: ${state.lastRotationCheck ? new Date(state.lastRotationCheck).toLocaleString() : "never"}`);
|
|
@@ -712,6 +778,10 @@ function cmdRemove(args) {
|
|
|
712
778
|
const state = loadState();
|
|
713
779
|
if (state.currentAccount === name) {
|
|
714
780
|
state.currentAccount = null;
|
|
781
|
+
if (state.manualAccount === name) {
|
|
782
|
+
state.selectionMode = "auto";
|
|
783
|
+
state.manualAccount = null;
|
|
784
|
+
}
|
|
715
785
|
saveState(state);
|
|
716
786
|
console.log(`${YELLOW}This was the active account. Rotation will pick a new one automatically.${RESET}`);
|
|
717
787
|
}
|
|
@@ -728,13 +798,13 @@ ${BOLD}COMMANDS:${RESET}
|
|
|
728
798
|
${CYAN}reauth${RESET} <name> Re-authenticate an existing account
|
|
729
799
|
${CYAN}usage${RESET} [--watch] Show usage dashboard with utilization metrics
|
|
730
800
|
${CYAN}config${RESET} [options] View or modify threshold/interval configuration
|
|
731
|
-
${CYAN}switch${RESET} <name>
|
|
801
|
+
${CYAN}switch${RESET} <name|auto> Pin a specific account or resume automatic selection
|
|
732
802
|
${CYAN}status${RESET} Show current active account and rotation state
|
|
733
803
|
${CYAN}remove${RESET} <name> Remove an account from the pool
|
|
734
804
|
|
|
735
805
|
${BOLD}CONFIG OPTIONS:${RESET}
|
|
736
806
|
--threshold <0-1> Set uniform threshold (e.g., 0.90)
|
|
737
|
-
--thresholds <a,b,c>
|
|
807
|
+
--thresholds <a,b,c[,d]> Set per-metric thresholds (5h,7d,7d-sonnet,7d-fable)
|
|
738
808
|
--interval <minutes> Set primary recovery check interval
|
|
739
809
|
--reset Reset to defaults
|
|
740
810
|
|
|
@@ -744,6 +814,7 @@ ${BOLD}EXAMPLES:${RESET}
|
|
|
744
814
|
oc-auth-switcher usage --watch
|
|
745
815
|
oc-auth-switcher config --threshold 0.90
|
|
746
816
|
oc-auth-switcher switch fallback-1
|
|
817
|
+
oc-auth-switcher switch auto
|
|
747
818
|
`);
|
|
748
819
|
}
|
|
749
820
|
async function main() {
|
package/dist/index.js
CHANGED
|
@@ -520,11 +520,15 @@ var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
|
|
|
520
520
|
var EMPTY_USAGE = {
|
|
521
521
|
session5h: { ...EMPTY_METRIC },
|
|
522
522
|
weekly7d: { ...EMPTY_METRIC },
|
|
523
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
523
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
524
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
525
|
+
rejected: { ...EMPTY_METRIC }
|
|
524
526
|
};
|
|
525
527
|
function defaultState() {
|
|
526
528
|
return {
|
|
527
529
|
currentAccount: null,
|
|
530
|
+
selectionMode: "auto",
|
|
531
|
+
manualAccount: null,
|
|
528
532
|
lastRotationCheck: 0,
|
|
529
533
|
requestCount: 0,
|
|
530
534
|
config: {
|
|
@@ -535,21 +539,43 @@ function defaultState() {
|
|
|
535
539
|
authFailures: {}
|
|
536
540
|
};
|
|
537
541
|
}
|
|
538
|
-
function
|
|
539
|
-
const raw = safeReadJSON(STATE_FILE, {});
|
|
542
|
+
function normalizeState(raw) {
|
|
540
543
|
const defaults = defaultState();
|
|
544
|
+
const threshold = raw.config?.threshold;
|
|
545
|
+
const migratedThreshold = typeof threshold === "object" && threshold !== null ? {
|
|
546
|
+
session5h: threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
547
|
+
weekly7d: threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
548
|
+
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
549
|
+
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
550
|
+
} : threshold ?? defaults.config.threshold;
|
|
551
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => [
|
|
552
|
+
name,
|
|
553
|
+
{
|
|
554
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
555
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
556
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
557
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
558
|
+
rejected: { ...EMPTY_METRIC, ...accountUsage?.rejected },
|
|
559
|
+
timestamp: accountUsage?.timestamp
|
|
560
|
+
}
|
|
561
|
+
]));
|
|
541
562
|
return {
|
|
542
563
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
564
|
+
selectionMode: raw.selectionMode === "manual" && raw.manualAccount ? "manual" : "auto",
|
|
565
|
+
manualAccount: raw.manualAccount ?? defaults.manualAccount,
|
|
543
566
|
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
544
567
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
545
568
|
config: {
|
|
546
|
-
threshold:
|
|
569
|
+
threshold: migratedThreshold,
|
|
547
570
|
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
548
571
|
},
|
|
549
|
-
usage
|
|
572
|
+
usage,
|
|
550
573
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
551
574
|
};
|
|
552
575
|
}
|
|
576
|
+
function loadState() {
|
|
577
|
+
return normalizeState(safeReadJSON(STATE_FILE, {}));
|
|
578
|
+
}
|
|
553
579
|
function saveState(state) {
|
|
554
580
|
safeWriteJSON(STATE_FILE, state);
|
|
555
581
|
}
|
|
@@ -558,16 +584,27 @@ function getThresholds(config) {
|
|
|
558
584
|
return {
|
|
559
585
|
session5h: config.threshold,
|
|
560
586
|
weekly7d: config.threshold,
|
|
561
|
-
weekly7dSonnet: config.threshold
|
|
587
|
+
weekly7dSonnet: config.threshold,
|
|
588
|
+
weekly7dFable: config.threshold
|
|
562
589
|
};
|
|
563
590
|
}
|
|
564
|
-
return
|
|
591
|
+
return {
|
|
592
|
+
session5h: config.threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
593
|
+
weekly7d: config.threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
594
|
+
weekly7dSonnet: config.threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
595
|
+
weekly7dFable: config.threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
596
|
+
};
|
|
565
597
|
}
|
|
566
598
|
function resolveStaleMetrics(state) {
|
|
567
599
|
const now = Date.now() / 1000;
|
|
568
600
|
for (const accountName of Object.keys(state.usage)) {
|
|
569
601
|
const usage = state.usage[accountName];
|
|
570
|
-
const metrics = [
|
|
602
|
+
const metrics = [
|
|
603
|
+
"session5h",
|
|
604
|
+
"weekly7d",
|
|
605
|
+
"weekly7dSonnet",
|
|
606
|
+
"weekly7dFable"
|
|
607
|
+
];
|
|
571
608
|
for (const key of metrics) {
|
|
572
609
|
const metric = usage[key];
|
|
573
610
|
if (metric.reset > 0 && metric.reset <= now) {
|
|
@@ -576,6 +613,9 @@ function resolveStaleMetrics(state) {
|
|
|
576
613
|
metric.status = "";
|
|
577
614
|
}
|
|
578
615
|
}
|
|
616
|
+
if (usage.rejected.reset > 0 && usage.rejected.reset <= now) {
|
|
617
|
+
usage.rejected = { ...EMPTY_METRIC };
|
|
618
|
+
}
|
|
579
619
|
}
|
|
580
620
|
}
|
|
581
621
|
function ensureAccountsInState(state, accountNames) {
|
|
@@ -584,7 +624,9 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
584
624
|
state.usage[name] = {
|
|
585
625
|
session5h: { ...EMPTY_METRIC },
|
|
586
626
|
weekly7d: { ...EMPTY_METRIC },
|
|
587
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
627
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
628
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
629
|
+
rejected: { ...EMPTY_METRIC }
|
|
588
630
|
};
|
|
589
631
|
}
|
|
590
632
|
}
|
|
@@ -594,7 +636,9 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
594
636
|
state.usage[accountName] = {
|
|
595
637
|
session5h: { ...EMPTY_METRIC },
|
|
596
638
|
weekly7d: { ...EMPTY_METRIC },
|
|
597
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
639
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
640
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
641
|
+
rejected: { ...EMPTY_METRIC }
|
|
598
642
|
};
|
|
599
643
|
}
|
|
600
644
|
const usage = state.usage[accountName];
|
|
@@ -611,6 +655,10 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
611
655
|
{
|
|
612
656
|
key: "weekly7dSonnet",
|
|
613
657
|
prefix: "anthropic-ratelimit-unified-7d_sonnet"
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
key: "weekly7dFable",
|
|
661
|
+
prefix: "anthropic-ratelimit-unified-7d_fable"
|
|
614
662
|
}
|
|
615
663
|
];
|
|
616
664
|
for (const { key, prefix } of metricFamilies) {
|
|
@@ -629,18 +677,47 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
629
677
|
const val = Number(resetHeader);
|
|
630
678
|
if (!isNaN(val)) {
|
|
631
679
|
usage[key].reset = val;
|
|
680
|
+
updated = true;
|
|
632
681
|
} else {
|
|
633
682
|
const parsed = Date.parse(resetHeader);
|
|
634
683
|
if (!isNaN(parsed)) {
|
|
635
684
|
usage[key].reset = parsed / 1000;
|
|
685
|
+
updated = true;
|
|
636
686
|
}
|
|
637
687
|
}
|
|
638
688
|
}
|
|
639
689
|
if (statusHeader !== null) {
|
|
640
690
|
usage[key].status = statusHeader;
|
|
691
|
+
updated = true;
|
|
641
692
|
}
|
|
642
693
|
}
|
|
643
694
|
}
|
|
695
|
+
for (const [headerName, headerValue] of headers.entries()) {
|
|
696
|
+
const normalizedName = headerName.toLowerCase();
|
|
697
|
+
if (!/^anthropic-ratelimit-unified-(?:.*-)?status$/.test(normalizedName) || headerValue.toLowerCase() !== "rejected") {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const prefix = normalizedName.slice(0, -"-status".length);
|
|
701
|
+
const resetHeader = headers.get(`${prefix}-reset`) ?? headers.get("anthropic-ratelimit-unified-reset");
|
|
702
|
+
let reset = resetHeader ? Number(resetHeader) : NaN;
|
|
703
|
+
if (isNaN(reset) && resetHeader) {
|
|
704
|
+
const parsed = Date.parse(resetHeader);
|
|
705
|
+
if (!isNaN(parsed))
|
|
706
|
+
reset = parsed / 1000;
|
|
707
|
+
}
|
|
708
|
+
if (!Number.isFinite(reset) || reset <= 0) {
|
|
709
|
+
const retryAfterHeader = headers.get("retry-after");
|
|
710
|
+
const retryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
|
|
711
|
+
reset = Date.now() / 1000 + (Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : state.config.checkInterval / 1000);
|
|
712
|
+
}
|
|
713
|
+
usage.rejected = {
|
|
714
|
+
utilization: 1,
|
|
715
|
+
reset: Math.max(usage.rejected.reset, reset),
|
|
716
|
+
status: "rejected",
|
|
717
|
+
prefix
|
|
718
|
+
};
|
|
719
|
+
updated = true;
|
|
720
|
+
}
|
|
644
721
|
if (updated) {
|
|
645
722
|
usage.timestamp = new Date().toISOString();
|
|
646
723
|
}
|
|
@@ -662,24 +739,36 @@ function isOverThreshold(usage, state) {
|
|
|
662
739
|
if (!usage)
|
|
663
740
|
return false;
|
|
664
741
|
const thresholds = getThresholds(state.config);
|
|
665
|
-
return usage.session5h.utilization
|
|
742
|
+
return usage.rejected.status?.toLowerCase() === "rejected" || thresholds.session5h > 0 && usage.session5h.utilization >= thresholds.session5h || thresholds.weekly7d > 0 && usage.weekly7d.utilization >= thresholds.weekly7d || thresholds.weekly7dSonnet > 0 && usage.weekly7dSonnet.utilization >= thresholds.weekly7dSonnet || thresholds.weekly7dFable > 0 && usage.weekly7dFable.utilization >= thresholds.weekly7dFable;
|
|
666
743
|
}
|
|
667
744
|
function getUtilizationScore(usage, state) {
|
|
668
745
|
if (!usage)
|
|
669
746
|
return 0;
|
|
670
747
|
const thresholds = getThresholds(state.config);
|
|
671
|
-
|
|
748
|
+
if (usage.rejected.status?.toLowerCase() === "rejected")
|
|
749
|
+
return Infinity;
|
|
750
|
+
const scores = [
|
|
751
|
+
{ util: usage.session5h.utilization, threshold: thresholds.session5h },
|
|
752
|
+
{ util: usage.weekly7d.utilization, threshold: thresholds.weekly7d },
|
|
753
|
+
{ util: usage.weekly7dSonnet.utilization, threshold: thresholds.weekly7dSonnet },
|
|
754
|
+
{ util: usage.weekly7dFable.utilization, threshold: thresholds.weekly7dFable }
|
|
755
|
+
].filter((metric) => metric.threshold > 0).map((metric) => metric.util / metric.threshold);
|
|
756
|
+
return scores.length > 0 ? Math.max(...scores) : 0;
|
|
672
757
|
}
|
|
673
758
|
function getExceededMetric(usage, state) {
|
|
674
759
|
if (!usage)
|
|
675
760
|
return null;
|
|
676
761
|
const thresholds = getThresholds(state.config);
|
|
762
|
+
if (usage.rejected.status?.toLowerCase() === "rejected") {
|
|
763
|
+
return usage.rejected.prefix ? `rejected rate limit (${usage.rejected.prefix})` : "rejected rate limit";
|
|
764
|
+
}
|
|
677
765
|
const metrics = [
|
|
678
766
|
{ name: "session5h", util: usage.session5h.utilization, thresh: thresholds.session5h },
|
|
679
767
|
{ name: "weekly7d", util: usage.weekly7d.utilization, thresh: thresholds.weekly7d },
|
|
680
|
-
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet }
|
|
768
|
+
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet },
|
|
769
|
+
{ name: "weekly7dFable", util: usage.weekly7dFable.utilization, thresh: thresholds.weekly7dFable }
|
|
681
770
|
];
|
|
682
|
-
const exceeded = metrics.filter((m) => m.
|
|
771
|
+
const exceeded = metrics.filter((m) => m.thresh > 0 && m.util >= m.thresh).sort((a, b) => b.util / b.thresh - a.util / a.thresh);
|
|
683
772
|
return exceeded.length > 0 ? exceeded[0].name : null;
|
|
684
773
|
}
|
|
685
774
|
function getEarliestReset(usage) {
|
|
@@ -688,7 +777,9 @@ function getEarliestReset(usage) {
|
|
|
688
777
|
const resets = [
|
|
689
778
|
usage.session5h.reset,
|
|
690
779
|
usage.weekly7d.reset,
|
|
691
|
-
usage.weekly7dSonnet.reset
|
|
780
|
+
usage.weekly7dSonnet.reset,
|
|
781
|
+
usage.weekly7dFable.reset,
|
|
782
|
+
usage.rejected.reset
|
|
692
783
|
].filter((r) => r > 0);
|
|
693
784
|
return resets.length > 0 ? Math.min(...resets) * 1000 : 0;
|
|
694
785
|
}
|
|
@@ -718,7 +809,7 @@ function findBestAvailable(candidates, state, exclude) {
|
|
|
718
809
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
719
810
|
continue;
|
|
720
811
|
const score = getUtilizationScore(state.usage[acct.name], state);
|
|
721
|
-
if (score < bestScore) {
|
|
812
|
+
if (!best || score < bestScore) {
|
|
722
813
|
bestScore = score;
|
|
723
814
|
best = acct;
|
|
724
815
|
}
|
|
@@ -730,6 +821,23 @@ function selectAccount(accounts, state) {
|
|
|
730
821
|
throw new Error("No accounts available");
|
|
731
822
|
}
|
|
732
823
|
purgeExpiredCooldowns(state);
|
|
824
|
+
if (state.selectionMode === "manual" && state.manualAccount) {
|
|
825
|
+
const manual = accounts.find((account) => account.name === state.manualAccount);
|
|
826
|
+
if (manual) {
|
|
827
|
+
const unavailable = isTemporarilyUnavailable(state, manual.name);
|
|
828
|
+
const exhausted = isOverThreshold(state.usage[manual.name], state);
|
|
829
|
+
if (!unavailable && !exhausted) {
|
|
830
|
+
return {
|
|
831
|
+
account: manual,
|
|
832
|
+
switched: state.currentAccount !== manual.name,
|
|
833
|
+
reason: state.currentAccount !== manual.name ? `Manual selection — switching to ${manual.name}` : undefined
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
state.currentAccount = manual.name;
|
|
837
|
+
}
|
|
838
|
+
state.selectionMode = "auto";
|
|
839
|
+
state.manualAccount = null;
|
|
840
|
+
}
|
|
733
841
|
if (accounts.length === 1) {
|
|
734
842
|
return { account: accounts[0], switched: false };
|
|
735
843
|
}
|
|
@@ -809,12 +917,35 @@ function selectAccount(accounts, state) {
|
|
|
809
917
|
}
|
|
810
918
|
function markAuthFailure(state, accountName) {
|
|
811
919
|
state.authFailures[accountName] = Date.now() + AUTH_FAILURE_COOLDOWN;
|
|
920
|
+
if (state.selectionMode === "manual" && state.manualAccount === accountName) {
|
|
921
|
+
state.selectionMode = "auto";
|
|
922
|
+
state.manualAccount = null;
|
|
923
|
+
}
|
|
812
924
|
}
|
|
813
925
|
function clearAuthFailure(state, accountName) {
|
|
814
926
|
delete state.authFailures[accountName];
|
|
815
927
|
}
|
|
816
928
|
|
|
817
929
|
// src/index.ts
|
|
930
|
+
function selectionSnapshot(state) {
|
|
931
|
+
return {
|
|
932
|
+
selectionMode: state.selectionMode,
|
|
933
|
+
manualAccount: state.manualAccount,
|
|
934
|
+
currentAccount: state.currentAccount,
|
|
935
|
+
lastRotationCheck: state.lastRotationCheck
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function saveRequestState(state, initiallyLoaded) {
|
|
939
|
+
const onDisk = loadState();
|
|
940
|
+
const selectionChangedSinceLoad = onDisk.selectionMode !== initiallyLoaded.selectionMode || onDisk.manualAccount !== initiallyLoaded.manualAccount || onDisk.currentAccount !== initiallyLoaded.currentAccount || onDisk.lastRotationCheck !== initiallyLoaded.lastRotationCheck;
|
|
941
|
+
if (selectionChangedSinceLoad) {
|
|
942
|
+
state.selectionMode = onDisk.selectionMode;
|
|
943
|
+
state.manualAccount = onDisk.manualAccount;
|
|
944
|
+
state.currentAccount = onDisk.currentAccount;
|
|
945
|
+
state.lastRotationCheck = onDisk.lastRotationCheck;
|
|
946
|
+
}
|
|
947
|
+
saveState(state);
|
|
948
|
+
}
|
|
818
949
|
var AuthSwitcherPlugin = async ({ client }) => {
|
|
819
950
|
return {
|
|
820
951
|
auth: {
|
|
@@ -836,6 +967,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
836
967
|
async fetch(input, init) {
|
|
837
968
|
const { accounts } = loadAccounts();
|
|
838
969
|
const state = loadState();
|
|
970
|
+
const initiallyLoadedSelection = selectionSnapshot(state);
|
|
839
971
|
if (accounts.length === 0) {
|
|
840
972
|
const auth2 = await getAuth();
|
|
841
973
|
if (auth2.type !== "oauth")
|
|
@@ -908,6 +1040,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
908
1040
|
markAuthFailure(state, account.name);
|
|
909
1041
|
const next = accounts.find((a) => !attemptedAccounts.has(a.name) && !state.authFailures[a.name]);
|
|
910
1042
|
if (!next) {
|
|
1043
|
+
saveRequestState(state, initiallyLoadedSelection);
|
|
911
1044
|
throw new Error(`[oc-auth-switcher] All accounts failed token refresh`);
|
|
912
1045
|
}
|
|
913
1046
|
account = next;
|
|
@@ -963,7 +1096,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
963
1096
|
});
|
|
964
1097
|
updateUsageFromHeaders(state, next.name, retryResponse.headers);
|
|
965
1098
|
clearAuthFailure(state, next.name);
|
|
966
|
-
|
|
1099
|
+
saveRequestState(state, initiallyLoadedSelection);
|
|
967
1100
|
return createStrippedStream(retryResponse);
|
|
968
1101
|
}
|
|
969
1102
|
}
|
|
@@ -971,7 +1104,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
971
1104
|
updateUsageFromHeaders(state, account.name, response.headers);
|
|
972
1105
|
clearAuthFailure(state, account.name);
|
|
973
1106
|
state.requestCount = (state.requestCount || 0) + 1;
|
|
974
|
-
|
|
1107
|
+
saveRequestState(state, initiallyLoadedSelection);
|
|
975
1108
|
return createStrippedStream(response);
|
|
976
1109
|
}
|
|
977
1110
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-auth-switcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "bun build src/index.ts --outdir dist --target node --format esm && bun build src/cli.ts --outdir dist --target bun --format esm",
|
|
12
12
|
"dev": "bun run build --watch",
|
|
13
|
+
"test": "bun test",
|
|
13
14
|
"prepublishOnly": "bun run build"
|
|
14
15
|
},
|
|
15
16
|
"peerDependencies": {
|