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.
Files changed (2) hide show
  1. package/dist/index.js +76 -41
  2. 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
- 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;
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
- usage.session5h.reset,
779
- usage.weekly7d.reset,
780
- usage.weekly7dSonnet.reset,
781
- usage.weekly7dFable.reset,
782
- usage.rejected.reset
783
- ].filter((r) => r > 0);
784
- return resets.length > 0 ? Math.min(...resets) * 1000 : 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
+ 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 selection = selectAccount(accounts, state);
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.4.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",