oc-auth-switcher 0.9.0 → 0.9.1

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 CHANGED
@@ -206,11 +206,12 @@ async function refreshAccountToken(refreshTokenValue) {
206
206
 
207
207
  // src/state.ts
208
208
  var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
209
+ var EMPTY_MODEL_METRIC = { utilization: null, reset: 0, status: "" };
209
210
  var EMPTY_USAGE = {
210
211
  session5h: { ...EMPTY_METRIC },
211
212
  weekly7d: { ...EMPTY_METRIC },
212
- weekly7dSonnet: { ...EMPTY_METRIC },
213
- weekly7dFable: { ...EMPTY_METRIC },
213
+ weekly7dSonnet: { ...EMPTY_MODEL_METRIC },
214
+ weekly7dFable: { ...EMPTY_MODEL_METRIC },
214
215
  rejected: { ...EMPTY_METRIC }
215
216
  };
216
217
  function defaultState() {
@@ -422,6 +423,85 @@ function updateUsageFromHeaders(state, accountName, headers) {
422
423
  }
423
424
  return updated;
424
425
  }
426
+ async function fetchOAuthUsage(accessToken) {
427
+ try {
428
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
429
+ method: "GET",
430
+ headers: {
431
+ authorization: `Bearer ${accessToken}`,
432
+ "anthropic-version": "2023-06-01",
433
+ "user-agent": "claude-cli/2.1.87 (external, cli)"
434
+ }
435
+ });
436
+ if (!response.ok) {
437
+ const body = await response.text().catch(() => "");
438
+ return { ok: false, error: `HTTP ${response.status}: ${body.slice(0, 100)}` };
439
+ }
440
+ const data = await response.json();
441
+ return { ok: true, data };
442
+ } catch (err) {
443
+ return {
444
+ ok: false,
445
+ error: err instanceof Error ? err.message : String(err)
446
+ };
447
+ }
448
+ }
449
+ function mapModelDisplayNameToField(displayName) {
450
+ const normalized = displayName.toLowerCase();
451
+ if (normalized === "fable")
452
+ return "weekly7dFable";
453
+ if (normalized === "sonnet")
454
+ return "weekly7dSonnet";
455
+ return;
456
+ }
457
+ function isoToEpochSeconds(iso) {
458
+ if (!iso)
459
+ return 0;
460
+ const parsed = Date.parse(iso);
461
+ return isNaN(parsed) ? 0 : parsed / 1000;
462
+ }
463
+ function percentToFraction(percent) {
464
+ return percent / 100;
465
+ }
466
+ function updateUsageFromOAuthEndpoint(state, accountName, response) {
467
+ if (!state.usage[accountName]) {
468
+ state.usage[accountName] = {
469
+ session5h: { ...EMPTY_METRIC },
470
+ weekly7d: { ...EMPTY_METRIC },
471
+ weekly7dSonnet: { ...EMPTY_MODEL_METRIC },
472
+ weekly7dFable: { ...EMPTY_MODEL_METRIC },
473
+ rejected: { ...EMPTY_METRIC }
474
+ };
475
+ }
476
+ const usage = state.usage[accountName];
477
+ let updated = false;
478
+ const warnings = [];
479
+ const limits = response.limits ?? [];
480
+ for (const limit of limits) {
481
+ if (limit.kind !== "weekly_scoped")
482
+ continue;
483
+ if (!limit.scope?.model?.display_name)
484
+ continue;
485
+ const displayName = limit.scope.model.display_name;
486
+ const field = mapModelDisplayNameToField(displayName);
487
+ if (!field) {
488
+ warnings.push(`unknown model "${displayName}" in weekly_scoped limit`);
489
+ continue;
490
+ }
491
+ const utilization = percentToFraction(limit.percent);
492
+ const reset = isoToEpochSeconds(limit.resets_at);
493
+ usage[field] = {
494
+ utilization,
495
+ reset,
496
+ status: limit.severity || ""
497
+ };
498
+ updated = true;
499
+ }
500
+ if (updated) {
501
+ usage.timestamp = new Date().toISOString();
502
+ }
503
+ return { updated, warnings };
504
+ }
425
505
 
