tokenmaxxing 1.170.0 → 1.171.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/package.json +1 -1
- package/src/lib/codex.ts +32 -10
- package/src/lib/codexusage.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.171.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/lib/codex.ts
CHANGED
|
@@ -41,10 +41,16 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
|
|
|
41
41
|
if (usage.planType != null) account.tier = usage.planType;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean };
|
|
44
|
+
type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean; rejectedToken?: string };
|
|
45
45
|
type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | CodexReadFailure;
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const REJECTED_REFRESH_BACKOFF_MS = 60 * 60 * 1000;
|
|
48
|
+
|
|
49
|
+
function afterRejection(rejected: CodexReadFailure, refreshFailure: CodexReadFailure): CodexReadFailure {
|
|
50
|
+
return { ...refreshFailure, reason: `${rejected.reason}; refresh skipped: ${refreshFailure.reason}` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function freshCodexAuth(account: Account, now: number, refresh: boolean, holder?: string, rejectedToken?: string): Promise<{ ok: true; auth: CodexAuthJson } | CodexReadFailure> {
|
|
48
54
|
let auth: CodexAuthJson | null;
|
|
49
55
|
try {
|
|
50
56
|
auth = readCodexStoreAuth(account.id);
|
|
@@ -52,7 +58,11 @@ async function freshCodexAuth(account: Account, now: number, refresh: boolean, h
|
|
|
52
58
|
return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
|
|
53
59
|
}
|
|
54
60
|
if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
|
|
55
|
-
|
|
61
|
+
const expiring = isCodexAccessExpiring({ auth, now });
|
|
62
|
+
if (auth.tokens.access_token !== rejectedToken && !expiring) return { ok: true, auth };
|
|
63
|
+
if (!expiring && auth.last_refresh != null && now - Date.parse(auth.last_refresh) < REJECTED_REFRESH_BACKOFF_MS) {
|
|
64
|
+
return { ok: false, reason: "the store was refreshed within the last hour", deadGrant: false };
|
|
65
|
+
}
|
|
56
66
|
if (!refresh) return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false, expiring: true };
|
|
57
67
|
if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
|
|
58
68
|
return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
|
|
@@ -68,18 +78,26 @@ async function freshCodexAuth(account: Account, now: number, refresh: boolean, h
|
|
|
68
78
|
return { ok: true, auth };
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
async function
|
|
72
|
-
const fresh = await freshCodexAuth(account, now, refresh, holder);
|
|
81
|
+
async function readCodexUsageOnce(account: Account, now: number, refresh: boolean, holder?: string, rejectedToken?: string): Promise<CodexReadOutcome> {
|
|
82
|
+
const fresh = await freshCodexAuth(account, now, refresh, holder, rejectedToken);
|
|
73
83
|
if (!fresh.ok) return fresh;
|
|
74
84
|
try {
|
|
75
85
|
const at = Date.now();
|
|
76
86
|
return { ok: true, usage: await fetchCodexUsage({ auth: fresh.auth, at }), at };
|
|
77
87
|
} catch (e) {
|
|
78
|
-
if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false };
|
|
88
|
+
if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false, rejectedToken: e.status === 401 ? fresh.auth.tokens.access_token : undefined };
|
|
79
89
|
throw e;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
|
|
94
|
+
const outcome = await readCodexUsageOnce(account, now, refresh, holder);
|
|
95
|
+
if (outcome.ok || outcome.rejectedToken === undefined || !refresh) return outcome;
|
|
96
|
+
const { rejectedToken } = outcome;
|
|
97
|
+
const fresh = await withLock(codexPool.lockFile, () => freshCodexAuth(account, now, true, holder, rejectedToken));
|
|
98
|
+
return fresh.ok ? readCodexUsageOnce(account, now, false) : afterRejection(outcome, fresh);
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
export async function observeCodex(account: Account, cfg: Config, now: number, opts: { probe: boolean; refresh: boolean; holder?: string }): Promise<Observation | null> {
|
|
84
102
|
if (opts.probe && (account.lastUsageAt == null || now - account.lastUsageAt > cfg.policy.usagePollTtlMs)) {
|
|
85
103
|
const outcome = await readCodexUsage(account, now, opts.refresh, opts.holder);
|
|
@@ -112,17 +130,21 @@ async function samplePool(accounts: Account[], now: number): Promise<Map<string,
|
|
|
112
130
|
reports.set(account.id, { ok: false, reason: outcome.reason });
|
|
113
131
|
}
|
|
114
132
|
};
|
|
115
|
-
const expiring: Account[] = [];
|
|
133
|
+
const expiring: { account: Account; rejected?: CodexReadFailure }[] = [];
|
|
116
134
|
await Promise.all(
|
|
117
135
|
accounts.map(async (account) => {
|
|
118
136
|
const outcome = await readCodexUsage(account, now, false);
|
|
119
|
-
if (!outcome.ok && outcome.expiring === true) expiring.push(account);
|
|
137
|
+
if (!outcome.ok && (outcome.expiring === true || outcome.rejectedToken !== undefined)) expiring.push({ account, rejected: outcome.rejectedToken === undefined ? undefined : outcome });
|
|
120
138
|
else record(account, outcome);
|
|
121
139
|
}),
|
|
122
140
|
);
|
|
123
141
|
if (expiring.length > 0) {
|
|
124
|
-
const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async (account) => ({ account, fresh: await freshCodexAuth(account, now, true) }))));
|
|
125
|
-
await Promise.all(
|
|
142
|
+
const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async ({ account, rejected }) => ({ account, rejected, fresh: await freshCodexAuth(account, now, true, undefined, rejected?.rejectedToken) }))));
|
|
143
|
+
await Promise.all(
|
|
144
|
+
rotated.map(async ({ account, rejected, fresh }) =>
|
|
145
|
+
record(account, fresh.ok ? await readCodexUsage(account, now, false) : rejected ? afterRejection(rejected, fresh) : fresh),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
126
148
|
}
|
|
127
149
|
return reports;
|
|
128
150
|
}
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -11,9 +11,11 @@ export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
|
11
11
|
const USAGE_URL = env("TOKENMAXXING_CODEX_USAGE_URL", CODEX_USAGE_URL);
|
|
12
12
|
|
|
13
13
|
export class CodexUsageReadError extends Error {
|
|
14
|
-
|
|
14
|
+
readonly status: number | null;
|
|
15
|
+
constructor(detail: string, status: number | null = null) {
|
|
15
16
|
super(`codex usage read failed: ${detail}`);
|
|
16
17
|
this.name = "CodexUsageReadError";
|
|
18
|
+
this.status = status;
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -73,7 +75,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
73
75
|
}
|
|
74
76
|
const text = await res.text();
|
|
75
77
|
if (!res.ok) {
|
|
76
|
-
throw new CodexUsageReadError(`HTTP ${res.status}: ${safeErrorDetail({ text })}
|
|
78
|
+
throw new CodexUsageReadError(`HTTP ${res.status}: ${safeErrorDetail({ text })}`, res.status);
|
|
77
79
|
}
|
|
78
80
|
const parsed = WireUsageSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
79
81
|
if (!parsed.success) {
|