oc-auth-switcher 0.8.0 → 0.9.0
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 +251 -0
- package/package.json +1 -1
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";
|
|
@@ -260,6 +263,20 @@ function loadState(stateFile = STATE_FILE) {
|
|
|
260
263
|
function saveState(state) {
|
|
261
264
|
return safeWriteJSON(STATE_FILE, state);
|
|
262
265
|
}
|
|
266
|
+
async function saveStateMerged(state, accountsToMerge) {
|
|
267
|
+
const onDisk = loadState();
|
|
268
|
+
for (const name of accountsToMerge) {
|
|
269
|
+
if (state.usage[name]) {
|
|
270
|
+
onDisk.usage[name] = state.usage[name];
|
|
271
|
+
}
|
|
272
|
+
if (state.authFailures[name] !== undefined) {
|
|
273
|
+
onDisk.authFailures[name] = state.authFailures[name];
|
|
274
|
+
} else {
|
|
275
|
+
delete onDisk.authFailures[name];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
await safeWriteJSON(STATE_FILE, onDisk);
|
|
279
|
+
}
|
|
263
280
|
function getThresholds(config) {
|
|
264
281
|
if (typeof config.threshold === "number") {
|
|
265
282
|
return {
|
|
@@ -312,6 +329,99 @@ function ensureAccountsInState(state, accountNames) {
|
|
|
312
329
|
}
|
|
313
330
|
}
|
|
314
331
|
}
|
|
332
|
+
function updateUsageFromHeaders(state, accountName, headers) {
|
|
333
|
+
if (!state.usage[accountName]) {
|
|
334
|
+
state.usage[accountName] = {
|
|
335
|
+
session5h: { ...EMPTY_METRIC },
|
|
336
|
+
weekly7d: { ...EMPTY_METRIC },
|
|
337
|
+
weekly7dSonnet: { ...EMPTY_METRIC },
|
|
338
|
+
weekly7dFable: { ...EMPTY_METRIC },
|
|
339
|
+
rejected: { ...EMPTY_METRIC }
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const usage = state.usage[accountName];
|
|
343
|
+
let updated = false;
|
|
344
|
+
const metricFamilies = [
|
|
345
|
+
{
|
|
346
|
+
key: "session5h",
|
|
347
|
+
prefix: "anthropic-ratelimit-unified-5h"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
key: "weekly7d",
|
|
351
|
+
prefix: "anthropic-ratelimit-unified-7d"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "weekly7dSonnet",
|
|
355
|
+
prefix: "anthropic-ratelimit-unified-7d_sonnet"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
key: "weekly7dFable",
|
|
359
|
+
prefix: "anthropic-ratelimit-unified-7d_fable"
|
|
360
|
+
}
|
|
361
|
+
];
|
|
362
|
+
for (const { key, prefix } of metricFamilies) {
|
|
363
|
+
const utilHeader = headers.get(`${prefix}-utilization`);
|
|
364
|
+
const resetHeader = headers.get(`${prefix}-reset`);
|
|
365
|
+
const statusHeader = headers.get(`${prefix}-status`);
|
|
366
|
+
if (utilHeader !== null || resetHeader !== null || statusHeader !== null) {
|
|
367
|
+
if (utilHeader !== null) {
|
|
368
|
+
const val = parseFloat(utilHeader);
|
|
369
|
+
if (!isNaN(val)) {
|
|
370
|
+
usage[key].utilization = val;
|
|
371
|
+
updated = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (resetHeader !== null) {
|
|
375
|
+
const val = Number(resetHeader);
|
|
376
|
+
if (!isNaN(val)) {
|
|
377
|
+
usage[key].reset = val;
|
|
378
|
+
updated = true;
|
|
379
|
+
} else {
|
|
380
|
+
const parsed = Date.parse(resetHeader);
|
|
381
|
+
if (!isNaN(parsed)) {
|
|
382
|
+
usage[key].reset = parsed / 1000;
|
|
383
|
+
updated = true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (statusHeader !== null) {
|
|
388
|
+
usage[key].status = statusHeader;
|
|
389
|
+
updated = true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const [headerName, headerValue] of headers.entries()) {
|
|
394
|
+
const normalizedName = headerName.toLowerCase();
|
|
395
|
+
const statusMatch = normalizedName.match(/^anthropic-ratelimit-unified-(.+)-status$/);
|
|
396
|
+
if (!statusMatch || !quotaWindowScope(statusMatch[1]) || headerValue.toLowerCase() !== "rejected") {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const prefix = normalizedName.slice(0, -"-status".length);
|
|
400
|
+
const resetHeader = headers.get(`${prefix}-reset`) ?? headers.get("anthropic-ratelimit-unified-reset");
|
|
401
|
+
let reset = resetHeader ? Number(resetHeader) : NaN;
|
|
402
|
+
if (isNaN(reset) && resetHeader) {
|
|
403
|
+
const parsed = Date.parse(resetHeader);
|
|
404
|
+
if (!isNaN(parsed))
|
|
405
|
+
reset = parsed / 1000;
|
|
406
|
+
}
|
|
407
|
+
if (!Number.isFinite(reset) || reset <= 0) {
|
|
408
|
+
const retryAfterHeader = headers.get("retry-after");
|
|
409
|
+
const retryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
|
|
410
|
+
reset = Date.now() / 1000 + (Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : REJECTION_FALLBACK_SECONDS);
|
|
411
|
+
}
|
|
412
|
+
usage.rejected = {
|
|
413
|
+
utilization: 1,
|
|
414
|
+
reset: Math.max(usage.rejected.reset, reset),
|
|
415
|
+
status: "rejected",
|
|
416
|
+
prefix
|
|
417
|
+
};
|
|
418
|
+
updated = true;
|
|
419
|
+
}
|
|
420
|
+
if (updated) {
|
|
421
|
+
usage.timestamp = new Date().toISOString();
|
|
422
|
+
}
|
|
423
|
+
return updated;
|
|
424
|
+
}
|
|
315
425
|
|
|
316
426
|
// src/rotation.ts
|
|
317
427
|
function isTemporarilyUnavailable(state, accountName) {
|
|
@@ -434,6 +544,9 @@ function selectAccount(accounts, state, model) {
|
|
|
434
544
|
}
|
|
435
545
|
return { account: current, switched: false };
|
|
436
546
|
}
|
|
547
|
+
function markAuthFailure(state, accountName) {
|
|
548
|
+
state.authFailures[accountName] = Date.now() + AUTH_FAILURE_COOLDOWN;
|
|
549
|
+
}
|
|
437
550
|
function clearAuthFailure(state, accountName) {
|
|
438
551
|
delete state.authFailures[accountName];
|
|
439
552
|
}
|
|
@@ -854,6 +967,132 @@ async function refreshExpiredAccounts(data, state) {
|
|
|
854
967
|
}
|
|
855
968
|
}
|
|
856
969
|
}
|
|
970
|
+
var PROBE_CONCURRENCY = 3;
|
|
971
|
+
var PROBE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
|
|
972
|
+
var PROBE_MODELS = [
|
|
973
|
+
"claude-sonnet-4-5",
|
|
974
|
+
"claude-opus-4-5"
|
|
975
|
+
];
|
|
976
|
+
var PROBE_BETA_HEADERS = "oauth-2025-04-20,interleaved-thinking-2025-05-14";
|
|
977
|
+
var PROBE_USER_AGENT = "claude-cli/2.1.87 (external, cli)";
|
|
978
|
+
var PROBE_VERSION = "2023-06-01";
|
|
979
|
+
var CLAUDE_CODE_VERSION = "2.1.87";
|
|
980
|
+
var CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
981
|
+
var CCH_SALT = "59cf53e54c78";
|
|
982
|
+
var CCH_POSITIONS = [4, 7, 20];
|
|
983
|
+
function computeCCH(messageText) {
|
|
984
|
+
return createHash("sha256").update(messageText).digest("hex").slice(0, 5);
|
|
985
|
+
}
|
|
986
|
+
function computeVersionSuffix(messageText) {
|
|
987
|
+
const chars = CCH_POSITIONS.map((i) => messageText[i] || "0").join("");
|
|
988
|
+
return createHash("sha256").update(`${CCH_SALT}${chars}${CLAUDE_CODE_VERSION}`).digest("hex").slice(0, 3);
|
|
989
|
+
}
|
|
990
|
+
function buildBillingHeader(messageText) {
|
|
991
|
+
const suffix = computeVersionSuffix(messageText);
|
|
992
|
+
const cch = computeCCH(messageText);
|
|
993
|
+
return "x-anthropic-billing-header: " + `cc_version=${CLAUDE_CODE_VERSION}.${suffix}; ` + `cc_entrypoint=sdk-cli; ` + `cch=${cch};`;
|
|
994
|
+
}
|
|
995
|
+
async function probeAccount(account, state) {
|
|
996
|
+
const result = { name: account.name, success: false };
|
|
997
|
+
if (!account.access || account.expires <= Date.now()) {
|
|
998
|
+
if (!account.refresh) {
|
|
999
|
+
result.error = "missing refresh token";
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
const refreshResult = await refreshAccountToken(account.refresh);
|
|
1003
|
+
if (!refreshResult.ok || !refreshResult.access) {
|
|
1004
|
+
result.error = `token refresh failed: ${refreshResult.error || "unknown"}`;
|
|
1005
|
+
markAuthFailure(state, account.name);
|
|
1006
|
+
return result;
|
|
1007
|
+
}
|
|
1008
|
+
account.access = refreshResult.access;
|
|
1009
|
+
account.refresh = refreshResult.refresh;
|
|
1010
|
+
account.expires = refreshResult.expires;
|
|
1011
|
+
await updateAccountTokens(account.name, refreshResult.access, refreshResult.refresh, refreshResult.expires);
|
|
1012
|
+
clearAuthFailure(state, account.name);
|
|
1013
|
+
}
|
|
1014
|
+
let lastStatus = 0;
|
|
1015
|
+
let lastBody = "";
|
|
1016
|
+
for (const model of PROBE_MODELS) {
|
|
1017
|
+
try {
|
|
1018
|
+
const messageText = ".";
|
|
1019
|
+
const billingHeader = buildBillingHeader(messageText);
|
|
1020
|
+
const response = await fetch(PROBE_API_URL, {
|
|
1021
|
+
method: "POST",
|
|
1022
|
+
headers: {
|
|
1023
|
+
authorization: `Bearer ${account.access}`,
|
|
1024
|
+
"anthropic-beta": PROBE_BETA_HEADERS,
|
|
1025
|
+
"anthropic-version": PROBE_VERSION,
|
|
1026
|
+
"user-agent": PROBE_USER_AGENT,
|
|
1027
|
+
"content-type": "application/json"
|
|
1028
|
+
},
|
|
1029
|
+
body: JSON.stringify({
|
|
1030
|
+
model,
|
|
1031
|
+
max_tokens: 1,
|
|
1032
|
+
system: [
|
|
1033
|
+
{ type: "text", text: billingHeader },
|
|
1034
|
+
{ type: "text", text: CLAUDE_CODE_IDENTITY }
|
|
1035
|
+
],
|
|
1036
|
+
messages: [{ role: "user", content: messageText }]
|
|
1037
|
+
})
|
|
1038
|
+
});
|
|
1039
|
+
lastStatus = response.status;
|
|
1040
|
+
const updated = updateUsageFromHeaders(state, account.name, response.headers);
|
|
1041
|
+
if (response.status === 401 || response.status === 403) {
|
|
1042
|
+
result.error = `auth error ${response.status}`;
|
|
1043
|
+
markAuthFailure(state, account.name);
|
|
1044
|
+
return result;
|
|
1045
|
+
}
|
|
1046
|
+
if (response.status === 429 && updated) {
|
|
1047
|
+
result.success = true;
|
|
1048
|
+
result.model = model;
|
|
1049
|
+
clearAuthFailure(state, account.name);
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
if (response.ok || updated) {
|
|
1053
|
+
result.success = true;
|
|
1054
|
+
result.model = model;
|
|
1055
|
+
clearAuthFailure(state, account.name);
|
|
1056
|
+
return result;
|
|
1057
|
+
}
|
|
1058
|
+
lastBody = await response.text().catch(() => "");
|
|
1059
|
+
if (response.status === 400 && lastBody.includes("model")) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
1064
|
+
return result;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
result.error = `HTTP ${lastStatus}: ${lastBody.slice(0, 100)}`;
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
async function probeAllAccounts(accounts) {
|
|
1071
|
+
const state = loadState();
|
|
1072
|
+
const results = [];
|
|
1073
|
+
const probeErrors = [];
|
|
1074
|
+
for (let i = 0;i < accounts.length; i += PROBE_CONCURRENCY) {
|
|
1075
|
+
const batch = accounts.slice(i, i + PROBE_CONCURRENCY);
|
|
1076
|
+
const batchResults = await Promise.allSettled(batch.map((account) => probeAccount(account, state)));
|
|
1077
|
+
for (const settled of batchResults) {
|
|
1078
|
+
if (settled.status === "fulfilled") {
|
|
1079
|
+
results.push(settled.value);
|
|
1080
|
+
if (!settled.value.success && settled.value.error) {
|
|
1081
|
+
probeErrors.push(`${settled.value.name}: ${settled.value.error}`);
|
|
1082
|
+
}
|
|
1083
|
+
} else {
|
|
1084
|
+
probeErrors.push(`unexpected error: ${settled.reason}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
await saveStateMerged(state, accounts.map((a) => a.name));
|
|
1089
|
+
if (probeErrors.length > 0) {
|
|
1090
|
+
for (const err of probeErrors) {
|
|
1091
|
+
console.log(` ${YELLOW}Warning: probe failed for ${err}${RESET}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return { state, results };
|
|
1095
|
+
}
|
|
857
1096
|
async function cmdUsage(args) {
|
|
858
1097
|
const watch = args.includes("--watch") || args.includes("-w");
|
|
859
1098
|
const showUsage = async () => {
|
|
@@ -909,6 +1148,18 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
909
1148
|
console.log();
|
|
910
1149
|
}
|
|
911
1150
|
};
|
|
1151
|
+
const accountsData = loadAccounts();
|
|
1152
|
+
if (accountsData.accounts.length > 0) {
|
|
1153
|
+
process.stdout.write(`${DIM} Probing ${accountsData.accounts.length} account(s) for current usage...${RESET}`);
|
|
1154
|
+
const { results } = await probeAllAccounts(accountsData.accounts);
|
|
1155
|
+
const successCount = results.filter((r) => r.success).length;
|
|
1156
|
+
const models = [...new Set(results.filter((r) => r.model).map((r) => r.model))];
|
|
1157
|
+
process.stdout.write(`\r\x1B[K`);
|
|
1158
|
+
if (successCount < accountsData.accounts.length) {
|
|
1159
|
+
console.log(`${DIM} Probed ${successCount}/${accountsData.accounts.length} accounts successfully${models.length ? ` (${models.join(", ")})` : ""}${RESET}
|
|
1160
|
+
`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
912
1163
|
await showUsage();
|
|
913
1164
|
if (watch) {
|
|
914
1165
|
console.log(`${DIM}Refreshing every 5 seconds. Press Ctrl+C to stop.${RESET}
|