oc-auth-switcher 0.1.0 → 0.1.2

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 CHANGED
@@ -1,37 +1,31 @@
1
1
  # oc-auth-switcher
2
2
 
3
- Multi-account rotation companion plugin for OpenCode. Works alongside `@ex-machina/opencode-anthropic-auth` to automatically rotate between multiple Anthropic Claude Max accounts based on utilization thresholds.
3
+ OpenCode auth plugin for multi-account Anthropic Claude Max rotation with automatic failover. Captures rate-limit headers in real-time and switches accounts before you hit usage limits.
4
4
 
5
5
  ## How It Works
6
6
 
7
- This plugin does **not** replace your auth plugin. Instead it:
7
+ This plugin registers as the Anthropic auth provider for OpenCode. It uses [`@ex-machina/opencode-anthropic-auth`](https://github.com/ex-machina-co/opencode-anthropic-auth) as a library for request/response transformation (billing header, system prompt sanitization, tool name prefixing) and adds:
8
8
 
9
- 1. Maintains a pool of Anthropic OAuth accounts in a separate file
10
- 2. Monitors utilization metrics via CLI `ping` commands
11
- 3. Rotates the active account by writing credentials to `auth.json` via the OpenCode SDK
12
- 4. `@ex-machina/opencode-anthropic-auth` reads the rotated credentials on the next API request
13
-
14
- The plugin also hooks into OpenCode's `session.idle` and `chat.message` events to check cached metrics and rotate proactively before thresholds are hit.
9
+ 1. **Multi-account rotation** — maintains a pool of OAuth accounts and selects the best one on each request
10
+ 2. **Real-time metric capture** reads Anthropic's rate-limit response headers on every API call (no manual pinging needed)
11
+ 3. **Automatic failover** when any utilization metric exceeds the threshold (default 90%), the next request automatically uses a different account
15
12
 
16
13
  ## Setup
17
14
 
18
- ### 1. Install both plugins
15
+ ### 1. Add the plugin to your `opencode.json`
19
16
 
20
17
  ```json
21
18
  {
22
- "plugin": [
23
- "@ex-machina/opencode-anthropic-auth@1.8.0",
24
- "oc-auth-switcher@latest"
25
- ]
19
+ "plugin": ["oc-auth-switcher@latest"]
26
20
  }
27
21
  ```
28
22
 
29
- **Important:** `@ex-machina/opencode-anthropic-auth` must be listed first.
23
+ This is the **only** auth plugin you need for Anthropic. Do not also list `@ex-machina/opencode-anthropic-auth` it is included as a dependency.
30
24
 
31
25
  ### 2. Add accounts
32
26
 
33
27
  ```bash
34
- # Add your primary account
28
+ # Add your primary account (first account = always preferred)
35
29
  oc-auth-switcher add primary
36
30
 
37
31
  # Add fallback accounts
@@ -41,34 +35,31 @@ oc-auth-switcher add fallback-2
41
35
 
42
36
  Each `add` command runs an OAuth flow — you'll be given a URL to open in your browser and prompted to paste the callback.
43
37
 
44
- ### 3. Ping to capture initial metrics
45
-
46
- ```bash
47
- oc-auth-switcher ping
48
- ```
49
-
50
- ### 4. Use OpenCode normally
38
+ ### 3. Use OpenCode normally
51
39
 
52
- The plugin handles rotation automatically. The first account in the pool is always the "primary" (preferred). When it exceeds the utilization threshold, traffic switches to the next available fallback. When the primary recovers, it switches back.
40
+ Metrics update automatically on every API request. When the primary account exceeds the utilization threshold, the plugin switches to the next available fallback. When the primary recovers, it switches back.
53
41
 
54
42
  ## CLI Commands
55
43
 
44
+ ```
45
+ oc-auth-switcher <command> [options]
46
+ ```
47
+
56
48
  | Command | Description |
57
49
  |---------|-------------|
58
50
  | `add [name]` | Add a new account via OAuth |
59
- | `remove <name>` | Remove an account from the pool |
51
+ | `reauth <name>` | Re-authenticate an existing account |
60
52
  | `usage [--watch]` | Show utilization dashboard with progress bars |
61
53
  | `config [options]` | View/modify thresholds and check interval |
62
- | `ping [name]` | Ping account(s) to refresh utilization metrics |
63
- | `reauth <name>` | Re-authenticate an existing account |
64
54
  | `switch <name>` | Manually switch to a specific account |
65
55
  | `status` | Show current active account and rotation state |
56
+ | `remove <name>` | Remove an account from the pool |
66
57
 
67
58
  ## Configuration
68
59
 
69
60
  ```bash
70
- # Set uniform threshold (default: 70%)
71
- oc-auth-switcher config --threshold 0.80
61
+ # Set uniform threshold (default: 90%)
62
+ oc-auth-switcher config --threshold 0.90
72
63
 
73
64
  # Set per-metric thresholds (5h, 7d, 7d-sonnet)
74
65
  oc-auth-switcher config --thresholds 90,80,70
@@ -96,6 +87,21 @@ Both files use atomic writes with `.bak` fallback for crash safety.
96
87
  - If all fallbacks are also over threshold, pick the one with the lowest utilization score
97
88
  - When on a fallback, periodically check if the primary has recovered (configurable interval)
98
89
  - Auth failures trigger a 1-hour cooldown per account
90
+ - Token refresh is handled automatically with retry and fallback to other accounts
91
+
92
+ ## Running the CLI
93
+
94
+ When installed via npm (as part of the OpenCode plugin):
95
+
96
+ ```bash
97
+ npx oc-auth-switcher <command>
98
+ ```
99
+
100
+ From the project directory (development):
101
+
102
+ ```bash
103
+ bun dist/cli.js <command>
104
+ ```
99
105
 
100
106
  ## Building from Source
101
107
 
@@ -104,6 +110,36 @@ bun install
104
110
  bun run build
105
111
  ```
106
112
 
113
+ ## Publishing to npm
114
+
115
+ ```bash
116
+ npm login
117
+ npm publish
118
+ ```
119
+
120
+ The `prepublishOnly` script automatically runs `bun run build` before publishing.
121
+
122
+ ### Versioning
123
+
124
+ ```bash
125
+ # Patch (bug fixes)
126
+ npm version patch && npm publish && git push --follow-tags
127
+
128
+ # Minor (new features)
129
+ npm version minor && npm publish && git push --follow-tags
130
+
131
+ # Major (breaking changes)
132
+ npm version major && npm publish && git push --follow-tags
133
+ ```
134
+
135
+ ### Updating the ex-machina dependency
136
+
137
+ ```bash
138
+ bun update @ex-machina/opencode-anthropic-auth
139
+ bun run build
140
+ # Test, then publish
141
+ ```
142
+
107
143
  ## License
108
144
 
109
145
  MIT
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ 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.9;
15
15
  var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
16
- var AUTH_FAILURE_COOLDOWN = 60 * 60 * 1000;
16
+ var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
17
17
 
18
18
  // src/accounts.ts
19
19
  function normalizeAccount(raw) {
package/dist/index.js CHANGED
@@ -415,7 +415,7 @@ 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.9;
417
417
  var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
418
- var AUTH_FAILURE_COOLDOWN = 60 * 60 * 1000;
418
+ var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
419
419
 
420
420
  // src/accounts.ts
421
421
  function normalizeAccount(raw) {
@@ -659,10 +659,44 @@ function getEarliestReset(usage) {
659
659
  ].filter((r) => r > 0);
660
660
  return resets.length > 0 ? Math.min(...resets) * 1000 : 0;
661
661
  }
662
+ function purgeExpiredCooldowns(state) {
663
+ const now = Date.now();
664
+ for (const name of Object.keys(state.authFailures)) {
665
+ if (state.authFailures[name] <= now) {
666
+ delete state.authFailures[name];
667
+ }
668
+ }
669
+ }
670
+ function findBestAvailable(candidates, state, exclude) {
671
+ for (const acct of candidates) {
672
+ if (exclude.has(acct.name))
673
+ continue;
674
+ if (isTemporarilyUnavailable(state, acct.name))
675
+ continue;
676
+ if (!isOverThreshold(state.usage[acct.name], state)) {
677
+ return acct;
678
+ }
679
+ }
680
+ let best = null;
681
+ let bestScore = Infinity;
682
+ for (const acct of candidates) {
683
+ if (exclude.has(acct.name))
684
+ continue;
685
+ if (isTemporarilyUnavailable(state, acct.name))
686
+ continue;
687
+ const score = getUtilizationScore(state.usage[acct.name], state);
688
+ if (score < bestScore) {
689
+ bestScore = score;
690
+ best = acct;
691
+ }
692
+ }
693
+ return best;
694
+ }
662
695
  function selectAccount(accounts, state) {
663
696
  if (accounts.length === 0) {
664
697
  throw new Error("No accounts available");
665
698
  }
699
+ purgeExpiredCooldowns(state);
666
700
  if (accounts.length === 1) {
667
701
  return { account: accounts[0], switched: false };
668
702
  }
@@ -690,40 +724,39 @@ function selectAccount(accounts, state) {
690
724
  if (isPrimary) {
691
725
  if (isOverThreshold(primaryUsage, state)) {
692
726
  const exceededMetric = getExceededMetric(primaryUsage, state);
693
- for (const fb of fallbacks) {
694
- if (isTemporarilyUnavailable(state, fb.name))
695
- continue;
696
- const fbUsage = state.usage[fb.name];
697
- if (!isOverThreshold(fbUsage, state)) {
698
- return {
699
- account: fb,
700
- switched: true,
701
- reason: `Primary exceeded ${exceededMetric} threshold — switching to ${fb.name}`
702
- };
703
- }
704
- }
705
- let bestFb = null;
706
- let bestScore = Infinity;
707
- for (const fb of fallbacks) {
708
- if (isTemporarilyUnavailable(state, fb.name))
709
- continue;
710
- const score = getUtilizationScore(state.usage[fb.name], state);
711
- if (score < bestScore) {
712
- bestScore = score;
713
- bestFb = fb;
714
- }
715
- }
716
- if (bestFb) {
727
+ const best = findBestAvailable(fallbacks, state, new Set);
728
+ if (best) {
717
729
  return {
718
- account: bestFb,
730
+ account: best,
719
731
  switched: true,
720
- reason: `Primary exceeded threshold, all fallbacks busy using least loaded: ${bestFb.name}`
732
+ reason: `Primary exceeded ${exceededMetric} threshold — switching to ${best.name}`
721
733
  };
722
734
  }
723
735
  return { account: primary, switched: false };
724
736
  }
725
737
  return { account: primary, switched: false };
726
738
  }
739
+ const currentOverThreshold = isOverThreshold(currentUsage, state);
740
+ const currentInCooldown = isTemporarilyUnavailable(state, current.name);
741
+ if (currentOverThreshold || currentInCooldown) {
742
+ const reason = currentInCooldown ? `${current.name} in auth-failure cooldown` : `${current.name} exceeded threshold`;
743
+ if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
744
+ return {
745
+ account: primary,
746
+ switched: true,
747
+ reason: `${reason} — switching back to primary`
748
+ };
749
+ }
750
+ const best = findBestAvailable(accounts, state, new Set([current.name]));
751
+ if (best && best.name !== current.name) {
752
+ return {
753
+ account: best,
754
+ switched: true,
755
+ reason: `${reason} — switching to ${best.name}`
756
+ };
757
+ }
758
+ return { account: current, switched: false };
759
+ }
727
760
  const now = Date.now();
728
761
  const checkInterval = state.config.checkInterval;
729
762
  const earliestReset = getEarliestReset(primaryUsage);
@@ -739,30 +772,6 @@ function selectAccount(accounts, state) {
739
772
  };
740
773
  }
741
774
  }
742
- if (!isTemporarilyUnavailable(state, current.name)) {
743
- return { account: current, switched: false };
744
- }
745
- for (const fb of fallbacks) {
746
- if (fb.name === current.name)
747
- continue;
748
- if (isTemporarilyUnavailable(state, fb.name))
749
- continue;
750
- const fbUsage = state.usage[fb.name];
751
- if (!isOverThreshold(fbUsage, state)) {
752
- return {
753
- account: fb,
754
- switched: true,
755
- reason: `Current account ${current.name} in cooldown — switching to ${fb.name}`
756
- };
757
- }
758
- }
759
- if (!isTemporarilyUnavailable(state, primary.name)) {
760
- return {
761
- account: primary,
762
- switched: true,
763
- reason: "All fallbacks unavailable — falling back to primary"
764
- };
765
- }
766
775
  return { account: current, switched: false };
767
776
  }
768
777
  function markAuthFailure(state, accountName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",