oc-auth-switcher 0.3.0 → 0.5.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 +10 -4
- package/dist/index.js +86 -42
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -171,8 +171,7 @@ function defaultState() {
|
|
|
171
171
|
authFailures: {}
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
|
-
function
|
|
175
|
-
const raw = safeReadJSON(STATE_FILE, {});
|
|
174
|
+
function normalizeState(raw) {
|
|
176
175
|
const defaults = defaultState();
|
|
177
176
|
const threshold = raw.config?.threshold;
|
|
178
177
|
const migratedThreshold = typeof threshold === "object" && threshold !== null ? {
|
|
@@ -206,6 +205,9 @@ function loadState() {
|
|
|
206
205
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
207
206
|
};
|
|
208
207
|
}
|
|
208
|
+
function loadState() {
|
|
209
|
+
return normalizeState(safeReadJSON(STATE_FILE, {}));
|
|
210
|
+
}
|
|
209
211
|
function saveState(state) {
|
|
210
212
|
safeWriteJSON(STATE_FILE, state);
|
|
211
213
|
}
|
|
@@ -649,8 +651,8 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
649
651
|
const arg = args[i];
|
|
650
652
|
if (arg === "--threshold" && args[i + 1]) {
|
|
651
653
|
const val = parseFloat(args[++i]);
|
|
652
|
-
if (isNaN(val) || val
|
|
653
|
-
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}`);
|
|
654
656
|
process.exit(1);
|
|
655
657
|
}
|
|
656
658
|
state.config.threshold = val;
|
|
@@ -662,6 +664,10 @@ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
|
|
|
662
664
|
process.exit(1);
|
|
663
665
|
}
|
|
664
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
|
+
}
|
|
665
671
|
const currentFableThreshold = getThresholds(state.config).weekly7dFable;
|
|
666
672
|
state.config.threshold = {
|
|
667
673
|
session5h: normalized[0],
|
package/dist/index.js
CHANGED
|
@@ -416,6 +416,12 @@ var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
|
416
416
|
var DEFAULT_THRESHOLD = 0.95;
|
|
417
417
|
var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
418
418
|
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
419
|
+
var METRIC_MODEL_FAMILY = {
|
|
420
|
+
session5h: null,
|
|
421
|
+
weekly7d: null,
|
|
422
|
+
weekly7dSonnet: "sonnet",
|
|
423
|
+
weekly7dFable: "fable"
|
|
424
|
+
};
|
|
419
425
|
|
|
420
426
|
// src/accounts.ts
|
|
421
427
|
function normalizeAccount(raw) {
|
|
@@ -539,8 +545,7 @@ function defaultState() {
|
|
|
539
545
|
authFailures: {}
|
|
540
546
|
};
|
|
541
547
|
}
|
|
542
|
-
function
|
|
543
|
-
const raw = safeReadJSON(STATE_FILE, {});
|
|
548
|
+
function normalizeState(raw) {
|
|
544
549
|
const defaults = defaultState();
|
|
545
550
|
const threshold = raw.config?.threshold;
|
|
546
551
|
const migratedThreshold = typeof threshold === "object" && threshold !== null ? {
|
|
@@ -574,6 +579,9 @@ function loadState() {
|
|
|
574
579
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
575
580
|
};
|
|
576
581
|
}
|
|
582
|
+
function loadState() {
|
|
583
|
+
return normalizeState(safeReadJSON(STATE_FILE, {}));
|
|
584
|
+
}
|
|
577
585
|
function saveState(state) {
|
|
578
586
|
safeWriteJSON(STATE_FILE, state);
|
|
579
587
|
}
|
|
@@ -711,7 +719,8 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
711
719
|
usage.rejected = {
|
|
712
720
|
utilization: 1,
|
|
713
721
|
reset: Math.max(usage.rejected.reset, reset),
|
|
714
|
-
status: "rejected"
|
|
722
|
+
status: "rejected",
|
|
723
|
+
prefix
|
|
715
724
|
};
|
|
716
725
|
updated = true;
|
|
717
726
|
}
|
|
@@ -732,47 +741,70 @@ function isTemporarilyUnavailable(state, accountName) {
|
|
|
732
741
|
}
|
|
733
742
|
return true;
|
|
734
743
|
}
|
|
735
|
-
function isOverThreshold(usage, state) {
|
|
744
|
+
function isOverThreshold(usage, state, modelFamily) {
|
|
736
745
|
if (!usage)
|
|
737
746
|
return false;
|
|
738
747
|
const thresholds = getThresholds(state.config);
|
|
739
|
-
|
|
748
|
+
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily))
|
|
749
|
+
return true;
|
|
750
|
+
return metricEntries(usage, thresholds, modelFamily).some((metric) => metric.threshold > 0 && metric.util >= metric.threshold);
|
|
740
751
|
}
|
|
741
|
-
function getUtilizationScore(usage, state) {
|
|
752
|
+
function getUtilizationScore(usage, state, modelFamily) {
|
|
742
753
|
if (!usage)
|
|
743
754
|
return 0;
|
|
744
755
|
const thresholds = getThresholds(state.config);
|
|
745
|
-
if (usage.rejected.status?.toLowerCase() === "rejected")
|
|
756
|
+
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily))
|
|
746
757
|
return Infinity;
|
|
747
|
-
|
|
758
|
+
const scores = metricEntries(usage, thresholds, modelFamily).filter((metric) => metric.threshold > 0).map((metric) => metric.util / metric.threshold);
|
|
759
|
+
return scores.length > 0 ? Math.max(...scores) : 0;
|
|
748
760
|
}
|
|
749
|
-
function getExceededMetric(usage, state) {
|
|
761
|
+
function getExceededMetric(usage, state, modelFamily) {
|
|
750
762
|
if (!usage)
|
|
751
763
|
return null;
|
|
752
764
|
const thresholds = getThresholds(state.config);
|
|
753
|
-
if (usage.rejected.status?.toLowerCase() === "rejected") {
|
|
754
|
-
return "rejected rate limit";
|
|
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";
|
|
755
767
|
}
|
|
756
|
-
const metrics =
|
|
757
|
-
|
|
758
|
-
{ name: "weekly7d", util: usage.weekly7d.utilization, thresh: thresholds.weekly7d },
|
|
759
|
-
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet },
|
|
760
|
-
{ name: "weekly7dFable", util: usage.weekly7dFable.utilization, thresh: thresholds.weekly7dFable }
|
|
761
|
-
];
|
|
762
|
-
const exceeded = metrics.filter((m) => m.util > m.thresh).sort((a, b) => b.util / b.thresh - a.util / a.thresh);
|
|
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);
|
|
763
770
|
return exceeded.length > 0 ? exceeded[0].name : null;
|
|
764
771
|
}
|
|
765
|
-
function getEarliestReset(usage) {
|
|
772
|
+
function getEarliestReset(usage, modelFamily) {
|
|
766
773
|
if (!usage)
|
|
767
774
|
return 0;
|
|
768
|
-
const resets = [
|
|
769
|
-
|
|
770
|
-
usage.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
+
function getModelFamily(model) {
|
|
783
|
+
if (!model)
|
|
784
|
+
return;
|
|
785
|
+
const normalized = model.toLowerCase();
|
|
786
|
+
return ["fable", "sonnet", "opus"].find((family) => normalized.includes(family));
|
|
787
|
+
}
|
|
788
|
+
function isMetricRelevant(metric, modelFamily) {
|
|
789
|
+
const metricFamily = METRIC_MODEL_FAMILY[metric];
|
|
790
|
+
return modelFamily === undefined || metricFamily === null || metricFamily === modelFamily;
|
|
791
|
+
}
|
|
792
|
+
function rejectionFamily(prefix) {
|
|
793
|
+
if (!prefix)
|
|
794
|
+
return;
|
|
795
|
+
const normalized = prefix.toLowerCase();
|
|
796
|
+
return ["fable", "sonnet", "opus"].find((family) => normalized === `7d_${family}` || normalized === `anthropic-ratelimit-unified-7d_${family}`);
|
|
797
|
+
}
|
|
798
|
+
function isRejectionRelevant(prefix, modelFamily) {
|
|
799
|
+
const rejectedFamily = rejectionFamily(prefix);
|
|
800
|
+
return modelFamily === undefined || rejectedFamily === undefined || rejectedFamily === modelFamily;
|
|
801
|
+
}
|
|
802
|
+
function metricEntries(usage, thresholds, modelFamily) {
|
|
803
|
+
return Object.keys(METRIC_MODEL_FAMILY).filter((key) => isMetricRelevant(key, modelFamily)).map((key) => ({
|
|
804
|
+
name: key,
|
|
805
|
+
util: usage[key].utilization,
|
|
806
|
+
threshold: thresholds[key]
|
|
807
|
+
}));
|
|
776
808
|
}
|
|
777
809
|
function purgeExpiredCooldowns(state) {
|
|
778
810
|
const now = Date.now();
|
|
@@ -782,13 +814,13 @@ function purgeExpiredCooldowns(state) {
|
|
|
782
814
|
}
|
|
783
815
|
}
|
|
784
816
|
}
|
|
785
|
-
function findBestAvailable(candidates, state, exclude) {
|
|
817
|
+
function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
786
818
|
for (const acct of candidates) {
|
|
787
819
|
if (exclude.has(acct.name))
|
|
788
820
|
continue;
|
|
789
821
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
790
822
|
continue;
|
|
791
|
-
if (!isOverThreshold(state.usage[acct.name], state)) {
|
|
823
|
+
if (!isOverThreshold(state.usage[acct.name], state, modelFamily)) {
|
|
792
824
|
return acct;
|
|
793
825
|
}
|
|
794
826
|
}
|
|
@@ -799,24 +831,25 @@ function findBestAvailable(candidates, state, exclude) {
|
|
|
799
831
|
continue;
|
|
800
832
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
801
833
|
continue;
|
|
802
|
-
const score = getUtilizationScore(state.usage[acct.name], state);
|
|
803
|
-
if (score < bestScore) {
|
|
834
|
+
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
835
|
+
if (!best || score < bestScore) {
|
|
804
836
|
bestScore = score;
|
|
805
837
|
best = acct;
|
|
806
838
|
}
|
|
807
839
|
}
|
|
808
840
|
return best;
|
|
809
841
|
}
|
|
810
|
-
function selectAccount(accounts, state) {
|
|
842
|
+
function selectAccount(accounts, state, model) {
|
|
811
843
|
if (accounts.length === 0) {
|
|
812
844
|
throw new Error("No accounts available");
|
|
813
845
|
}
|
|
814
846
|
purgeExpiredCooldowns(state);
|
|
847
|
+
const modelFamily = getModelFamily(model);
|
|
815
848
|
if (state.selectionMode === "manual" && state.manualAccount) {
|
|
816
849
|
const manual = accounts.find((account) => account.name === state.manualAccount);
|
|
817
850
|
if (manual) {
|
|
818
851
|
const unavailable = isTemporarilyUnavailable(state, manual.name);
|
|
819
|
-
const exhausted = isOverThreshold(state.usage[manual.name], state);
|
|
852
|
+
const exhausted = isOverThreshold(state.usage[manual.name], state, modelFamily);
|
|
820
853
|
if (!unavailable && !exhausted) {
|
|
821
854
|
return {
|
|
822
855
|
account: manual,
|
|
@@ -854,9 +887,9 @@ function selectAccount(accounts, state) {
|
|
|
854
887
|
const primaryUsage = state.usage[primary.name];
|
|
855
888
|
const isPrimary = current.name === primary.name;
|
|
856
889
|
if (isPrimary) {
|
|
857
|
-
if (isOverThreshold(primaryUsage, state)) {
|
|
858
|
-
const exceededMetric = getExceededMetric(primaryUsage, state);
|
|
859
|
-
const best = findBestAvailable(fallbacks, state, new Set);
|
|
890
|
+
if (isOverThreshold(primaryUsage, state, modelFamily)) {
|
|
891
|
+
const exceededMetric = getExceededMetric(primaryUsage, state, modelFamily);
|
|
892
|
+
const best = findBestAvailable(fallbacks, state, new Set, modelFamily);
|
|
860
893
|
if (best) {
|
|
861
894
|
return {
|
|
862
895
|
account: best,
|
|
@@ -868,18 +901,18 @@ function selectAccount(accounts, state) {
|
|
|
868
901
|
}
|
|
869
902
|
return { account: primary, switched: false };
|
|
870
903
|
}
|
|
871
|
-
const currentOverThreshold = isOverThreshold(currentUsage, state);
|
|
904
|
+
const currentOverThreshold = isOverThreshold(currentUsage, state, modelFamily);
|
|
872
905
|
const currentInCooldown = isTemporarilyUnavailable(state, current.name);
|
|
873
906
|
if (currentOverThreshold || currentInCooldown) {
|
|
874
907
|
const reason = currentInCooldown ? `${current.name} in auth-failure cooldown` : `${current.name} exceeded threshold`;
|
|
875
|
-
if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
908
|
+
if (!isOverThreshold(primaryUsage, state, modelFamily) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
876
909
|
return {
|
|
877
910
|
account: primary,
|
|
878
911
|
switched: true,
|
|
879
912
|
reason: `${reason} — switching back to primary`
|
|
880
913
|
};
|
|
881
914
|
}
|
|
882
|
-
const best = findBestAvailable(accounts, state, new Set([current.name]));
|
|
915
|
+
const best = findBestAvailable(accounts, state, new Set([current.name]), modelFamily);
|
|
883
916
|
if (best && best.name !== current.name) {
|
|
884
917
|
return {
|
|
885
918
|
account: best,
|
|
@@ -891,12 +924,12 @@ function selectAccount(accounts, state) {
|
|
|
891
924
|
}
|
|
892
925
|
const now = Date.now();
|
|
893
926
|
const checkInterval = state.config.checkInterval;
|
|
894
|
-
const earliestReset = getEarliestReset(primaryUsage);
|
|
927
|
+
const earliestReset = getEarliestReset(primaryUsage, modelFamily);
|
|
895
928
|
const timeSinceLastCheck = now - state.lastRotationCheck;
|
|
896
929
|
const shouldCheckPrimary = earliestReset > 0 && earliestReset <= now || timeSinceLastCheck >= checkInterval;
|
|
897
930
|
if (shouldCheckPrimary) {
|
|
898
931
|
state.lastRotationCheck = now;
|
|
899
|
-
if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
932
|
+
if (!isOverThreshold(primaryUsage, state, modelFamily) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
900
933
|
return {
|
|
901
934
|
account: primary,
|
|
902
935
|
switched: true,
|
|
@@ -937,6 +970,16 @@ function saveRequestState(state, initiallyLoaded) {
|
|
|
937
970
|
}
|
|
938
971
|
saveState(state);
|
|
939
972
|
}
|
|
973
|
+
function getRequestModel(body) {
|
|
974
|
+
if (typeof body !== "string")
|
|
975
|
+
return;
|
|
976
|
+
try {
|
|
977
|
+
const parsed = JSON.parse(body);
|
|
978
|
+
return typeof parsed.model === "string" ? parsed.model : undefined;
|
|
979
|
+
} catch {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
940
983
|
var AuthSwitcherPlugin = async ({ client }) => {
|
|
941
984
|
return {
|
|
942
985
|
auth: {
|
|
@@ -1005,7 +1048,8 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1005
1048
|
}
|
|
1006
1049
|
ensureAccountsInState(state, accounts.map((a) => a.name));
|
|
1007
1050
|
resolveStaleMetrics(state);
|
|
1008
|
-
const
|
|
1051
|
+
const model = getRequestModel(init?.body);
|
|
1052
|
+
const selection = selectAccount(accounts, state, model);
|
|
1009
1053
|
let account = selection.account;
|
|
1010
1054
|
if (selection.switched) {
|
|
1011
1055
|
await client.app.log({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-auth-switcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|