oc-auth-switcher 0.8.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 +359 -8
- package/dist/index.js +6 -5
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
|
|
4
7
|
// src/accounts.ts
|
|
5
8
|
import fs from "fs";
|
|
6
9
|
import path2 from "path";
|
|
@@ -203,11 +206,12 @@ async function refreshAccountToken(refreshTokenValue) {
|
|
|
203
206
|
|
|
204
207
|
// src/state.ts
|
|
205
208
|
var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
|
|
209
|
+
var EMPTY_MODEL_METRIC = { utilization: null, reset: 0, status: "" };
|
|
206
210
|
var EMPTY_USAGE = {
|
|
207
211
|
session5h: { ...EMPTY_METRIC },
|
|
208
212
|
weekly7d: { ...EMPTY_METRIC },
|
|
209
|
-
weekly7dSonnet: { ...
|
|
210
|
-
weekly7dFable: { ...
|
|
213
|
+
weekly7dSonnet: { ...EMPTY_MODEL_METRIC },
|
|
214
|
+
weekly7dFable: { ...EMPTY_MODEL_METRIC },
|
|
211
215
|
rejected: { ...EMPTY_METRIC }
|
|
212
216
|
};
|
|
213
217
|
function defaultState() {
|
|
@@ -260,6 +264,20 @@ function loadState(stateFile = STATE_FILE) {
|
|
|
260
264
|
function saveState(state) {
|
|
261
265
|
return safeWriteJSON(STATE_FILE, state);
|
|
262
266
|
}
|
|
267
|
+
async function saveStateMerged(state, accountsToMerge) {
|
|
268
|
+
const onDisk = loadState();
|
|
269
|
+
for (const name of accountsToMerge) {
|
|
270
|
+
if (state.usage[name]) {
|
|
271
|
+
onDisk.usage[name] = state.usage[name];
|
|
272
|
+
}
|
|
273
|
+
if (state.authFailures[name] !== undefined) {
|
|
274
|
+
onDisk.authFailures[name] = state.authFailures[name];
|
|
275
|
+
} else {
|
|
276
|
+
delete onDisk.authFailures[name];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
await safeWriteJSON(STATE_FILE, onDisk);
|
|
280
|
+
}
|
|
263
281
|
function getThresholds(config) {
|
|
264
282
|
if (typeof config.threshold === "number") {
|
|
265
283
|
return {
|
|
@@ -312,6 +330,178 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
312
330
|
}
|
|
313
331
|
}
|
|
314
332
|
}
|
|
333
|
+
function updateUsageFromHeaders(state, accountName, headers) {
|
|
334
|
+
if (!state.usage[accountName]) {
|
|
335
|
+
state.usage[accountName] = {
|
|
336
|
+
session5h: { ...EMPTY_METRIC },
|
|
337
|
+
weekly7d: { ...EMPTY_METRIC },
|
|
338
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
339
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
340
|
+
rejected: { ...EMPTY_METRIC }
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const usage = state.usage[accountName];
|
|
344
|
+
let updated = false;
|
|
345
|
+
const metricFamilies = [
|
|
346
|
+
{
|
|
347
|
+
key: "session5h",
|
|
348
|
+
prefix: "anthropic-ratelimit-unified-5h"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
key: "weekly7d",
|
|
352
|
+
prefix: "anthropic-ratelimit-unified-7d"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
key: "weekly7dSonnet",
|
|
356
|
+
prefix: "anthropic-ratelimit-unified-7d_sonnet"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
key: "weekly7dFable",
|
|
360
|
+
prefix: "anthropic-ratelimit-unified-7d_fable"
|
|
361
|
+
}
|
|
362
|
+
];
|
|
363
|
+
for (const { key, prefix } of metricFamilies) {
|
|
364
|
+
const utilHeader = headers.get(`${prefix}-utilization`);
|
|
365
|
+
const resetHeader = headers.get(`${prefix}-reset`);
|
|
366
|
+
const statusHeader = headers.get(`${prefix}-status`);
|
|
367
|
+
if (utilHeader !== null || resetHeader !== null || statusHeader !== null) {
|
|
368
|
+
if (utilHeader !== null) {
|
|
369
|
+
const val = parseFloat(utilHeader);
|
|
370
|
+
if (!isNaN(val)) {
|
|
371
|
+
usage[key].utilization = val;
|
|
372
|
+
updated = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (resetHeader !== null) {
|
|
376
|
+
const val = Number(resetHeader);
|
|
377
|
+
if (!isNaN(val)) {
|
|
378
|
+
usage[key].reset = val;
|
|
379
|
+
updated = true;
|
|
380
|
+
} else {
|
|
381
|
+
const parsed = Date.parse(resetHeader);
|
|
382
|
+
if (!isNaN(parsed)) {
|
|
383
|
+
usage[key].reset = parsed / 1000;
|
|
384
|
+
updated = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (statusHeader !== null) {
|
|
389
|
+
usage[key].status = statusHeader;
|
|
390
|
+
updated = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const [headerName, headerValue] of headers.entries()) {
|
|
395
|
+
const normalizedName = headerName.toLowerCase();
|
|
396
|
+
const statusMatch = normalizedName.match(/^anthropic-ratelimit-unified-(.+)-status$/);
|
|
397
|
+
if (!statusMatch || !quotaWindowScope(statusMatch[1]) || headerValue.toLowerCase() !== "rejected") {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const prefix = normalizedName.slice(0, -"-status".length);
|
|
401
|
+
const resetHeader = headers.get(`${prefix}-reset`) ?? headers.get("anthropic-ratelimit-unified-reset");
|
|
402
|
+
let reset = resetHeader ? Number(resetHeader) : NaN;
|
|
403
|
+
if (isNaN(reset) && resetHeader) {
|
|
404
|
+
const parsed = Date.parse(resetHeader);
|
|
405
|
+
if (!isNaN(parsed))
|
|
406
|
+
reset = parsed / 1000;
|
|
407
|
+
}
|
|
408
|
+
if (!Number.isFinite(reset) || reset <= 0) {
|
|
409
|
+
const retryAfterHeader = headers.get("retry-after");
|
|
410
|
+
const retryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
|
|
411
|
+
reset = Date.now() / 1000 + (Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : REJECTION_FALLBACK_SECONDS);
|
|
412
|
+
}
|
|
413
|
+
usage.rejected = {
|
|
414
|
+
utilization: 1,
|
|
415
|
+
reset: Math.max(usage.rejected.reset, reset),
|
|
416
|
+
status: "rejected",
|
|
417
|
+
prefix
|
|
418
|
+
};
|
|
419
|
+
updated = true;
|
|
420
|
+
}
|
|
421
|
+
if (updated) {
|
|
422
|
+
usage.timestamp = new Date().toISOString();
|
|
423
|
+
}
|
|
424
|
+
return updated;
|
|
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
|
+
}
|
|
315
505
|
|
|
316
506
|
// src/rotation.ts
|
|
317
507
|
function isTemporarilyUnavailable(state, accountName) {
|
|
@@ -364,7 +554,7 @@ function isRejectionRelevant(prefix, modelFamily) {
|
|
|
364
554
|
function metricEntries(usage, thresholds, modelFamily) {
|
|
365
555
|
return Object.keys(METRIC_MODEL_FAMILY).filter((key) => isMetricRelevant(key, modelFamily)).map((key) => ({
|
|
366
556
|
name: key,
|
|
367
|
-
util: usage[key].utilization,
|
|
557
|
+
util: usage[key].utilization ?? 0,
|
|
368
558
|
threshold: thresholds[key]
|
|
369
559
|
}));
|
|
370
560
|
}
|
|
@@ -434,6 +624,9 @@ function selectAccount(accounts, state, model) {
|
|
|
434
624
|
}
|
|
435
625
|
return { account: current, switched: false };
|
|
436
626
|
}
|
|
627
|
+
function markAuthFailure(state, accountName) {
|
|
628
|
+
state.authFailures[accountName] = Date.now() + AUTH_FAILURE_COOLDOWN;
|
|
629
|
+
}
|
|
437
630
|
function clearAuthFailure(state, accountName) {
|
|
438
631
|
delete state.authFailures[accountName];
|
|
439
632
|
}
|
|
@@ -493,7 +686,7 @@ function tokenExpiryDescription(account, nowMs) {
|
|
|
493
686
|
function deriveAccountHealth(account, usage, thresholds, cooldownUntil, nowMs = Date.now()) {
|
|
494
687
|
const availability = Object.fromEntries(MODEL_FAMILIES.map((family) => [family, true]));
|
|
495
688
|
const reasons = [];
|
|
496
|
-
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) => ({
|
|
497
690
|
metric,
|
|
498
691
|
utilization: usage[metric].utilization,
|
|
499
692
|
threshold: thresholds[metric],
|
|
@@ -688,6 +881,13 @@ function progressBar(value, threshold, width = 30) {
|
|
|
688
881
|
const label = `${(value * 100).toFixed(1)}%`;
|
|
689
882
|
return `${bar} ${color}${label}${RESET}`;
|
|
690
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
|
+
}
|
|
691
891
|
function resetSuffix(metric, now) {
|
|
692
892
|
if (!metric.reset || metric.reset * 1000 <= now)
|
|
693
893
|
return "";
|
|
@@ -854,6 +1054,145 @@ async function refreshExpiredAccounts(data, state) {
|
|
|
854
1054
|
}
|
|
855
1055
|
}
|
|
856
1056
|
}
|
|
1057
|
+
var PROBE_CONCURRENCY = 3;
|
|
1058
|
+
var PROBE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
|
|
1059
|
+
var PROBE_MODELS = [
|
|
1060
|
+
"claude-sonnet-4-5",
|
|
1061
|
+
"claude-opus-4-5"
|
|
1062
|
+
];
|
|
1063
|
+
var PROBE_BETA_HEADERS = "oauth-2025-04-20,interleaved-thinking-2025-05-14";
|
|
1064
|
+
var PROBE_USER_AGENT = "claude-cli/2.1.87 (external, cli)";
|
|
1065
|
+
var PROBE_VERSION = "2023-06-01";
|
|
1066
|
+
var CLAUDE_CODE_VERSION = "2.1.87";
|
|
1067
|
+
var CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
1068
|
+
var CCH_SALT = "59cf53e54c78";
|
|
1069
|
+
var CCH_POSITIONS = [4, 7, 20];
|
|
1070
|
+
function computeCCH(messageText) {
|
|
1071
|
+
return createHash("sha256").update(messageText).digest("hex").slice(0, 5);
|
|
1072
|
+
}
|
|
1073
|
+
function computeVersionSuffix(messageText) {
|
|
1074
|
+
const chars = CCH_POSITIONS.map((i) => messageText[i] || "0").join("");
|
|
1075
|
+
return createHash("sha256").update(`${CCH_SALT}${chars}${CLAUDE_CODE_VERSION}`).digest("hex").slice(0, 3);
|
|
1076
|
+
}
|
|
1077
|
+
function buildBillingHeader(messageText) {
|
|
1078
|
+
const suffix = computeVersionSuffix(messageText);
|
|
1079
|
+
const cch = computeCCH(messageText);
|
|
1080
|
+
return "x-anthropic-billing-header: " + `cc_version=${CLAUDE_CODE_VERSION}.${suffix}; ` + `cc_entrypoint=sdk-cli; ` + `cch=${cch};`;
|
|
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
|
+
}
|
|
1093
|
+
async function probeAccount(account, state) {
|
|
1094
|
+
const result = { name: account.name, success: false };
|
|
1095
|
+
if (!account.access || account.expires <= Date.now()) {
|
|
1096
|
+
if (!account.refresh) {
|
|
1097
|
+
result.error = "missing refresh token";
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
const refreshResult = await refreshAccountToken(account.refresh);
|
|
1101
|
+
if (!refreshResult.ok || !refreshResult.access) {
|
|
1102
|
+
result.error = `token refresh failed: ${refreshResult.error || "unknown"}`;
|
|
1103
|
+
markAuthFailure(state, account.name);
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
account.access = refreshResult.access;
|
|
1107
|
+
account.refresh = refreshResult.refresh;
|
|
1108
|
+
account.expires = refreshResult.expires;
|
|
1109
|
+
await updateAccountTokens(account.name, refreshResult.access, refreshResult.refresh, refreshResult.expires);
|
|
1110
|
+
clearAuthFailure(state, account.name);
|
|
1111
|
+
}
|
|
1112
|
+
let lastStatus = 0;
|
|
1113
|
+
let lastBody = "";
|
|
1114
|
+
for (const model of PROBE_MODELS) {
|
|
1115
|
+
try {
|
|
1116
|
+
const messageText = ".";
|
|
1117
|
+
const billingHeader = buildBillingHeader(messageText);
|
|
1118
|
+
const response = await fetch(PROBE_API_URL, {
|
|
1119
|
+
method: "POST",
|
|
1120
|
+
headers: {
|
|
1121
|
+
authorization: `Bearer ${account.access}`,
|
|
1122
|
+
"anthropic-beta": PROBE_BETA_HEADERS,
|
|
1123
|
+
"anthropic-version": PROBE_VERSION,
|
|
1124
|
+
"user-agent": PROBE_USER_AGENT,
|
|
1125
|
+
"content-type": "application/json"
|
|
1126
|
+
},
|
|
1127
|
+
body: JSON.stringify({
|
|
1128
|
+
model,
|
|
1129
|
+
max_tokens: 1,
|
|
1130
|
+
system: [
|
|
1131
|
+
{ type: "text", text: billingHeader },
|
|
1132
|
+
{ type: "text", text: CLAUDE_CODE_IDENTITY }
|
|
1133
|
+
],
|
|
1134
|
+
messages: [{ role: "user", content: messageText }]
|
|
1135
|
+
})
|
|
1136
|
+
});
|
|
1137
|
+
lastStatus = response.status;
|
|
1138
|
+
const updated = updateUsageFromHeaders(state, account.name, response.headers);
|
|
1139
|
+
if (response.status === 401 || response.status === 403) {
|
|
1140
|
+
result.error = `auth error ${response.status}`;
|
|
1141
|
+
markAuthFailure(state, account.name);
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
if (response.status === 429 && updated) {
|
|
1145
|
+
result.success = true;
|
|
1146
|
+
result.model = model;
|
|
1147
|
+
clearAuthFailure(state, account.name);
|
|
1148
|
+
await fetchAndApplyOAuthUsage(account, state, result);
|
|
1149
|
+
return result;
|
|
1150
|
+
}
|
|
1151
|
+
if (response.ok || updated) {
|
|
1152
|
+
result.success = true;
|
|
1153
|
+
result.model = model;
|
|
1154
|
+
clearAuthFailure(state, account.name);
|
|
1155
|
+
await fetchAndApplyOAuthUsage(account, state, result);
|
|
1156
|
+
return result;
|
|
1157
|
+
}
|
|
1158
|
+
lastBody = await response.text().catch(() => "");
|
|
1159
|
+
if (response.status === 400 && lastBody.includes("model")) {
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
1164
|
+
return result;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
result.error = `HTTP ${lastStatus}: ${lastBody.slice(0, 100)}`;
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
async function probeAllAccounts(accounts) {
|
|
1171
|
+
const state = loadState();
|
|
1172
|
+
const results = [];
|
|
1173
|
+
const probeErrors = [];
|
|
1174
|
+
for (let i = 0;i < accounts.length; i += PROBE_CONCURRENCY) {
|
|
1175
|
+
const batch = accounts.slice(i, i + PROBE_CONCURRENCY);
|
|
1176
|
+
const batchResults = await Promise.allSettled(batch.map((account) => probeAccount(account, state)));
|
|
1177
|
+
for (const settled of batchResults) {
|
|
1178
|
+
if (settled.status === "fulfilled") {
|
|
1179
|
+
results.push(settled.value);
|
|
1180
|
+
if (!settled.value.success && settled.value.error) {
|
|
1181
|
+
probeErrors.push(`${settled.value.name}: ${settled.value.error}`);
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
probeErrors.push(`unexpected error: ${settled.reason}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
await saveStateMerged(state, accounts.map((a) => a.name));
|
|
1189
|
+
if (probeErrors.length > 0) {
|
|
1190
|
+
for (const err of probeErrors) {
|
|
1191
|
+
console.log(` ${YELLOW}Warning: probe failed for ${err}${RESET}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return { state, results };
|
|
1195
|
+
}
|
|
857
1196
|
async function cmdUsage(args) {
|
|
858
1197
|
const watch = args.includes("--watch") || args.includes("-w");
|
|
859
1198
|
const showUsage = async () => {
|
|
@@ -887,10 +1226,10 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
887
1226
|
const tag = isActive ? `${GREEN} [ACTIVE]${RESET}` : isCooling ? `${RED} [COOLDOWN]${RESET}` : "";
|
|
888
1227
|
console.log(` ${BOLD}${account.name}${RESET}${tag}`);
|
|
889
1228
|
if (usage) {
|
|
890
|
-
console.log(` 5h session: ${
|
|
891
|
-
console.log(` 7d weekly: ${
|
|
892
|
-
console.log(` 7d sonnet: ${
|
|
893
|
-
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)}`);
|
|
894
1233
|
const rejection = deriveAccountHealth(account, usage, thresholds, state.authFailures[account.name], now).reasons.find((reason) => reason.kind === "rejection");
|
|
895
1234
|
if (rejection) {
|
|
896
1235
|
const reset = rejection.reset ? ` \u2014 resets ${formatResetTime(rejection.reset, now)}` : "";
|
|
@@ -909,6 +1248,18 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
909
1248
|
console.log();
|
|
910
1249
|
}
|
|
911
1250
|
};
|
|
1251
|
+
const accountsData = loadAccounts();
|
|
1252
|
+
if (accountsData.accounts.length > 0) {
|
|
1253
|
+
process.stdout.write(`${DIM} Probing ${accountsData.accounts.length} account(s) for current usage...${RESET}`);
|
|
1254
|
+
const { results } = await probeAllAccounts(accountsData.accounts);
|
|
1255
|
+
const successCount = results.filter((r) => r.success).length;
|
|
1256
|
+
const models = [...new Set(results.filter((r) => r.model).map((r) => r.model))];
|
|
1257
|
+
process.stdout.write(`\r\x1B[K`);
|
|
1258
|
+
if (successCount < accountsData.accounts.length) {
|
|
1259
|
+
console.log(`${DIM} Probed ${successCount}/${accountsData.accounts.length} accounts successfully${models.length ? ` (${models.join(", ")})` : ""}${RESET}
|
|
1260
|
+
`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
912
1263
|
await showUsage();
|
|
913
1264
|
if (watch) {
|
|
914
1265
|
console.log(`${DIM}Refreshing every 5 seconds. Press Ctrl+C to stop.${RESET}
|
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.
|
|
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.
|
|
20
|
+
"@ex-machina/opencode-anthropic-auth": "1.8.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@opencode-ai/plugin": "latest",
|