oc-auth-switcher 0.9.0 → 0.9.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/dist/cli.js +108 -8
- package/dist/index.js +6 -5
- package/package.json +2 -2
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: { ...
|
|
213
|
-
weekly7dFable: { ...
|
|
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: ${
|
|
1130
|
-
console.log(` 7d weekly: ${
|
|
1131
|
-
console.log(` 7d sonnet: ${
|
|
1132
|
-
console.log(` 7d fable: ${
|
|
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: { ...
|
|
578
|
-
weekly7dFable: { ...
|
|
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
|
-
|
|
1232
|
-
|
|
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.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@opencode-ai/plugin": "*"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@ex-machina/opencode-anthropic-auth": "1.8.
|
|
20
|
+
"@ex-machina/opencode-anthropic-auth": "1.8.3"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@opencode-ai/plugin": "latest",
|