oc-auth-switcher 0.2.0 → 0.3.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 +80 -15
- package/dist/index.js +145 -15
- package/package.json +1 -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: {
|
|
@@ -170,15 +174,35 @@ function defaultState() {
|
|
|
170
174
|
function loadState() {
|
|
171
175
|
const raw = safeReadJSON(STATE_FILE, {});
|
|
172
176
|
const defaults = defaultState();
|
|
177
|
+
const threshold = raw.config?.threshold;
|
|
178
|
+
const migratedThreshold = typeof threshold === "object" && threshold !== null ? {
|
|
179
|
+
session5h: threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
180
|
+
weekly7d: threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
181
|
+
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
182
|
+
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
183
|
+
} : threshold ?? defaults.config.threshold;
|
|
184
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => [
|
|
185
|
+
name,
|
|
186
|
+
{
|
|
187
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
188
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
189
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
190
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
191
|
+
rejected: { ...EMPTY_METRIC, ...accountUsage?.rejected },
|
|
192
|
+
timestamp: accountUsage?.timestamp
|
|
193
|
+
}
|
|
194
|
+
]));
|
|
173
195
|
return {
|
|
174
196
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
197
|
+
selectionMode: raw.selectionMode === "manual" && raw.manualAccount ? "manual" : "auto",
|
|
198
|
+
manualAccount: raw.manualAccount ?? defaults.manualAccount,
|
|
175
199
|
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
176
200
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
177
201
|
config: {
|
|
178
|
-
threshold:
|
|
202
|
+
threshold: migratedThreshold,
|
|
179
203
|
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
180
204
|
},
|
|
181
|
-
usage
|
|
205
|
+
usage,
|
|
182
206
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
183
207
|
};
|
|
184
208
|
}
|
|
@@ -190,16 +214,27 @@ function getThresholds(config) {
|
|
|
190
214
|
return {
|
|
191
215
|
session5h: config.threshold,
|
|
192
216
|
weekly7d: config.threshold,
|
|
193
|
-
weekly7dSonnet: config.threshold
|
|
217
|
+
weekly7dSonnet: config.threshold,
|
|
218
|
+
weekly7dFable: config.threshold
|
|
194
219
|
};
|
|
195
220
|
}
|
|
196
|
-
return
|
|
221
|
+
return {
|
|
222
|
+
session5h: config.threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
223
|
+
weekly7d: config.threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
224
|
+
weekly7dSonnet: config.threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
225
|
+
weekly7dFable: config.threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
226
|
+
};
|
|
197
227
|
}
|
|
198
228
|
function resolveStaleMetrics(state) {
|
|
199
229
|
const now = Date.now() / 1000;
|
|
200
230
|
for (const accountName of Object.keys(state.usage)) {
|
|
201
231
|
const usage = state.usage[accountName];
|
|
202
|
-
const metrics = [
|
|
232
|
+
const metrics = [
|
|
233
|
+
"session5h",
|
|
234
|
+
"weekly7d",
|
|
235
|
+
"weekly7dSonnet",
|
|
236
|
+
"weekly7dFable"
|
|
237
|
+
];
|
|
203
238
|
for (const key of metrics) {
|
|
204
239
|
const metric = usage[key];
|
|
205
240
|
if (metric.reset > 0 && metric.reset <= now) {
|
|
@@ -208,6 +243,9 @@ function resolveStaleMetrics(state) {
|
|
|
208
243
|
metric.status = "";
|
|
209
244
|
}
|
|
210
245
|
}
|
|
246
|
+
if (usage.rejected.reset > 0 && usage.rejected.reset <= now) {
|
|
247
|
+
usage.rejected = { ...EMPTY_METRIC };
|
|
248
|
+
}
|
|
211
249
|
}
|
|
212
250
|
}
|
|
213
251
|
function ensureAccountsInState(state, accountNames) {
|
|
@@ -216,7 +254,9 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
216
254
|
state.usage[name] = {
|
|
217
255
|
session5h: { ...EMPTY_METRIC },
|
|
218
256
|
weekly7d: { ...EMPTY_METRIC },
|
|
219
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
257
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
258
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
259
|
+
rejected: { ...EMPTY_METRIC }
|
|
220
260
|
};
|
|
221
261
|
}
|
|
222
262
|
}
|
|
@@ -538,7 +578,7 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
538
578
|
`);
|
|
539
579
|
console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
|
|
540
580
|
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)}%`);
|
|
581
|
+
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
582
|
console.log();
|
|
543
583
|
if (data.accounts.length === 0) {
|
|
544
584
|
console.log(` ${DIM}No accounts configured. Run 'oc-auth-switcher add' to add one.${RESET}
|
|
@@ -555,6 +595,10 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
555
595
|
console.log(` 5h session: ${progressBar(usage.session5h.utilization, thresholds.session5h)}`);
|
|
556
596
|
console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}`);
|
|
557
597
|
console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}`);
|
|
598
|
+
console.log(` 7d fable: ${progressBar(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}`);
|
|
599
|
+
if (usage.rejected.status === "rejected") {
|
|
600
|
+
console.log(` ${RED}Rate limit status: REJECTED${RESET}`);
|
|
601
|
+
}
|
|
558
602
|
if (usage.timestamp) {
|
|
559
603
|
console.log(` ${DIM}Last updated: ${usage.timestamp}${RESET}`);
|
|
560
604
|
}
|
|
@@ -594,6 +638,7 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
594
638
|
console.log(` Threshold (5h): ${(thresholds.session5h * 100).toFixed(0)}%`);
|
|
595
639
|
console.log(` Threshold (7d): ${(thresholds.weekly7d * 100).toFixed(0)}%`);
|
|
596
640
|
console.log(` Threshold (7d sonnet): ${(thresholds.weekly7dSonnet * 100).toFixed(0)}%`);
|
|
641
|
+
console.log(` Threshold (7d fable): ${(thresholds.weekly7dFable * 100).toFixed(0)}%`);
|
|
597
642
|
console.log(` Check interval: ${state.config.checkInterval / 60000} min`);
|
|
598
643
|
console.log();
|
|
599
644
|
console.log(`${DIM} Config file: ${STATE_FILE}${RESET}`);
|
|
@@ -612,17 +657,19 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
612
657
|
console.log(`${GREEN}Set uniform threshold to ${(val * 100).toFixed(0)}%${RESET}`);
|
|
613
658
|
} else if (arg === "--thresholds" && args[i + 1]) {
|
|
614
659
|
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}`);
|
|
660
|
+
if (parts.length !== 3 && parts.length !== 4 || parts.some(isNaN)) {
|
|
661
|
+
console.error(`${RED}--thresholds requires 3 or 4 comma-separated values (e.g., 90,80,70,70)${RESET}`);
|
|
617
662
|
process.exit(1);
|
|
618
663
|
}
|
|
619
664
|
const normalized = parts.map((v) => v > 1 ? v / 100 : v);
|
|
665
|
+
const currentFableThreshold = getThresholds(state.config).weekly7dFable;
|
|
620
666
|
state.config.threshold = {
|
|
621
667
|
session5h: normalized[0],
|
|
622
668
|
weekly7d: normalized[1],
|
|
623
|
-
weekly7dSonnet: normalized[2]
|
|
669
|
+
weekly7dSonnet: normalized[2],
|
|
670
|
+
weekly7dFable: normalized[3] ?? currentFableThreshold
|
|
624
671
|
};
|
|
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}`);
|
|
672
|
+
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
673
|
} else if (arg === "--interval" && args[i + 1]) {
|
|
627
674
|
const minutes = parseInt(args[++i], 10);
|
|
628
675
|
if (isNaN(minutes) || minutes < 1) {
|
|
@@ -654,10 +701,20 @@ ${BOLD}Available accounts:${RESET}`);
|
|
|
654
701
|
const tag = a.name === state2.currentAccount ? ` ${GREEN}[ACTIVE]${RESET}` : "";
|
|
655
702
|
console.log(` - ${a.name}${tag}`);
|
|
656
703
|
}
|
|
704
|
+
console.log(` - auto ${DIM}(resume automatic selection)${RESET}`);
|
|
657
705
|
console.error(`
|
|
658
|
-
${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
706
|
+
${RED}Usage: oc-auth-switcher switch <account-name|auto>${RESET}`);
|
|
659
707
|
process.exit(1);
|
|
660
708
|
}
|
|
709
|
+
if (name === "auto") {
|
|
710
|
+
const state2 = loadState();
|
|
711
|
+
state2.selectionMode = "auto";
|
|
712
|
+
state2.manualAccount = null;
|
|
713
|
+
state2.lastRotationCheck = Date.now();
|
|
714
|
+
saveState(state2);
|
|
715
|
+
console.log(`${GREEN}Automatic account selection resumed.${RESET}`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
661
718
|
const account = data.accounts.find((a) => a.name === name);
|
|
662
719
|
if (!account) {
|
|
663
720
|
console.error(`${RED}Account "${name}" not found${RESET}`);
|
|
@@ -666,6 +723,8 @@ ${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
|
666
723
|
console.log(`${CYAN}Switching to account: ${name}...${RESET}`);
|
|
667
724
|
const state = loadState();
|
|
668
725
|
state.currentAccount = name;
|
|
726
|
+
state.selectionMode = "manual";
|
|
727
|
+
state.manualAccount = name;
|
|
669
728
|
state.lastRotationCheck = Date.now();
|
|
670
729
|
saveState(state);
|
|
671
730
|
console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
|
|
@@ -678,6 +737,7 @@ function cmdStatus() {
|
|
|
678
737
|
${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
679
738
|
`);
|
|
680
739
|
console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
|
|
740
|
+
console.log(` Selection mode: ${state.selectionMode}${state.manualAccount ? ` (${state.manualAccount})` : ""}`);
|
|
681
741
|
console.log(` Total accounts: ${data.accounts.length}`);
|
|
682
742
|
console.log(` Request count: ${state.requestCount}`);
|
|
683
743
|
console.log(` Last rotation: ${state.lastRotationCheck ? new Date(state.lastRotationCheck).toLocaleString() : "never"}`);
|
|
@@ -712,6 +772,10 @@ function cmdRemove(args) {
|
|
|
712
772
|
const state = loadState();
|
|
713
773
|
if (state.currentAccount === name) {
|
|
714
774
|
state.currentAccount = null;
|
|
775
|
+
if (state.manualAccount === name) {
|
|
776
|
+
state.selectionMode = "auto";
|
|
777
|
+
state.manualAccount = null;
|
|
778
|
+
}
|
|
715
779
|
saveState(state);
|
|
716
780
|
console.log(`${YELLOW}This was the active account. Rotation will pick a new one automatically.${RESET}`);
|
|
717
781
|
}
|
|
@@ -728,13 +792,13 @@ ${BOLD}COMMANDS:${RESET}
|
|
|
728
792
|
${CYAN}reauth${RESET} <name> Re-authenticate an existing account
|
|
729
793
|
${CYAN}usage${RESET} [--watch] Show usage dashboard with utilization metrics
|
|
730
794
|
${CYAN}config${RESET} [options] View or modify threshold/interval configuration
|
|
731
|
-
${CYAN}switch${RESET} <name>
|
|
795
|
+
${CYAN}switch${RESET} <name|auto> Pin a specific account or resume automatic selection
|
|
732
796
|
${CYAN}status${RESET} Show current active account and rotation state
|
|
733
797
|
${CYAN}remove${RESET} <name> Remove an account from the pool
|
|
734
798
|
|
|
735
799
|
${BOLD}CONFIG OPTIONS:${RESET}
|
|
736
800
|
--threshold <0-1> Set uniform threshold (e.g., 0.90)
|
|
737
|
-
--thresholds <a,b,c>
|
|
801
|
+
--thresholds <a,b,c[,d]> Set per-metric thresholds (5h,7d,7d-sonnet,7d-fable)
|
|
738
802
|
--interval <minutes> Set primary recovery check interval
|
|
739
803
|
--reset Reset to defaults
|
|
740
804
|
|
|
@@ -744,6 +808,7 @@ ${BOLD}EXAMPLES:${RESET}
|
|
|
744
808
|
oc-auth-switcher usage --watch
|
|
745
809
|
oc-auth-switcher config --threshold 0.90
|
|
746
810
|
oc-auth-switcher switch fallback-1
|
|
811
|
+
oc-auth-switcher switch auto
|
|
747
812
|
`);
|
|
748
813
|
}
|
|
749
814
|
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: {
|
|
@@ -538,15 +542,35 @@ function defaultState() {
|
|
|
538
542
|
function loadState() {
|
|
539
543
|
const raw = safeReadJSON(STATE_FILE, {});
|
|
540
544
|
const defaults = defaultState();
|
|
545
|
+
const threshold = raw.config?.threshold;
|
|
546
|
+
const migratedThreshold = typeof threshold === "object" && threshold !== null ? {
|
|
547
|
+
session5h: threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
548
|
+
weekly7d: threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
549
|
+
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
550
|
+
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
551
|
+
} : threshold ?? defaults.config.threshold;
|
|
552
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => [
|
|
553
|
+
name,
|
|
554
|
+
{
|
|
555
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
556
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
557
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
558
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
559
|
+
rejected: { ...EMPTY_METRIC, ...accountUsage?.rejected },
|
|
560
|
+
timestamp: accountUsage?.timestamp
|
|
561
|
+
}
|
|
562
|
+
]));
|
|
541
563
|
return {
|
|
542
564
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
565
|
+
selectionMode: raw.selectionMode === "manual" && raw.manualAccount ? "manual" : "auto",
|
|
566
|
+
manualAccount: raw.manualAccount ?? defaults.manualAccount,
|
|
543
567
|
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
544
568
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
545
569
|
config: {
|
|
546
|
-
threshold:
|
|
570
|
+
threshold: migratedThreshold,
|
|
547
571
|
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
548
572
|
},
|
|
549
|
-
usage
|
|
573
|
+
usage,
|
|
550
574
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
551
575
|
};
|
|
552
576
|
}
|
|
@@ -558,16 +582,27 @@ function getThresholds(config) {
|
|
|
558
582
|
return {
|
|
559
583
|
session5h: config.threshold,
|
|
560
584
|
weekly7d: config.threshold,
|
|
561
|
-
weekly7dSonnet: config.threshold
|
|
585
|
+
weekly7dSonnet: config.threshold,
|
|
586
|
+
weekly7dFable: config.threshold
|
|
562
587
|
};
|
|
563
588
|
}
|
|
564
|
-
return
|
|
589
|
+
return {
|
|
590
|
+
session5h: config.threshold.session5h ?? DEFAULT_THRESHOLD,
|
|
591
|
+
weekly7d: config.threshold.weekly7d ?? DEFAULT_THRESHOLD,
|
|
592
|
+
weekly7dSonnet: config.threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
593
|
+
weekly7dFable: config.threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
594
|
+
};
|
|
565
595
|
}
|
|
566
596
|
function resolveStaleMetrics(state) {
|
|
567
597
|
const now = Date.now() / 1000;
|
|
568
598
|
for (const accountName of Object.keys(state.usage)) {
|
|
569
599
|
const usage = state.usage[accountName];
|
|
570
|
-
const metrics = [
|
|
600
|
+
const metrics = [
|
|
601
|
+
"session5h",
|
|
602
|
+
"weekly7d",
|
|
603
|
+
"weekly7dSonnet",
|
|
604
|
+
"weekly7dFable"
|
|
605
|
+
];
|
|
571
606
|
for (const key of metrics) {
|
|
572
607
|
const metric = usage[key];
|
|
573
608
|
if (metric.reset > 0 && metric.reset <= now) {
|
|
@@ -576,6 +611,9 @@ function resolveStaleMetrics(state) {
|
|
|
576
611
|
metric.status = "";
|
|
577
612
|
}
|
|
578
613
|
}
|
|
614
|
+
if (usage.rejected.reset > 0 && usage.rejected.reset <= now) {
|
|
615
|
+
usage.rejected = { ...EMPTY_METRIC };
|
|
616
|
+
}
|
|
579
617
|
}
|
|
580
618
|
}
|
|
581
619
|
function ensureAccountsInState(state, accountNames) {
|
|
@@ -584,7 +622,9 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
584
622
|
state.usage[name] = {
|
|
585
623
|
session5h: { ...EMPTY_METRIC },
|
|
586
624
|
weekly7d: { ...EMPTY_METRIC },
|
|
587
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
625
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
626
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
627
|
+
rejected: { ...EMPTY_METRIC }
|
|
588
628
|
};
|
|
589
629
|
}
|
|
590
630
|
}
|
|
@@ -594,7 +634,9 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
594
634
|
state.usage[accountName] = {
|
|
595
635
|
session5h: { ...EMPTY_METRIC },
|
|
596
636
|
weekly7d: { ...EMPTY_METRIC },
|
|
597
|
-
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
637
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
638
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
639
|
+
rejected: { ...EMPTY_METRIC }
|
|
598
640
|
};
|
|
599
641
|
}
|
|
600
642
|
const usage = state.usage[accountName];
|
|
@@ -611,6 +653,10 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
611
653
|
{
|
|
612
654
|
key: "weekly7dSonnet",
|
|
613
655
|
prefix: "anthropic-ratelimit-unified-7d_sonnet"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
key: "weekly7dFable",
|
|
659
|
+
prefix: "anthropic-ratelimit-unified-7d_fable"
|
|
614
660
|
}
|
|
615
661
|
];
|
|
616
662
|
for (const { key, prefix } of metricFamilies) {
|
|
@@ -629,18 +675,46 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
629
675
|
const val = Number(resetHeader);
|
|
630
676
|
if (!isNaN(val)) {
|
|
631
677
|
usage[key].reset = val;
|
|
678
|
+
updated = true;
|
|
632
679
|
} else {
|
|
633
680
|
const parsed = Date.parse(resetHeader);
|
|
634
681
|
if (!isNaN(parsed)) {
|
|
635
682
|
usage[key].reset = parsed / 1000;
|
|
683
|
+
updated = true;
|
|
636
684
|
}
|
|
637
685
|
}
|
|
638
686
|
}
|
|
639
687
|
if (statusHeader !== null) {
|
|
640
688
|
usage[key].status = statusHeader;
|
|
689
|
+
updated = true;
|
|
641
690
|
}
|
|
642
691
|
}
|
|
643
692
|
}
|
|
693
|
+
for (const [headerName, headerValue] of headers.entries()) {
|
|
694
|
+
const normalizedName = headerName.toLowerCase();
|
|
695
|
+
if (!/^anthropic-ratelimit-unified-(?:.*-)?status$/.test(normalizedName) || headerValue.toLowerCase() !== "rejected") {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const prefix = normalizedName.slice(0, -"-status".length);
|
|
699
|
+
const resetHeader = headers.get(`${prefix}-reset`) ?? headers.get("anthropic-ratelimit-unified-reset");
|
|
700
|
+
let reset = resetHeader ? Number(resetHeader) : NaN;
|
|
701
|
+
if (isNaN(reset) && resetHeader) {
|
|
702
|
+
const parsed = Date.parse(resetHeader);
|
|
703
|
+
if (!isNaN(parsed))
|
|
704
|
+
reset = parsed / 1000;
|
|
705
|
+
}
|
|
706
|
+
if (!Number.isFinite(reset) || reset <= 0) {
|
|
707
|
+
const retryAfterHeader = headers.get("retry-after");
|
|
708
|
+
const retryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
|
|
709
|
+
reset = Date.now() / 1000 + (Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : state.config.checkInterval / 1000);
|
|
710
|
+
}
|
|
711
|
+
usage.rejected = {
|
|
712
|
+
utilization: 1,
|
|
713
|
+
reset: Math.max(usage.rejected.reset, reset),
|
|
714
|
+
status: "rejected"
|
|
715
|
+
};
|
|
716
|
+
updated = true;
|
|
717
|
+
}
|
|
644
718
|
if (updated) {
|
|
645
719
|
usage.timestamp = new Date().toISOString();
|
|
646
720
|
}
|
|
@@ -662,22 +736,28 @@ function isOverThreshold(usage, state) {
|
|
|
662
736
|
if (!usage)
|
|
663
737
|
return false;
|
|
664
738
|
const thresholds = getThresholds(state.config);
|
|
665
|
-
return usage.session5h.utilization > thresholds.session5h || usage.weekly7d.utilization > thresholds.weekly7d || usage.weekly7dSonnet.utilization > thresholds.weekly7dSonnet;
|
|
739
|
+
return usage.rejected.status?.toLowerCase() === "rejected" || usage.session5h.utilization > thresholds.session5h || usage.weekly7d.utilization > thresholds.weekly7d || usage.weekly7dSonnet.utilization > thresholds.weekly7dSonnet || usage.weekly7dFable.utilization > thresholds.weekly7dFable;
|
|
666
740
|
}
|
|
667
741
|
function getUtilizationScore(usage, state) {
|
|
668
742
|
if (!usage)
|
|
669
743
|
return 0;
|
|
670
744
|
const thresholds = getThresholds(state.config);
|
|
671
|
-
|
|
745
|
+
if (usage.rejected.status?.toLowerCase() === "rejected")
|
|
746
|
+
return Infinity;
|
|
747
|
+
return Math.max(usage.session5h.utilization / thresholds.session5h, usage.weekly7d.utilization / thresholds.weekly7d, usage.weekly7dSonnet.utilization / thresholds.weekly7dSonnet, usage.weekly7dFable.utilization / thresholds.weekly7dFable);
|
|
672
748
|
}
|
|
673
749
|
function getExceededMetric(usage, state) {
|
|
674
750
|
if (!usage)
|
|
675
751
|
return null;
|
|
676
752
|
const thresholds = getThresholds(state.config);
|
|
753
|
+
if (usage.rejected.status?.toLowerCase() === "rejected") {
|
|
754
|
+
return "rejected rate limit";
|
|
755
|
+
}
|
|
677
756
|
const metrics = [
|
|
678
757
|
{ name: "session5h", util: usage.session5h.utilization, thresh: thresholds.session5h },
|
|
679
758
|
{ name: "weekly7d", util: usage.weekly7d.utilization, thresh: thresholds.weekly7d },
|
|
680
|
-
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet }
|
|
759
|
+
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet },
|
|
760
|
+
{ name: "weekly7dFable", util: usage.weekly7dFable.utilization, thresh: thresholds.weekly7dFable }
|
|
681
761
|
];
|
|
682
762
|
const exceeded = metrics.filter((m) => m.util > m.thresh).sort((a, b) => b.util / b.thresh - a.util / a.thresh);
|
|
683
763
|
return exceeded.length > 0 ? exceeded[0].name : null;
|
|
@@ -688,7 +768,9 @@ function getEarliestReset(usage) {
|
|
|
688
768
|
const resets = [
|
|
689
769
|
usage.session5h.reset,
|
|
690
770
|
usage.weekly7d.reset,
|
|
691
|
-
usage.weekly7dSonnet.reset
|
|
771
|
+
usage.weekly7dSonnet.reset,
|
|
772
|
+
usage.weekly7dFable.reset,
|
|
773
|
+
usage.rejected.reset
|
|
692
774
|
].filter((r) => r > 0);
|
|
693
775
|
return resets.length > 0 ? Math.min(...resets) * 1000 : 0;
|
|
694
776
|
}
|
|
@@ -730,6 +812,23 @@ function selectAccount(accounts, state) {
|
|
|
730
812
|
throw new Error("No accounts available");
|
|
731
813
|
}
|
|
732
814
|
purgeExpiredCooldowns(state);
|
|
815
|
+
if (state.selectionMode === "manual" && state.manualAccount) {
|
|
816
|
+
const manual = accounts.find((account) => account.name === state.manualAccount);
|
|
817
|
+
if (manual) {
|
|
818
|
+
const unavailable = isTemporarilyUnavailable(state, manual.name);
|
|
819
|
+
const exhausted = isOverThreshold(state.usage[manual.name], state);
|
|
820
|
+
if (!unavailable && !exhausted) {
|
|
821
|
+
return {
|
|
822
|
+
account: manual,
|
|
823
|
+
switched: state.currentAccount !== manual.name,
|
|
824
|
+
reason: state.currentAccount !== manual.name ? `Manual selection — switching to ${manual.name}` : undefined
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
state.currentAccount = manual.name;
|
|
828
|
+
}
|
|
829
|
+
state.selectionMode = "auto";
|
|
830
|
+
state.manualAccount = null;
|
|
831
|
+
}
|
|
733
832
|
if (accounts.length === 1) {
|
|
734
833
|
return { account: accounts[0], switched: false };
|
|
735
834
|
}
|
|
@@ -809,12 +908,35 @@ function selectAccount(accounts, state) {
|
|
|
809
908
|
}
|
|
810
909
|
function markAuthFailure(state, accountName) {
|
|
811
910
|
state.authFailures[accountName] = Date.now() + AUTH_FAILURE_COOLDOWN;
|
|
911
|
+
if (state.selectionMode === "manual" && state.manualAccount === accountName) {
|
|
912
|
+
state.selectionMode = "auto";
|
|
913
|
+
state.manualAccount = null;
|
|
914
|
+
}
|
|
812
915
|
}
|
|
813
916
|
function clearAuthFailure(state, accountName) {
|
|
814
917
|
delete state.authFailures[accountName];
|
|
815
918
|
}
|
|
816
919
|
|
|
817
920
|
// src/index.ts
|
|
921
|
+
function selectionSnapshot(state) {
|
|
922
|
+
return {
|
|
923
|
+
selectionMode: state.selectionMode,
|
|
924
|
+
manualAccount: state.manualAccount,
|
|
925
|
+
currentAccount: state.currentAccount,
|
|
926
|
+
lastRotationCheck: state.lastRotationCheck
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function saveRequestState(state, initiallyLoaded) {
|
|
930
|
+
const onDisk = loadState();
|
|
931
|
+
const selectionChangedSinceLoad = onDisk.selectionMode !== initiallyLoaded.selectionMode || onDisk.manualAccount !== initiallyLoaded.manualAccount || onDisk.currentAccount !== initiallyLoaded.currentAccount || onDisk.lastRotationCheck !== initiallyLoaded.lastRotationCheck;
|
|
932
|
+
if (selectionChangedSinceLoad) {
|
|
933
|
+
state.selectionMode = onDisk.selectionMode;
|
|
934
|
+
state.manualAccount = onDisk.manualAccount;
|
|
935
|
+
state.currentAccount = onDisk.currentAccount;
|
|
936
|
+
state.lastRotationCheck = onDisk.lastRotationCheck;
|
|
937
|
+
}
|
|
938
|
+
saveState(state);
|
|
939
|
+
}
|
|
818
940
|
var AuthSwitcherPlugin = async ({ client }) => {
|
|
819
941
|
return {
|
|
820
942
|
auth: {
|
|
@@ -836,6 +958,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
836
958
|
async fetch(input, init) {
|
|
837
959
|
const { accounts } = loadAccounts();
|
|
838
960
|
const state = loadState();
|
|
961
|
+
const initiallyLoadedSelection = selectionSnapshot(state);
|
|
839
962
|
if (accounts.length === 0) {
|
|
840
963
|
const auth2 = await getAuth();
|
|
841
964
|
if (auth2.type !== "oauth")
|
|
@@ -885,7 +1008,13 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
885
1008
|
const selection = selectAccount(accounts, state);
|
|
886
1009
|
let account = selection.account;
|
|
887
1010
|
if (selection.switched) {
|
|
888
|
-
|
|
1011
|
+
await client.app.log({
|
|
1012
|
+
body: {
|
|
1013
|
+
service: "oc-auth-switcher",
|
|
1014
|
+
level: "info",
|
|
1015
|
+
message: selection.reason ?? "switched account"
|
|
1016
|
+
}
|
|
1017
|
+
}).catch(() => {});
|
|
889
1018
|
}
|
|
890
1019
|
state.currentAccount = account.name;
|
|
891
1020
|
const attemptedAccounts = new Set;
|
|
@@ -902,6 +1031,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
902
1031
|
markAuthFailure(state, account.name);
|
|
903
1032
|
const next = accounts.find((a) => !attemptedAccounts.has(a.name) && !state.authFailures[a.name]);
|
|
904
1033
|
if (!next) {
|
|
1034
|
+
saveRequestState(state, initiallyLoadedSelection);
|
|
905
1035
|
throw new Error(`[oc-auth-switcher] All accounts failed token refresh`);
|
|
906
1036
|
}
|
|
907
1037
|
account = next;
|
|
@@ -957,7 +1087,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
957
1087
|
});
|
|
958
1088
|
updateUsageFromHeaders(state, next.name, retryResponse.headers);
|
|
959
1089
|
clearAuthFailure(state, next.name);
|
|
960
|
-
|
|
1090
|
+
saveRequestState(state, initiallyLoadedSelection);
|
|
961
1091
|
return createStrippedStream(retryResponse);
|
|
962
1092
|
}
|
|
963
1093
|
}
|
|
@@ -965,7 +1095,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
965
1095
|
updateUsageFromHeaders(state, account.name, response.headers);
|
|
966
1096
|
clearAuthFailure(state, account.name);
|
|
967
1097
|
state.requestCount = (state.requestCount || 0) + 1;
|
|
968
|
-
|
|
1098
|
+
saveRequestState(state, initiallyLoadedSelection);
|
|
969
1099
|
return createStrippedStream(response);
|
|
970
1100
|
}
|
|
971
1101
|
};
|