oc-auth-switcher 0.5.0 → 0.6.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/README.md +16 -20
- package/dist/cli.js +10 -49
- package/dist/index.js +25 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,19 +25,17 @@ This is the **only** auth plugin you need for Anthropic. Do not also list `@ex-m
|
|
|
25
25
|
### 2. Add accounts
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
# Add
|
|
29
|
-
oc-auth-switcher add
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
oc-auth-switcher add fallback-1
|
|
33
|
-
oc-auth-switcher add fallback-2
|
|
28
|
+
# Add accounts in any order
|
|
29
|
+
oc-auth-switcher add work
|
|
30
|
+
oc-auth-switcher add personal
|
|
31
|
+
oc-auth-switcher add team
|
|
34
32
|
```
|
|
35
33
|
|
|
36
34
|
Each `add` command runs an OAuth flow — you'll be given a URL to open in your browser and prompted to paste the callback.
|
|
37
35
|
|
|
38
36
|
### 3. Use OpenCode normally
|
|
39
37
|
|
|
40
|
-
Metrics update automatically on every API request.
|
|
38
|
+
Metrics update automatically on every API request. The active account remains selected until it reaches a relevant utilization threshold or becomes unavailable, then the plugin switches to the available account with the most headroom.
|
|
41
39
|
|
|
42
40
|
## CLI Commands
|
|
43
41
|
|
|
@@ -50,8 +48,8 @@ oc-auth-switcher <command> [options]
|
|
|
50
48
|
| `add [name]` | Add a new account via OAuth |
|
|
51
49
|
| `reauth <name>` | Re-authenticate an existing account |
|
|
52
50
|
| `usage [--watch]` | Show utilization dashboard with progress bars |
|
|
53
|
-
| `config [options]` | View/modify thresholds
|
|
54
|
-
| `switch <name>` |
|
|
51
|
+
| `config [options]` | View/modify thresholds |
|
|
52
|
+
| `switch <name>` | Set the active account |
|
|
55
53
|
| `status` | Show current active account and rotation state |
|
|
56
54
|
| `remove <name>` | Remove an account from the pool |
|
|
57
55
|
|
|
@@ -61,11 +59,8 @@ oc-auth-switcher <command> [options]
|
|
|
61
59
|
# Set uniform threshold (default: 95%)
|
|
62
60
|
oc-auth-switcher config --threshold 0.95
|
|
63
61
|
|
|
64
|
-
# Set per-metric thresholds (5h, 7d, 7d-sonnet)
|
|
65
|
-
oc-auth-switcher config --thresholds 90,80,70
|
|
66
|
-
|
|
67
|
-
# Set primary recovery check interval (default: 60 min)
|
|
68
|
-
oc-auth-switcher config --interval 30
|
|
62
|
+
# Set per-metric thresholds (5h, 7d, 7d-sonnet, 7d-fable)
|
|
63
|
+
oc-auth-switcher config --thresholds 90,80,70,70
|
|
69
64
|
|
|
70
65
|
# Reset to defaults
|
|
71
66
|
oc-auth-switcher config --reset
|
|
@@ -82,12 +77,13 @@ Both files use atomic writes with `.bak` fallback for crash safety.
|
|
|
82
77
|
|
|
83
78
|
## Rotation Algorithm
|
|
84
79
|
|
|
85
|
-
-
|
|
86
|
-
- When
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
80
|
+
- Keep the active account while its model-relevant utilization remains below threshold
|
|
81
|
+
- When rotation is required, select the available account with the lowest maximum threshold-normalized utilization
|
|
82
|
+
- Break exact utilization ties by account array order
|
|
83
|
+
- If every account is exhausted, select the least-loaded account rather than failing
|
|
84
|
+
- Recovered accounts do not preempt a healthy active account
|
|
85
|
+
- Auth failures trigger a 10-minute cooldown per account
|
|
86
|
+
- Token refresh is handled automatically with retry and failover to other accounts
|
|
91
87
|
|
|
92
88
|
## Running the CLI
|
|
93
89
|
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,8 @@ var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(),
|
|
|
12
12
|
var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
|
|
13
13
|
var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
14
14
|
var DEFAULT_THRESHOLD = 0.95;
|
|
15
|
-
var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
16
15
|
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
16
|
+
var REJECTION_FALLBACK_SECONDS = 60 * 60;
|
|
17
17
|
|
|
18
18
|
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
19
19
|
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
@@ -159,13 +159,9 @@ var EMPTY_USAGE = {
|
|
|
159
159
|
function defaultState() {
|
|
160
160
|
return {
|
|
161
161
|
currentAccount: null,
|
|
162
|
-
selectionMode: "auto",
|
|
163
|
-
manualAccount: null,
|
|
164
|
-
lastRotationCheck: 0,
|
|
165
162
|
requestCount: 0,
|
|
166
163
|
config: {
|
|
167
|
-
threshold: DEFAULT_THRESHOLD
|
|
168
|
-
checkInterval: DEFAULT_CHECK_INTERVAL
|
|
164
|
+
threshold: DEFAULT_THRESHOLD
|
|
169
165
|
},
|
|
170
166
|
usage: {},
|
|
171
167
|
authFailures: {}
|
|
@@ -193,13 +189,9 @@ function normalizeState(raw) {
|
|
|
193
189
|
]));
|
|
194
190
|
return {
|
|
195
191
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
196
|
-
selectionMode: raw.selectionMode === "manual" && raw.manualAccount ? "manual" : "auto",
|
|
197
|
-
manualAccount: raw.manualAccount ?? defaults.manualAccount,
|
|
198
|
-
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
199
192
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
200
193
|
config: {
|
|
201
|
-
threshold: migratedThreshold
|
|
202
|
-
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
194
|
+
threshold: migratedThreshold
|
|
203
195
|
},
|
|
204
196
|
usage,
|
|
205
197
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
@@ -641,7 +633,6 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
641
633
|
console.log(` Threshold (7d): ${(thresholds.weekly7d * 100).toFixed(0)}%`);
|
|
642
634
|
console.log(` Threshold (7d sonnet): ${(thresholds.weekly7dSonnet * 100).toFixed(0)}%`);
|
|
643
635
|
console.log(` Threshold (7d fable): ${(thresholds.weekly7dFable * 100).toFixed(0)}%`);
|
|
644
|
-
console.log(` Check interval: ${state.config.checkInterval / 60000} min`);
|
|
645
636
|
console.log();
|
|
646
637
|
console.log(`${DIM} Config file: ${STATE_FILE}${RESET}`);
|
|
647
638
|
console.log();
|
|
@@ -676,18 +667,9 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
676
667
|
weekly7dFable: normalized[3] ?? currentFableThreshold
|
|
677
668
|
};
|
|
678
669
|
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}`);
|
|
679
|
-
} else if (arg === "--interval" && args[i + 1]) {
|
|
680
|
-
const minutes = parseInt(args[++i], 10);
|
|
681
|
-
if (isNaN(minutes) || minutes < 1) {
|
|
682
|
-
console.error(`${RED}Interval must be a positive number of minutes${RESET}`);
|
|
683
|
-
process.exit(1);
|
|
684
|
-
}
|
|
685
|
-
state.config.checkInterval = minutes * 60 * 1000;
|
|
686
|
-
console.log(`${GREEN}Set check interval to ${minutes} minutes${RESET}`);
|
|
687
670
|
} else if (arg === "--reset") {
|
|
688
671
|
state.config.threshold = DEFAULT_THRESHOLD;
|
|
689
|
-
|
|
690
|
-
console.log(`${GREEN}Reset to defaults: threshold=${(DEFAULT_THRESHOLD * 100).toFixed(0)}% interval=${DEFAULT_CHECK_INTERVAL / 60000}min${RESET}`);
|
|
672
|
+
console.log(`${GREEN}Reset to default threshold: ${(DEFAULT_THRESHOLD * 100).toFixed(0)}%${RESET}`);
|
|
691
673
|
}
|
|
692
674
|
}
|
|
693
675
|
saveState(state);
|
|
@@ -707,20 +689,10 @@ ${BOLD}Available accounts:${RESET}`);
|
|
|
707
689
|
const tag = a.name === state2.currentAccount ? ` ${GREEN}[ACTIVE]${RESET}` : "";
|
|
708
690
|
console.log(` - ${a.name}${tag}`);
|
|
709
691
|
}
|
|
710
|
-
console.log(` - auto ${DIM}(resume automatic selection)${RESET}`);
|
|
711
692
|
console.error(`
|
|
712
|
-
${RED}Usage: oc-auth-switcher switch <account-name
|
|
693
|
+
${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
|
|
713
694
|
process.exit(1);
|
|
714
695
|
}
|
|
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
|
-
}
|
|
724
696
|
const account = data.accounts.find((a) => a.name === name);
|
|
725
697
|
if (!account) {
|
|
726
698
|
console.error(`${RED}Account "${name}" not found${RESET}`);
|
|
@@ -729,9 +701,6 @@ ${RED}Usage: oc-auth-switcher switch <account-name|auto>${RESET}`);
|
|
|
729
701
|
console.log(`${CYAN}Switching to account: ${name}...${RESET}`);
|
|
730
702
|
const state = loadState();
|
|
731
703
|
state.currentAccount = name;
|
|
732
|
-
state.selectionMode = "manual";
|
|
733
|
-
state.manualAccount = name;
|
|
734
|
-
state.lastRotationCheck = Date.now();
|
|
735
704
|
saveState(state);
|
|
736
705
|
console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
|
|
737
706
|
}
|
|
@@ -743,10 +712,8 @@ function cmdStatus() {
|
|
|
743
712
|
${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
744
713
|
`);
|
|
745
714
|
console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
|
|
746
|
-
console.log(` Selection mode: ${state.selectionMode}${state.manualAccount ? ` (${state.manualAccount})` : ""}`);
|
|
747
715
|
console.log(` Total accounts: ${data.accounts.length}`);
|
|
748
716
|
console.log(` Request count: ${state.requestCount}`);
|
|
749
|
-
console.log(` Last rotation: ${state.lastRotationCheck ? new Date(state.lastRotationCheck).toLocaleString() : "never"}`);
|
|
750
717
|
console.log();
|
|
751
718
|
const failures = Object.entries(state.authFailures).filter(([_, until]) => until > Date.now());
|
|
752
719
|
if (failures.length > 0) {
|
|
@@ -778,10 +745,6 @@ function cmdRemove(args) {
|
|
|
778
745
|
const state = loadState();
|
|
779
746
|
if (state.currentAccount === name) {
|
|
780
747
|
state.currentAccount = null;
|
|
781
|
-
if (state.manualAccount === name) {
|
|
782
|
-
state.selectionMode = "auto";
|
|
783
|
-
state.manualAccount = null;
|
|
784
|
-
}
|
|
785
748
|
saveState(state);
|
|
786
749
|
console.log(`${YELLOW}This was the active account. Rotation will pick a new one automatically.${RESET}`);
|
|
787
750
|
}
|
|
@@ -797,24 +760,22 @@ ${BOLD}COMMANDS:${RESET}
|
|
|
797
760
|
${CYAN}add${RESET} [name] Add a new Anthropic account via OAuth
|
|
798
761
|
${CYAN}reauth${RESET} <name> Re-authenticate an existing account
|
|
799
762
|
${CYAN}usage${RESET} [--watch] Show usage dashboard with utilization metrics
|
|
800
|
-
${CYAN}config${RESET} [options] View or modify threshold
|
|
801
|
-
${CYAN}switch${RESET} <name
|
|
763
|
+
${CYAN}config${RESET} [options] View or modify threshold configuration
|
|
764
|
+
${CYAN}switch${RESET} <name> Set the active account
|
|
802
765
|
${CYAN}status${RESET} Show current active account and rotation state
|
|
803
766
|
${CYAN}remove${RESET} <name> Remove an account from the pool
|
|
804
767
|
|
|
805
768
|
${BOLD}CONFIG OPTIONS:${RESET}
|
|
806
769
|
--threshold <0-1> Set uniform threshold (e.g., 0.90)
|
|
807
770
|
--thresholds <a,b,c[,d]> Set per-metric thresholds (5h,7d,7d-sonnet,7d-fable)
|
|
808
|
-
--interval <minutes> Set primary recovery check interval
|
|
809
771
|
--reset Reset to defaults
|
|
810
772
|
|
|
811
773
|
${BOLD}EXAMPLES:${RESET}
|
|
812
|
-
oc-auth-switcher add
|
|
813
|
-
oc-auth-switcher add
|
|
774
|
+
oc-auth-switcher add work
|
|
775
|
+
oc-auth-switcher add personal
|
|
814
776
|
oc-auth-switcher usage --watch
|
|
815
777
|
oc-auth-switcher config --threshold 0.90
|
|
816
|
-
oc-auth-switcher switch
|
|
817
|
-
oc-auth-switcher switch auto
|
|
778
|
+
oc-auth-switcher switch personal
|
|
818
779
|
`);
|
|
819
780
|
}
|
|
820
781
|
async function main() {
|
package/dist/index.js
CHANGED
|
@@ -414,8 +414,8 @@ var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(),
|
|
|
414
414
|
var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
|
|
415
415
|
var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
416
416
|
var DEFAULT_THRESHOLD = 0.95;
|
|
417
|
-
var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
418
417
|
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
418
|
+
var REJECTION_FALLBACK_SECONDS = 60 * 60;
|
|
419
419
|
var METRIC_MODEL_FAMILY = {
|
|
420
420
|
session5h: null,
|
|
421
421
|
weekly7d: null,
|
|
@@ -533,13 +533,9 @@ var EMPTY_USAGE = {
|
|
|
533
533
|
function defaultState() {
|
|
534
534
|
return {
|
|
535
535
|
currentAccount: null,
|
|
536
|
-
selectionMode: "auto",
|
|
537
|
-
manualAccount: null,
|
|
538
|
-
lastRotationCheck: 0,
|
|
539
536
|
requestCount: 0,
|
|
540
537
|
config: {
|
|
541
|
-
threshold: DEFAULT_THRESHOLD
|
|
542
|
-
checkInterval: DEFAULT_CHECK_INTERVAL
|
|
538
|
+
threshold: DEFAULT_THRESHOLD
|
|
543
539
|
},
|
|
544
540
|
usage: {},
|
|
545
541
|
authFailures: {}
|
|
@@ -567,13 +563,9 @@ function normalizeState(raw) {
|
|
|
567
563
|
]));
|
|
568
564
|
return {
|
|
569
565
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
570
|
-
selectionMode: raw.selectionMode === "manual" && raw.manualAccount ? "manual" : "auto",
|
|
571
|
-
manualAccount: raw.manualAccount ?? defaults.manualAccount,
|
|
572
|
-
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
573
566
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
574
567
|
config: {
|
|
575
|
-
threshold: migratedThreshold
|
|
576
|
-
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
568
|
+
threshold: migratedThreshold
|
|
577
569
|
},
|
|
578
570
|
usage,
|
|
579
571
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
@@ -714,7 +706,7 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
714
706
|
if (!Number.isFinite(reset) || reset <= 0) {
|
|
715
707
|
const retryAfterHeader = headers.get("retry-after");
|
|
716
708
|
const retryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
|
|
717
|
-
reset = Date.now() / 1000 + (Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter :
|
|
709
|
+
reset = Date.now() / 1000 + (Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : REJECTION_FALLBACK_SECONDS);
|
|
718
710
|
}
|
|
719
711
|
usage.rejected = {
|
|
720
712
|
utilization: 1,
|
|
@@ -758,27 +750,6 @@ function getUtilizationScore(usage, state, modelFamily) {
|
|
|
758
750
|
const scores = metricEntries(usage, thresholds, modelFamily).filter((metric) => metric.threshold > 0).map((metric) => metric.util / metric.threshold);
|
|
759
751
|
return scores.length > 0 ? Math.max(...scores) : 0;
|
|
760
752
|
}
|
|
761
|
-
function getExceededMetric(usage, state, modelFamily) {
|
|
762
|
-
if (!usage)
|
|
763
|
-
return null;
|
|
764
|
-
const thresholds = getThresholds(state.config);
|
|
765
|
-
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily)) {
|
|
766
|
-
return usage.rejected.prefix ? `rejected rate limit (${usage.rejected.prefix})` : "rejected rate limit";
|
|
767
|
-
}
|
|
768
|
-
const metrics = metricEntries(usage, thresholds, modelFamily).map(({ name, util, threshold }) => ({ name, util, thresh: threshold }));
|
|
769
|
-
const exceeded = metrics.filter((m) => m.thresh > 0 && m.util >= m.thresh).sort((a, b) => b.util / b.thresh - a.util / a.thresh);
|
|
770
|
-
return exceeded.length > 0 ? exceeded[0].name : null;
|
|
771
|
-
}
|
|
772
|
-
function getEarliestReset(usage, modelFamily) {
|
|
773
|
-
if (!usage)
|
|
774
|
-
return 0;
|
|
775
|
-
const resets = Object.keys(METRIC_MODEL_FAMILY).filter((key) => isMetricRelevant(key, modelFamily)).map((key) => usage[key].reset);
|
|
776
|
-
if (isRejectionRelevant(usage.rejected.prefix, modelFamily)) {
|
|
777
|
-
resets.push(usage.rejected.reset);
|
|
778
|
-
}
|
|
779
|
-
const activeResets = resets.filter((reset) => reset > 0);
|
|
780
|
-
return activeResets.length > 0 ? Math.min(...activeResets) * 1000 : 0;
|
|
781
|
-
}
|
|
782
753
|
function getModelFamily(model) {
|
|
783
754
|
if (!model)
|
|
784
755
|
return;
|
|
@@ -815,17 +786,25 @@ function purgeExpiredCooldowns(state) {
|
|
|
815
786
|
}
|
|
816
787
|
}
|
|
817
788
|
function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
789
|
+
let best = null;
|
|
790
|
+
let bestScore = Infinity;
|
|
818
791
|
for (const acct of candidates) {
|
|
819
792
|
if (exclude.has(acct.name))
|
|
820
793
|
continue;
|
|
821
794
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
822
795
|
continue;
|
|
823
796
|
if (!isOverThreshold(state.usage[acct.name], state, modelFamily)) {
|
|
824
|
-
|
|
797
|
+
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
798
|
+
if (!best || score < bestScore) {
|
|
799
|
+
bestScore = score;
|
|
800
|
+
best = acct;
|
|
801
|
+
}
|
|
825
802
|
}
|
|
826
803
|
}
|
|
827
|
-
|
|
828
|
-
|
|
804
|
+
if (best)
|
|
805
|
+
return best;
|
|
806
|
+
best = null;
|
|
807
|
+
bestScore = Infinity;
|
|
829
808
|
for (const acct of candidates) {
|
|
830
809
|
if (exclude.has(acct.name))
|
|
831
810
|
continue;
|
|
@@ -845,73 +824,22 @@ function selectAccount(accounts, state, model) {
|
|
|
845
824
|
}
|
|
846
825
|
purgeExpiredCooldowns(state);
|
|
847
826
|
const modelFamily = getModelFamily(model);
|
|
848
|
-
if (state.selectionMode === "manual" && state.manualAccount) {
|
|
849
|
-
const manual = accounts.find((account) => account.name === state.manualAccount);
|
|
850
|
-
if (manual) {
|
|
851
|
-
const unavailable = isTemporarilyUnavailable(state, manual.name);
|
|
852
|
-
const exhausted = isOverThreshold(state.usage[manual.name], state, modelFamily);
|
|
853
|
-
if (!unavailable && !exhausted) {
|
|
854
|
-
return {
|
|
855
|
-
account: manual,
|
|
856
|
-
switched: state.currentAccount !== manual.name,
|
|
857
|
-
reason: state.currentAccount !== manual.name ? `Manual selection — switching to ${manual.name}` : undefined
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
state.currentAccount = manual.name;
|
|
861
|
-
}
|
|
862
|
-
state.selectionMode = "auto";
|
|
863
|
-
state.manualAccount = null;
|
|
864
|
-
}
|
|
865
|
-
if (accounts.length === 1) {
|
|
866
|
-
return { account: accounts[0], switched: false };
|
|
867
|
-
}
|
|
868
|
-
const primary = accounts[0];
|
|
869
|
-
const fallbacks = accounts.slice(1);
|
|
870
|
-
if (!state.currentAccount) {
|
|
871
|
-
return {
|
|
872
|
-
account: primary,
|
|
873
|
-
switched: true,
|
|
874
|
-
reason: "Initial selection — using primary account"
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
827
|
const currentIdx = accounts.findIndex((a) => a.name === state.currentAccount);
|
|
878
828
|
if (currentIdx < 0) {
|
|
829
|
+
const best = findBestAvailable(accounts, state, new Set, modelFamily);
|
|
830
|
+
const selected = best ?? accounts[0];
|
|
879
831
|
return {
|
|
880
|
-
account:
|
|
832
|
+
account: selected,
|
|
881
833
|
switched: true,
|
|
882
|
-
reason:
|
|
834
|
+
reason: `Selecting account ${selected.name}`
|
|
883
835
|
};
|
|
884
836
|
}
|
|
885
837
|
const current = accounts[currentIdx];
|
|
886
838
|
const currentUsage = state.usage[current.name];
|
|
887
|
-
const primaryUsage = state.usage[primary.name];
|
|
888
|
-
const isPrimary = current.name === primary.name;
|
|
889
|
-
if (isPrimary) {
|
|
890
|
-
if (isOverThreshold(primaryUsage, state, modelFamily)) {
|
|
891
|
-
const exceededMetric = getExceededMetric(primaryUsage, state, modelFamily);
|
|
892
|
-
const best = findBestAvailable(fallbacks, state, new Set, modelFamily);
|
|
893
|
-
if (best) {
|
|
894
|
-
return {
|
|
895
|
-
account: best,
|
|
896
|
-
switched: true,
|
|
897
|
-
reason: `Primary exceeded ${exceededMetric} threshold — switching to ${best.name}`
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
return { account: primary, switched: false };
|
|
901
|
-
}
|
|
902
|
-
return { account: primary, switched: false };
|
|
903
|
-
}
|
|
904
839
|
const currentOverThreshold = isOverThreshold(currentUsage, state, modelFamily);
|
|
905
840
|
const currentInCooldown = isTemporarilyUnavailable(state, current.name);
|
|
906
841
|
if (currentOverThreshold || currentInCooldown) {
|
|
907
842
|
const reason = currentInCooldown ? `${current.name} in auth-failure cooldown` : `${current.name} exceeded threshold`;
|
|
908
|
-
if (!isOverThreshold(primaryUsage, state, modelFamily) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
909
|
-
return {
|
|
910
|
-
account: primary,
|
|
911
|
-
switched: true,
|
|
912
|
-
reason: `${reason} — switching back to primary`
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
843
|
const best = findBestAvailable(accounts, state, new Set([current.name]), modelFamily);
|
|
916
844
|
if (best && best.name !== current.name) {
|
|
917
845
|
return {
|
|
@@ -922,29 +850,10 @@ function selectAccount(accounts, state, model) {
|
|
|
922
850
|
}
|
|
923
851
|
return { account: current, switched: false };
|
|
924
852
|
}
|
|
925
|
-
const now = Date.now();
|
|
926
|
-
const checkInterval = state.config.checkInterval;
|
|
927
|
-
const earliestReset = getEarliestReset(primaryUsage, modelFamily);
|
|
928
|
-
const timeSinceLastCheck = now - state.lastRotationCheck;
|
|
929
|
-
const shouldCheckPrimary = earliestReset > 0 && earliestReset <= now || timeSinceLastCheck >= checkInterval;
|
|
930
|
-
if (shouldCheckPrimary) {
|
|
931
|
-
state.lastRotationCheck = now;
|
|
932
|
-
if (!isOverThreshold(primaryUsage, state, modelFamily) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
933
|
-
return {
|
|
934
|
-
account: primary,
|
|
935
|
-
switched: true,
|
|
936
|
-
reason: "Primary has recovered — switching back"
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
853
|
return { account: current, switched: false };
|
|
941
854
|
}
|
|
942
855
|
function markAuthFailure(state, accountName) {
|
|
943
856
|
state.authFailures[accountName] = Date.now() + AUTH_FAILURE_COOLDOWN;
|
|
944
|
-
if (state.selectionMode === "manual" && state.manualAccount === accountName) {
|
|
945
|
-
state.selectionMode = "auto";
|
|
946
|
-
state.manualAccount = null;
|
|
947
|
-
}
|
|
948
857
|
}
|
|
949
858
|
function clearAuthFailure(state, accountName) {
|
|
950
859
|
delete state.authFailures[accountName];
|
|
@@ -953,20 +862,14 @@ function clearAuthFailure(state, accountName) {
|
|
|
953
862
|
// src/index.ts
|
|
954
863
|
function selectionSnapshot(state) {
|
|
955
864
|
return {
|
|
956
|
-
|
|
957
|
-
manualAccount: state.manualAccount,
|
|
958
|
-
currentAccount: state.currentAccount,
|
|
959
|
-
lastRotationCheck: state.lastRotationCheck
|
|
865
|
+
currentAccount: state.currentAccount
|
|
960
866
|
};
|
|
961
867
|
}
|
|
962
868
|
function saveRequestState(state, initiallyLoaded) {
|
|
963
869
|
const onDisk = loadState();
|
|
964
|
-
const selectionChangedSinceLoad = onDisk.
|
|
870
|
+
const selectionChangedSinceLoad = onDisk.currentAccount !== initiallyLoaded.currentAccount;
|
|
965
871
|
if (selectionChangedSinceLoad) {
|
|
966
|
-
state.selectionMode = onDisk.selectionMode;
|
|
967
|
-
state.manualAccount = onDisk.manualAccount;
|
|
968
872
|
state.currentAccount = onDisk.currentAccount;
|
|
969
|
-
state.lastRotationCheck = onDisk.lastRotationCheck;
|
|
970
873
|
}
|
|
971
874
|
saveState(state);
|
|
972
875
|
}
|
|
@@ -1073,7 +976,8 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1073
976
|
updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
1074
977
|
} else {
|
|
1075
978
|
markAuthFailure(state, account.name);
|
|
1076
|
-
const
|
|
979
|
+
const available = accounts.filter((candidate) => !attemptedAccounts.has(candidate.name) && (!state.authFailures[candidate.name] || state.authFailures[candidate.name] <= Date.now()));
|
|
980
|
+
const next = available.length > 0 ? selectAccount(available, state, model).account : undefined;
|
|
1077
981
|
if (!next) {
|
|
1078
982
|
saveRequestState(state, initiallyLoadedSelection);
|
|
1079
983
|
throw new Error(`[oc-auth-switcher] All accounts failed token refresh`);
|
|
@@ -1105,7 +1009,8 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1105
1009
|
const isScopeError = errorBody.includes("scope") || errorBody.includes("unauthorized") || errorBody.includes("invalid");
|
|
1106
1010
|
if (isScopeError) {
|
|
1107
1011
|
markAuthFailure(state, account.name);
|
|
1108
|
-
const
|
|
1012
|
+
const available = accounts.filter((candidate) => !attemptedAccounts.has(candidate.name) && (!state.authFailures[candidate.name] || state.authFailures[candidate.name] <= Date.now()));
|
|
1013
|
+
const next = available.length > 0 ? selectAccount(available, state, model).account : undefined;
|
|
1109
1014
|
if (next) {
|
|
1110
1015
|
attemptedAccounts.add("__retried__");
|
|
1111
1016
|
account = next;
|