tokenmaxxing 1.41.0 → 1.42.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/sample.ts +24 -6
- package/src/lib/usage.ts +43 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.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/sample.ts
CHANGED
|
@@ -6,11 +6,12 @@ import { writeFileAtomic } from "./atomic.ts";
|
|
|
6
6
|
import { readStore } from "./credstore.ts";
|
|
7
7
|
import { withLock } from "./lock.ts";
|
|
8
8
|
import { log } from "./log.ts";
|
|
9
|
-
import { claudeTierLabel, isDeadCredential } from "./oauth.ts";
|
|
10
|
-
import { claudePool, sampleDirFor, storeDirFor } from "./paths.ts";
|
|
9
|
+
import { claudeTierLabel, isAccessTokenExpiring, isDeadCredential } from "./oauth.ts";
|
|
10
|
+
import { claudePool, paths, sampleDirFor, storeDirFor } from "./paths.ts";
|
|
11
|
+
import { seatCounts } from "./presence.ts";
|
|
11
12
|
import type { Observation } from "./provider.ts";
|
|
12
13
|
import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
|
|
13
|
-
import { mergeWindows, probeUsage, windowsOf } from "./usage.ts";
|
|
14
|
+
import { fetchUsageDirect, mergeWindows, probeUsage, windowsOf } from "./usage.ts";
|
|
14
15
|
import { UsageWindowsSchema, type Account, type Config } from "./types.ts";
|
|
15
16
|
|
|
16
17
|
const SampleOutcomeSchema = z.discriminatedUnion("ok", [
|
|
@@ -93,11 +94,28 @@ export async function sampleOldest(cfg: Config): Promise<void> {
|
|
|
93
94
|
log("sample.failed", { account: target.id.slice(0, 8), reason: prepared.reason.slice(0, 200) });
|
|
94
95
|
return null;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
+
let token: string | null = null;
|
|
98
|
+
if (!seatCounts(paths.presenceDir).has(target.id)) {
|
|
99
|
+
const creds = await readStore(target.id).catch(() => null);
|
|
100
|
+
if (creds && !isDeadCredential(creds) && !isAccessTokenExpiring(creds)) token = creds.accessToken;
|
|
101
|
+
}
|
|
102
|
+
return { account: target, dir: prepared.dir, token };
|
|
97
103
|
});
|
|
98
104
|
if (!reserved) return;
|
|
99
105
|
const startedAt = Date.now();
|
|
100
|
-
|
|
106
|
+
let via = "probe";
|
|
107
|
+
let outcome: SampleOutcome;
|
|
108
|
+
if (reserved.token) {
|
|
109
|
+
const usage = await fetchUsageDirect(reserved.token);
|
|
110
|
+
if (usage) {
|
|
111
|
+
via = "get";
|
|
112
|
+
outcome = { ok: true, usage };
|
|
113
|
+
} else {
|
|
114
|
+
outcome = await runProbe(reserved.account, reserved.dir, { retries: 0 });
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
outcome = await runProbe(reserved.account, reserved.dir, { retries: 0 });
|
|
118
|
+
}
|
|
101
119
|
await withLock(claudePool.lockFile, () => {
|
|
102
120
|
const idx = loadAccounts(claudePool);
|
|
103
121
|
const stored = idx.accounts.find((a) => a.id === reserved.account.id);
|
|
@@ -108,7 +126,7 @@ export async function sampleOldest(cfg: Config): Promise<void> {
|
|
|
108
126
|
}
|
|
109
127
|
log(outcome.ok ? "sample.ok" : "sample.failed", {
|
|
110
128
|
account: reserved.account.id.slice(0, 8),
|
|
111
|
-
...(outcome.ok ? {} : { reason: outcome.reason.slice(0, 200) }),
|
|
129
|
+
...(outcome.ok ? { via } : { reason: outcome.reason.slice(0, 200) }),
|
|
112
130
|
});
|
|
113
131
|
});
|
|
114
132
|
}
|
package/src/lib/usage.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
|
2
2
|
import { delay } from "es-toolkit";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./claudebin.ts";
|
|
5
|
+
import { http } from "./http.ts";
|
|
5
6
|
import { log } from "./log.ts";
|
|
7
|
+
import { env } from "./paths.ts";
|
|
6
8
|
import { JsonTextSchema, RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
|
|
7
9
|
|
|
8
10
|
export function normalizeResetsAt(v: unknown): number | null {
|
|
@@ -341,10 +343,10 @@ function probeEnv(target: ProbeTarget): Record<string, string> {
|
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
export async function probeUsage(target: ProbeTarget, now = Date.now(), opts: { retries?: number } = {}): Promise<UsageWindows | null> {
|
|
344
|
-
const
|
|
346
|
+
const probe = probeEnv(target);
|
|
345
347
|
|
|
346
348
|
for (let attempt = 0; ; attempt++) {
|
|
347
|
-
const full = await probeUsageOnce(
|
|
349
|
+
const full = await probeUsageOnce(probe, now);
|
|
348
350
|
if (full) return full;
|
|
349
351
|
if (attempt >= (opts.retries ?? PROBE_RETRY_DELAYS_MS.length)) {
|
|
350
352
|
log("usage.probe_gave_up", { attempts: attempt + 1 });
|
|
@@ -353,3 +355,42 @@ export async function probeUsage(target: ProbeTarget, now = Date.now(), opts: {
|
|
|
353
355
|
await delay(PROBE_RETRY_DELAYS_MS[Math.min(attempt, PROBE_RETRY_DELAYS_MS.length - 1)]!);
|
|
354
356
|
}
|
|
355
357
|
}
|
|
358
|
+
|
|
359
|
+
const USAGE_URL = env("TOKENMAXXING_OAUTH_USAGE_URL", "https://api.anthropic.com/api/oauth/usage");
|
|
360
|
+
const USAGE_DEADLINE_MS = 10_000;
|
|
361
|
+
|
|
362
|
+
const UsageWindowResponseSchema = z.object({ utilization: z.number(), resets_at: z.number().nullable().optional() });
|
|
363
|
+
const UsageResponseSchema = z.object({
|
|
364
|
+
rate_limits: z
|
|
365
|
+
.object({ five_hour: UsageWindowResponseSchema.nullish(), seven_day: UsageWindowResponseSchema.nullish() })
|
|
366
|
+
.nullish(),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
export async function fetchUsageDirect(accessToken: string): Promise<UsageWindows | null> {
|
|
370
|
+
let res: Response;
|
|
371
|
+
try {
|
|
372
|
+
res = await http.get(`${USAGE_URL}?at_wall=1&skip_spend=1`, {
|
|
373
|
+
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
374
|
+
signal: AbortSignal.timeout(USAGE_DEADLINE_MS),
|
|
375
|
+
});
|
|
376
|
+
} catch (e) {
|
|
377
|
+
log("usage.get_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (!res.ok) {
|
|
381
|
+
log("usage.get_failed", { status: res.status });
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const parsed = UsageResponseSchema.safeParse(JsonTextSchema.safeParse(await res.text()).data);
|
|
385
|
+
const five = parsed.success ? parsed.data.rate_limits?.five_hour : null;
|
|
386
|
+
const seven = parsed.success ? parsed.data.rate_limits?.seven_day : null;
|
|
387
|
+
if (!five || !seven) {
|
|
388
|
+
log("usage.get_incomplete", { ok: parsed.success });
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const win = (w: z.infer<typeof UsageWindowResponseSchema>): UsageWindow => ({
|
|
392
|
+
usedPercentage: w.utilization * 100,
|
|
393
|
+
resetsAt: w.resets_at != null ? w.resets_at * 1000 : null,
|
|
394
|
+
});
|
|
395
|
+
return { fiveHour: win(five), sevenDay: win(seven), perModel: {} };
|
|
396
|
+
}
|