oc-auth-switcher 0.4.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/index.js +76 -41
- package/package.json +1 -1
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) {
|
|
@@ -735,53 +741,70 @@ function isTemporarilyUnavailable(state, accountName) {
|
|
|
735
741
|
}
|
|
736
742
|
return true;
|
|
737
743
|
}
|
|
738
|
-
function isOverThreshold(usage, state) {
|
|
744
|
+
function isOverThreshold(usage, state, modelFamily) {
|
|
739
745
|
if (!usage)
|
|
740
746
|
return false;
|
|
741
747
|
const thresholds = getThresholds(state.config);
|
|
742
|
-
|
|
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);
|
|
743
751
|
}
|
|
744
|
-
function getUtilizationScore(usage, state) {
|
|
752
|
+
function getUtilizationScore(usage, state, modelFamily) {
|
|
745
753
|
if (!usage)
|
|
746
754
|
return 0;
|
|
747
755
|
const thresholds = getThresholds(state.config);
|
|
748
|
-
if (usage.rejected.status?.toLowerCase() === "rejected")
|
|
756
|
+
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily))
|
|
749
757
|
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);
|
|
758
|
+
const scores = metricEntries(usage, thresholds, modelFamily).filter((metric) => metric.threshold > 0).map((metric) => metric.util / metric.threshold);
|
|
756
759
|
return scores.length > 0 ? Math.max(...scores) : 0;
|
|
757
760
|
}
|
|
758
|
-
function getExceededMetric(usage, state) {
|
|
761
|
+
function getExceededMetric(usage, state, modelFamily) {
|
|
759
762
|
if (!usage)
|
|
760
763
|
return null;
|
|
761
764
|
const thresholds = getThresholds(state.config);
|
|
762
|
-
if (usage.rejected.status?.toLowerCase() === "rejected") {
|
|
765
|
+
if (usage.rejected.status?.toLowerCase() === "rejected" && isRejectionRelevant(usage.rejected.prefix, modelFamily)) {
|
|
763
766
|
return usage.rejected.prefix ? `rejected rate limit (${usage.rejected.prefix})` : "rejected rate limit";
|
|
764
767
|
}
|
|
765
|
-
const metrics =
|
|
766
|
-
{ name: "session5h", util: usage.session5h.utilization, thresh: thresholds.session5h },
|
|
767
|
-
{ name: "weekly7d", util: usage.weekly7d.utilization, thresh: thresholds.weekly7d },
|
|
768
|
-
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet },
|
|
769
|
-
{ name: "weekly7dFable", util: usage.weekly7dFable.utilization, thresh: thresholds.weekly7dFable }
|
|
770
|
-
];
|
|
768
|
+
const metrics = metricEntries(usage, thresholds, modelFamily).map(({ name, util, threshold }) => ({ name, util, thresh: threshold }));
|
|
771
769
|
const exceeded = metrics.filter((m) => m.thresh > 0 && m.util >= m.thresh).sort((a, b) => b.util / b.thresh - a.util / a.thresh);
|
|
772
770
|
return exceeded.length > 0 ? exceeded[0].name : null;
|
|
773
771
|
}
|
|
774
|
-
function getEarliestReset(usage) {
|
|
772
|
+
function getEarliestReset(usage, modelFamily) {
|
|
775
773
|
if (!usage)
|
|
776
774
|
return 0;
|
|
777
|
-
const resets = [
|
|
778
|
-
|
|
779
|
-
usage.
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
}));
|
|
785
808
|
}
|
|
786
809
|
function purgeExpiredCooldowns(state) {
|
|
787
810
|
const now = Date.now();
|
|
@@ -791,13 +814,13 @@ function purgeExpiredCooldowns(state) {
|
|
|
791
814
|
}
|
|
792
815
|
}
|
|
793
816
|
}
|
|
794
|
-
function findBestAvailable(candidates, state, exclude) {
|
|
817
|
+
function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
795
818
|
for (const acct of candidates) {
|
|
796
819
|
if (exclude.has(acct.name))
|
|
797
820
|
continue;
|
|
798
821
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
799
822
|
continue;
|
|
800
|
-
if (!isOverThreshold(state.usage[acct.name], state)) {
|
|
823
|
+
if (!isOverThreshold(state.usage[acct.name], state, modelFamily)) {
|
|
801
824
|
return acct;
|
|
802
825
|
}
|
|
803
826
|
}
|
|
@@ -808,7 +831,7 @@ function findBestAvailable(candidates, state, exclude) {
|
|
|
808
831
|
continue;
|
|
809
832
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
810
833
|
continue;
|
|
811
|
-
const score = getUtilizationScore(state.usage[acct.name], state);
|
|
834
|
+
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
812
835
|
if (!best || score < bestScore) {
|
|
813
836
|
bestScore = score;
|
|
814
837
|
best = acct;
|
|
@@ -816,16 +839,17 @@ function findBestAvailable(candidates, state, exclude) {
|
|
|
816
839
|
}
|
|
817
840
|
return best;
|
|
818
841
|
}
|
|
819
|
-
function selectAccount(accounts, state) {
|
|
842
|
+
function selectAccount(accounts, state, model) {
|
|
820
843
|
if (accounts.length === 0) {
|
|
821
844
|
throw new Error("No accounts available");
|
|
822
845
|
}
|
|
823
846
|
purgeExpiredCooldowns(state);
|
|
847
|
+
const modelFamily = getModelFamily(model);
|
|
824
848
|
if (state.selectionMode === "manual" && state.manualAccount) {
|
|
825
849
|
const manual = accounts.find((account) => account.name === state.manualAccount);
|
|
826
850
|
if (manual) {
|
|
827
851
|
const unavailable = isTemporarilyUnavailable(state, manual.name);
|
|
828
|
-
const exhausted = isOverThreshold(state.usage[manual.name], state);
|
|
852
|
+
const exhausted = isOverThreshold(state.usage[manual.name], state, modelFamily);
|
|
829
853
|
if (!unavailable && !exhausted) {
|
|
830
854
|
return {
|
|
831
855
|
account: manual,
|
|
@@ -863,9 +887,9 @@ function selectAccount(accounts, state) {
|
|
|
863
887
|
const primaryUsage = state.usage[primary.name];
|
|
864
888
|
const isPrimary = current.name === primary.name;
|
|
865
889
|
if (isPrimary) {
|
|
866
|
-
if (isOverThreshold(primaryUsage, state)) {
|
|
867
|
-
const exceededMetric = getExceededMetric(primaryUsage, state);
|
|
868
|
-
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);
|
|
869
893
|
if (best) {
|
|
870
894
|
return {
|
|
871
895
|
account: best,
|
|
@@ -877,18 +901,18 @@ function selectAccount(accounts, state) {
|
|
|
877
901
|
}
|
|
878
902
|
return { account: primary, switched: false };
|
|
879
903
|
}
|
|
880
|
-
const currentOverThreshold = isOverThreshold(currentUsage, state);
|
|
904
|
+
const currentOverThreshold = isOverThreshold(currentUsage, state, modelFamily);
|
|
881
905
|
const currentInCooldown = isTemporarilyUnavailable(state, current.name);
|
|
882
906
|
if (currentOverThreshold || currentInCooldown) {
|
|
883
907
|
const reason = currentInCooldown ? `${current.name} in auth-failure cooldown` : `${current.name} exceeded threshold`;
|
|
884
|
-
if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
908
|
+
if (!isOverThreshold(primaryUsage, state, modelFamily) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
885
909
|
return {
|
|
886
910
|
account: primary,
|
|
887
911
|
switched: true,
|
|
888
912
|
reason: `${reason} — switching back to primary`
|
|
889
913
|
};
|
|
890
914
|
}
|
|
891
|
-
const best = findBestAvailable(accounts, state, new Set([current.name]));
|
|
915
|
+
const best = findBestAvailable(accounts, state, new Set([current.name]), modelFamily);
|
|
892
916
|
if (best && best.name !== current.name) {
|
|
893
917
|
return {
|
|
894
918
|
account: best,
|
|
@@ -900,12 +924,12 @@ function selectAccount(accounts, state) {
|
|
|
900
924
|
}
|
|
901
925
|
const now = Date.now();
|
|
902
926
|
const checkInterval = state.config.checkInterval;
|
|
903
|
-
const earliestReset = getEarliestReset(primaryUsage);
|
|
927
|
+
const earliestReset = getEarliestReset(primaryUsage, modelFamily);
|
|
904
928
|
const timeSinceLastCheck = now - state.lastRotationCheck;
|
|
905
929
|
const shouldCheckPrimary = earliestReset > 0 && earliestReset <= now || timeSinceLastCheck >= checkInterval;
|
|
906
930
|
if (shouldCheckPrimary) {
|
|
907
931
|
state.lastRotationCheck = now;
|
|
908
|
-
if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
932
|
+
if (!isOverThreshold(primaryUsage, state, modelFamily) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
909
933
|
return {
|
|
910
934
|
account: primary,
|
|
911
935
|
switched: true,
|
|
@@ -946,6 +970,16 @@ function saveRequestState(state, initiallyLoaded) {
|
|
|
946
970
|
}
|
|
947
971
|
saveState(state);
|
|
948
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
|
+
}
|
|
949
983
|
var AuthSwitcherPlugin = async ({ client }) => {
|
|
950
984
|
return {
|
|
951
985
|
auth: {
|
|
@@ -1014,7 +1048,8 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1014
1048
|
}
|
|
1015
1049
|
ensureAccountsInState(state, accounts.map((a) => a.name));
|
|
1016
1050
|
resolveStaleMetrics(state);
|
|
1017
|
-
const
|
|
1051
|
+
const model = getRequestModel(init?.body);
|
|
1052
|
+
const selection = selectAccount(accounts, state, model);
|
|
1018
1053
|
let account = selection.account;
|
|
1019
1054
|
if (selection.switched) {
|
|
1020
1055
|
await client.app.log({
|