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 +64 -28
- package/dist/cli.js +1 -1
- package/dist/index.js +60 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
# oc-auth-switcher
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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.
|
|
10
|
-
2.
|
|
11
|
-
3.
|
|
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.
|
|
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
|
-
**
|
|
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.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
oc-auth-switcher ping
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### 4. Use OpenCode normally
|
|
38
|
+
### 3. Use OpenCode normally
|
|
51
39
|
|
|
52
|
-
|
|
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
|
-
| `
|
|
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:
|
|
71
|
-
oc-auth-switcher config --threshold 0.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
694
|
-
|
|
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:
|
|
730
|
+
account: best,
|
|
719
731
|
switched: true,
|
|
720
|
-
reason: `Primary exceeded threshold
|
|
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) {
|