426
506
  // src/rotation.ts
427
507
  function isTemporarilyUnavailable(state, accountName) {
@@ -474,7 +554,7 @@ function isRejectionRelevant(prefix, modelFamily) {
474
554
  function metricEntries(usage, thresholds, modelFamily) {
475
555
  return Object.keys(METRIC_MODEL_FAMILY).filter((key) => isMetricRelevant(key, modelFamily)).map((key) => ({
476
556
  name: key,
477
- util: usage[key].utilization,
557
+ util: usage[key].utilization ?? 0,
478
558
  threshold: thresholds[key]
479
559
  }));
480
560
  }
@@ -606,7 +686,7 @@ function tokenExpiryDescription(account, nowMs) {
606
686
  function deriveAccountHealth(account, usage, thresholds, cooldownUntil, nowMs = Date.now()) {
607
687
  const availability = Object.fromEntries(MODEL_FAMILIES.map((family) => [family, true]));
608
688
  const reasons = [];
609
- const metrics = usage ? Object.keys(METRIC_MODEL_FAMILY).filter((metric) => thresholds[metric] > 0).map((metric) => ({
689
+ const metrics = usage ? Object.keys(METRIC_MODEL_FAMILY).filter((metric) => thresholds[metric] > 0).filter((metric) => usage[metric].utilization !== null).map((metric) => ({
610
690
  metric,
611
691
  utilization: usage[metric].utilization,
612
692
  threshold: thresholds[metric],
@@ -801,6 +881,13 @@ function progressBar(value, threshold, width = 30) {
801
881
  const label = `${(value * 100).toFixed(1)}%`;
802
882
  return `${bar} ${color}${label}${RESET}`;
803
883
  }
884
+ function formatMetric(value, threshold) {
885
+ if (value === null) {
886
+ const emptyBar = DIM + "\u2591".repeat(30) + RESET;
887
+ return `${emptyBar} ${DIM}n/a${RESET}`;
888
+ }
889
+ return progressBar(value, threshold);
890
+ }
804
891
  function resetSuffix(metric, now) {
805
892
  if (!metric.reset || metric.reset * 1000 <= now)
806
893
  return "";
@@ -992,6 +1079,17 @@ function buildBillingHeader(messageText) {
992
1079
  const cch = computeCCH(messageText);
993
1080
  return "x-anthropic-billing-header: " + `cc_version=${CLAUDE_CODE_VERSION}.${suffix}; ` + `cc_entrypoint=sdk-cli; ` + `cch=${cch};`;
994
1081
  }
1082
+ async function fetchAndApplyOAuthUsage(account, state, result) {
1083
+ const oauthResult = await fetchOAuthUsage(account.access);
1084
+ if (!oauthResult.ok) {
1085
+ result.oauthUsageWarnings = [`oauth usage: ${oauthResult.error}`];
1086
+ return;
1087
+ }
1088
+ const { warnings } = updateUsageFromOAuthEndpoint(state, account.name, oauthResult.data);
1089
+ if (warnings.length > 0) {
1090
+ result.oauthUsageWarnings = warnings;
1091
+ }
1092
+ }
995
1093
  async function probeAccount(account, state) {
996
1094
  const result = { name: account.name, success: false };
997
1095
  if (!account.access || account.expires <= Date.now()) {
@@ -1047,12 +1145,14 @@ async function probeAccount(account, state) {
1047
1145
  result.success = true;
1048
1146
  result.model = model;
1049
1147
  clearAuthFailure(state, account.name);
1148
+ await fetchAndApplyOAuthUsage(account, state, result);
1050
1149
  return result;
1051
1150
  }
1052
1151
  if (response.ok || updated) {
1053
1152
  result.success = true;
1054
1153
  result.model = model;
1055
1154
  clearAuthFailure(state, account.name);
1155
+ await fetchAndApplyOAuthUsage(account, state, result);
1056
1156
  return result;
1057
1157
  }
1058
1158
  lastBody = await response.text().catch(() => "");
@@ -1126,10 +1226,10 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
1126
1226
  const tag = isActive ? `${GREEN} [ACTIVE]${RESET}` : isCooling ? `${RED} [COOLDOWN]${RESET}` : "";
1127
1227
  console.log(` ${BOLD}${account.name}${RESET}${tag}`);
1128
1228
  if (usage) {
1129
- console.log(` 5h session: ${progressBar(usage.session5h.utilization, thresholds.session5h)}${resetSuffix(usage.session5h, now)}`);
1130
- console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}${resetSuffix(usage.weekly7d, now)}`);
1131
- console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}${resetSuffix(usage.weekly7dSonnet, now)}`);
1132
- console.log(` 7d fable: ${progressBar(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}${resetSuffix(usage.weekly7dFable, now)}`);
1229
+ console.log(` 5h session: ${formatMetric(usage.session5h.utilization, thresholds.session5h)}${resetSuffix(usage.session5h, now)}`);
1230
+ console.log(` 7d weekly: ${formatMetric(usage.weekly7d.utilization, thresholds.weekly7d)}${resetSuffix(usage.weekly7d, now)}`);
1231
+ console.log(` 7d sonnet: ${formatMetric(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}${resetSuffix(usage.weekly7dSonnet, now)}`);
1232
+ console.log(` 7d fable: ${formatMetric(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}${resetSuffix(usage.weekly7dFable, now)}`);
1133
1233
  const rejection = deriveAccountHealth(account, usage, thresholds, state.authFailures[account.name], now).reasons.find((reason) => reason.kind === "rejection");
1134
1234
  if (rejection) {
1135
1235
  const reset = rejection.reset ? ` \u2014 resets ${formatResetTime(rejection.reset, now)}` : "";
package/dist/index.js CHANGED
@@ -571,11 +571,12 @@ async function refreshAccountToken(refreshTokenValue) {
571
571
 
572
572
  // src/state.ts
573
573
  var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
574
+ var EMPTY_MODEL_METRIC = { utilization: null, reset: 0, status: "" };
574
575
  var EMPTY_USAGE = {
575
576
  session5h: { ...EMPTY_METRIC },
576
577
  weekly7d: { ...EMPTY_METRIC },
577
- weekly7dSonnet: { ...EMPTY_METRIC },
578
- weekly7dFable: { ...EMPTY_METRIC },
578
+ weekly7dSonnet: { ...EMPTY_MODEL_METRIC },
579
+ weekly7dFable: { ...EMPTY_MODEL_METRIC },
579
580
  rejected: { ...EMPTY_METRIC }
580
581
  };
581
582
  function defaultState() {
@@ -825,7 +826,7 @@ function isRejectionRelevant(prefix, modelFamily) {
825
826
  function metricEntries(usage, thresholds, modelFamily) {
826
827
  return Object.keys(METRIC_MODEL_FAMILY).filter((key) => isMetricRelevant(key, modelFamily)).map((key) => ({
827
828
  name: key,
828
- util: usage[key].utilization,
829
+ util: usage[key].utilization ?? 0,
829
830
  threshold: thresholds[key]
830
831
  }));
831
832
  }
@@ -1228,6 +1229,6 @@ var AuthSwitcherPlugin = async ({ client }) => {
1228
1229
  };
1229
1230
  var src_default = AuthSwitcherPlugin;
1230
1231
  export {
1231
- src_default as default,
1232
- AuthSwitcherPlugin
1232
+ AuthSwitcherPlugin,
1233
+ src_default as default
1233
1234
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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",
@@ -17,7 +17,7 @@
17
17
  "@opencode-ai/plugin": "*"
18
18
  },
19
19
  "dependencies": {
20
- "@ex-machina/opencode-anthropic-auth": "1.8.1"
20
+ "@ex-machina/opencode-anthropic-auth": "1.8.2"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@opencode-ai/plugin": "latest",