tokenmaxxing 1.19.1 → 1.21.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/DESIGN.md +17 -6
- package/README.md +11 -5
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/switching-policy/SKILL.md +7 -3
- package/agent-plugin/skills/switching-policy/references/policy.md +4 -3
- package/package.json +1 -1
- package/src/cli/check.ts +0 -3
- package/src/cli/config.ts +0 -1
- package/src/cli/status.ts +2 -12
- package/src/cli/switch.ts +5 -1
- package/src/entries/codexstophook.ts +0 -1
- package/src/entries/stopfailurehook.ts +6 -9
- package/src/entries/stophook.ts +0 -1
- package/src/entries/supervisor.ts +1 -2
- package/src/lib/codexdecide.ts +1 -25
- package/src/lib/codexpick.ts +3 -3
- package/src/lib/codexusage.ts +0 -9
- package/src/lib/decide.ts +59 -76
- package/src/lib/oauth.ts +2 -1
- package/src/lib/paths.ts +0 -1
- package/src/lib/picker.ts +25 -5
- package/src/lib/sample.ts +25 -11
- package/src/lib/state.ts +2 -82
- package/src/lib/swap.ts +17 -1
- package/src/lib/types.ts +0 -53
- package/src/lib/usage.ts +13 -7
- package/src/lib/bankedreset.ts +0 -234
- package/src/lib/codexreset.ts +0 -122
package/src/lib/types.ts
CHANGED
|
@@ -41,51 +41,22 @@ export type UsageWindows = z.infer<typeof UsageWindowsSchema>;
|
|
|
41
41
|
const ModelInfoSchema = z.object({ id: z.string(), display: z.string() });
|
|
42
42
|
export type ModelInfo = z.infer<typeof ModelInfoSchema>;
|
|
43
43
|
|
|
44
|
-
const WindowAnchorSchema = z.object({
|
|
45
|
-
fiveHourResetsAt: z.number().nullable(),
|
|
46
|
-
sevenDayResetsAt: z.number().nullable(),
|
|
47
|
-
session: z.number(),
|
|
48
|
-
weekly: z.number(),
|
|
49
|
-
});
|
|
50
|
-
export type WindowAnchor = z.infer<typeof WindowAnchorSchema>;
|
|
51
|
-
|
|
52
44
|
export const UsageStateSchema = UsageWindowsSchema.extend({
|
|
53
45
|
account: z.string().nullable(),
|
|
54
46
|
ts: z.number(),
|
|
55
47
|
model: ModelInfoSchema.nullable().default(null),
|
|
56
|
-
anchor: WindowAnchorSchema.optional(),
|
|
57
|
-
sessionWindowWeeklyCost: z.number().positive().optional(),
|
|
58
48
|
sampledAt: z.number().optional(),
|
|
59
49
|
});
|
|
60
50
|
export type UsageState = z.infer<typeof UsageStateSchema>;
|
|
61
51
|
|
|
62
|
-
const FamilyAnchorSchema = z.object({
|
|
63
|
-
fiveHourResetsAt: z.number().nullable(),
|
|
64
|
-
session: z.number(),
|
|
65
|
-
caps: z.record(z.string(), z.number()),
|
|
66
|
-
});
|
|
67
|
-
export type FamilyAnchor = z.infer<typeof FamilyAnchorSchema>;
|
|
68
|
-
|
|
69
52
|
export const ModelUsageStateSchema = z.object({
|
|
70
53
|
perModel: z.record(z.string(), UsageWindowSchema).default({}),
|
|
71
54
|
account: z.string().nullable(),
|
|
72
55
|
ts: z.number(),
|
|
73
56
|
sampledAt: z.number().optional(),
|
|
74
|
-
anchor: FamilyAnchorSchema.optional(),
|
|
75
|
-
familyCosts: z.record(z.string(), z.number()).optional(),
|
|
76
57
|
});
|
|
77
58
|
export type ModelUsageState = z.infer<typeof ModelUsageStateSchema>;
|
|
78
59
|
|
|
79
|
-
export const BankedResetOutcomeSchema = z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "rate_limited", "auth_error", "error"]);
|
|
80
|
-
export type BankedResetOutcome = z.infer<typeof BankedResetOutcomeSchema>;
|
|
81
|
-
|
|
82
|
-
export const BankedResetRecordSchema = z.object({
|
|
83
|
-
outcome: BankedResetOutcomeSchema,
|
|
84
|
-
at: z.number(),
|
|
85
|
-
nextAvailableAt: z.number().nullable(),
|
|
86
|
-
});
|
|
87
|
-
export type BankedResetRecord = z.infer<typeof BankedResetRecordSchema>;
|
|
88
|
-
|
|
89
60
|
export const AccountSchema = z.object({
|
|
90
61
|
accountUuid: z.string(),
|
|
91
62
|
email: z.string(),
|
|
@@ -102,8 +73,6 @@ export const AccountSchema = z.object({
|
|
|
102
73
|
needsReauth: z.boolean().optional(),
|
|
103
74
|
subscriptionType: z.string().optional(),
|
|
104
75
|
rateLimitTier: z.string().optional(),
|
|
105
|
-
sessionWindowWeeklyCost: z.number().positive().optional(),
|
|
106
|
-
bankedReset: BankedResetRecordSchema.optional(),
|
|
107
76
|
});
|
|
108
77
|
export type Account = z.infer<typeof AccountSchema>;
|
|
109
78
|
|
|
@@ -120,7 +89,6 @@ export const NextCheckSchema = z.object({ dueAt: z.number(), ts: z.number() });
|
|
|
120
89
|
|
|
121
90
|
export const EnforcedLimitSchema = z.object({
|
|
122
91
|
account: z.string(),
|
|
123
|
-
kind: z.enum(["session", "weekly", "model"]),
|
|
124
92
|
family: z.string().nullable(),
|
|
125
93
|
resetsAt: z.number().nullable(),
|
|
126
94
|
windowMs: z.number(),
|
|
@@ -145,10 +113,6 @@ export const ScreeningThresholdsSchema = z.object({
|
|
|
145
113
|
weekly: z.number().min(0).max(100),
|
|
146
114
|
});
|
|
147
115
|
|
|
148
|
-
export const BankedResetProviderSchema = z.enum(["claude", "codex"]);
|
|
149
|
-
export type BankedResetProvider = z.infer<typeof BankedResetProviderSchema>;
|
|
150
|
-
export const BankedResetProvidersSchema = z.array(BankedResetProviderSchema);
|
|
151
|
-
|
|
152
116
|
export const ConfigSchema = z
|
|
153
117
|
.object({
|
|
154
118
|
thresholds: ScreeningThresholdsSchema,
|
|
@@ -163,7 +127,6 @@ export const ConfigSchema = z
|
|
|
163
127
|
usagePollTtlMs: z.number().int().positive(),
|
|
164
128
|
maxWaitMs: z.number().int().positive(),
|
|
165
129
|
checkIntervalMs: z.number().int().min(10_000),
|
|
166
|
-
preferToUseBankedReset: BankedResetProvidersSchema,
|
|
167
130
|
}),
|
|
168
131
|
})
|
|
169
132
|
.refine((cfg) => cfg.policy.projectionMargin < Math.min(...cfg.thresholds.session, cfg.thresholds.weekly), {
|
|
@@ -181,7 +144,6 @@ export const RespawnMarkerSchema = z.object({
|
|
|
181
144
|
sessionId: z.string(),
|
|
182
145
|
prompt: z.string().optional(),
|
|
183
146
|
launchedAt: z.number().optional(),
|
|
184
|
-
reset: z.boolean().optional(),
|
|
185
147
|
});
|
|
186
148
|
|
|
187
149
|
export const RateLimitsStdinSchema = z.looseObject({
|
|
@@ -281,21 +243,9 @@ export const CodexUsageSchema = z.object({
|
|
|
281
243
|
planType: z.string().nullable(),
|
|
282
244
|
aggregate: z.array(CodexWindowSchema),
|
|
283
245
|
perLimit: z.record(z.string(), z.array(CodexWindowSchema)),
|
|
284
|
-
resetCredits: z.number().int().nullable(),
|
|
285
|
-
reachedType: z.string().nullable(),
|
|
286
246
|
});
|
|
287
247
|
export type CodexUsage = z.infer<typeof CodexUsageSchema>;
|
|
288
248
|
|
|
289
|
-
export const CodexBankedResetOutcomeSchema = z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed", "auth_error", "error"]);
|
|
290
|
-
export type CodexBankedResetOutcome = z.infer<typeof CodexBankedResetOutcomeSchema>;
|
|
291
|
-
|
|
292
|
-
export const CodexBankedResetRecordSchema = z.object({
|
|
293
|
-
outcome: CodexBankedResetOutcomeSchema,
|
|
294
|
-
at: z.number(),
|
|
295
|
-
key: z.string().optional(),
|
|
296
|
-
});
|
|
297
|
-
export type CodexBankedResetRecord = z.infer<typeof CodexBankedResetRecordSchema>;
|
|
298
|
-
|
|
299
249
|
const BareFileNameSchema = z
|
|
300
250
|
.string()
|
|
301
251
|
.refine((s) => s.length > 0 && s !== "." && s !== ".." && !s.includes("/") && !s.includes("\\"), {
|
|
@@ -314,12 +264,9 @@ export const CodexAccountSchema = z.object({
|
|
|
314
264
|
.object({
|
|
315
265
|
aggregate: z.array(CodexWindowSchema),
|
|
316
266
|
perLimit: z.record(z.string(), z.array(CodexWindowSchema)),
|
|
317
|
-
resetCredits: z.number().int().nullable().optional(),
|
|
318
|
-
reachedType: z.string().nullable().optional(),
|
|
319
267
|
})
|
|
320
268
|
.optional(),
|
|
321
269
|
lastUsageAt: z.number().optional(),
|
|
322
|
-
bankedReset: CodexBankedResetRecordSchema.optional(),
|
|
323
270
|
});
|
|
324
271
|
export type CodexAccount = z.infer<typeof CodexAccountSchema>;
|
|
325
272
|
|
package/src/lib/usage.ts
CHANGED
|
@@ -286,9 +286,14 @@ async function spawnClaudeBounded(
|
|
|
286
286
|
cmd: string[],
|
|
287
287
|
env: Record<string, string>,
|
|
288
288
|
cwd?: string,
|
|
289
|
+
killMs = PROBE_KILL_MS,
|
|
290
|
+
signal?: AbortSignal,
|
|
289
291
|
): Promise<SpawnResult | null> {
|
|
292
|
+
signal?.throwIfAborted();
|
|
290
293
|
const p = Bun.spawn(cmd, { env, cwd, stdout: "pipe", stderr: "pipe" });
|
|
291
|
-
const
|
|
294
|
+
const abort = () => p.kill("SIGKILL");
|
|
295
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
296
|
+
const killer = setTimeout(() => p.kill("SIGKILL"), killMs);
|
|
292
297
|
try {
|
|
293
298
|
const reads = Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()]);
|
|
294
299
|
const settled = await Promise.race([
|
|
@@ -301,13 +306,14 @@ async function spawnClaudeBounded(
|
|
|
301
306
|
return { exitCode: p.exitCode, stdout, stderr };
|
|
302
307
|
} finally {
|
|
303
308
|
clearTimeout(killer);
|
|
309
|
+
signal?.removeEventListener("abort", abort);
|
|
304
310
|
}
|
|
305
311
|
}
|
|
306
312
|
|
|
307
|
-
async function probeUsageOnce(env: Record<string, string>, now: number): Promise<FullUsage | null> {
|
|
313
|
+
async function probeUsageOnce(env: Record<string, string>, now: number, killMs: number, signal?: AbortSignal): Promise<FullUsage | null> {
|
|
308
314
|
let out: string;
|
|
309
315
|
try {
|
|
310
|
-
const r = await spawnClaudeBounded([resolveRealClaude(), "-p", "/usage", "--output-format", "json"], env);
|
|
316
|
+
const r = await spawnClaudeBounded([resolveRealClaude(), "-p", "/usage", "--output-format", "json"], env, undefined, killMs, signal);
|
|
311
317
|
if (r === null) {
|
|
312
318
|
log("usage.probe_failed", { err: "output pipes still open after child exit (leaked descendant)" });
|
|
313
319
|
return null;
|
|
@@ -345,17 +351,17 @@ function probeEnv(configDir?: string): Record<string, string> {
|
|
|
345
351
|
return env;
|
|
346
352
|
}
|
|
347
353
|
|
|
348
|
-
export async function probeUsage(configDir?: string, now = Date.now()): Promise<FullUsage | null> {
|
|
354
|
+
export async function probeUsage(configDir?: string, now = Date.now(), opts: { retries?: number; killMs?: number; signal?: AbortSignal } = {}): Promise<FullUsage | null> {
|
|
349
355
|
const env = probeEnv(configDir);
|
|
350
356
|
|
|
351
357
|
for (let attempt = 0; ; attempt++) {
|
|
352
|
-
const full = await probeUsageOnce(env, now);
|
|
358
|
+
const full = await probeUsageOnce(env, now, opts.killMs ?? PROBE_KILL_MS, opts.signal);
|
|
353
359
|
if (full) return full;
|
|
354
|
-
if (attempt >= PROBE_RETRY_DELAYS_MS.length) {
|
|
360
|
+
if (attempt >= (opts.retries ?? PROBE_RETRY_DELAYS_MS.length) || opts.signal?.aborted) {
|
|
355
361
|
log("usage.probe_gave_up", { attempts: attempt + 1 });
|
|
356
362
|
return null;
|
|
357
363
|
}
|
|
358
|
-
await delay(PROBE_RETRY_DELAYS_MS[attempt]!);
|
|
364
|
+
await delay(PROBE_RETRY_DELAYS_MS[Math.min(attempt, PROBE_RETRY_DELAYS_MS.length - 1)]!);
|
|
359
365
|
}
|
|
360
366
|
}
|
|
361
367
|
|
package/src/lib/bankedreset.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./claudebin.ts";
|
|
5
|
-
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
6
|
-
import { liveTarget, mergeIntoLive, readItem, writeItem } from "./credstore.ts";
|
|
7
|
-
import { http, safeErrorDetail } from "./http.ts";
|
|
8
|
-
import { log } from "./log.ts";
|
|
9
|
-
import { InvalidGrantError, fetchTokenIdentity, isAccessTokenExpiring, isDeadCredential, refreshCredential } from "./oauth.ts";
|
|
10
|
-
import { clearDepletedWait, clearNextCheck, loadAccounts, loadUsage, saveAccounts, saveLastResetAt, writeUsage } from "./state.ts";
|
|
11
|
-
import { keepRotatedPair } from "./swap.ts";
|
|
12
|
-
import { normalizeResetsAt } from "./usage.ts";
|
|
13
|
-
import { CredentialBlobSchema, type Account, type BankedResetOutcome, type BankedResetRecord, type OAuthCreds } from "./types.ts";
|
|
14
|
-
|
|
15
|
-
const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
16
|
-
const API_BASE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_OAUTH_API_BASE_URL) ?? "https://api.anthropic.com";
|
|
17
|
-
const OAUTH_BETA = "oauth-2025-04-20";
|
|
18
|
-
const RESET_PROGRAM = "juniper_tide";
|
|
19
|
-
const CLAIM_TIMEOUT_MS = 25_000;
|
|
20
|
-
const VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
21
|
-
const REFRESH_SKEW_MS = 300_000;
|
|
22
|
-
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
-
const INELIGIBLE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
24
|
-
|
|
25
|
-
const ClaimResponseSchema = z.looseObject({
|
|
26
|
-
result: z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable"]).catch("unavailable"),
|
|
27
|
-
next_available_at: z.union([z.string(), z.number()]).nullable().optional().catch(null),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const ClaimSchema = z.object({ outcome: z.enum(["reset", "pass"]) });
|
|
31
|
-
export type Claim = z.infer<typeof ClaimSchema>["outcome"];
|
|
32
|
-
|
|
33
|
-
export function bankedResetBelievedAvailable(account: Account, now: number): boolean {
|
|
34
|
-
const rec = account.bankedReset;
|
|
35
|
-
if (!rec) return true;
|
|
36
|
-
if (rec.nextAvailableAt != null) return rec.nextAvailableAt <= now;
|
|
37
|
-
if (rec.outcome === "reset" || rec.outcome === "already_used") return now - rec.at >= WEEK_MS;
|
|
38
|
-
if (rec.outcome === "ineligible") return now - rec.at >= INELIGIBLE_TTL_MS;
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const VersionSchema = z.string().regex(/^\d+\.\d+\.\d+\S*$/);
|
|
43
|
-
const PackageJsonSchema = z.looseObject({ version: VersionSchema });
|
|
44
|
-
|
|
45
|
-
function claudeVersion(): string | undefined {
|
|
46
|
-
const bin = resolveRealClaude();
|
|
47
|
-
const fromPath = VersionSchema.safeParse(bin.match(/\/versions\/(\d+\.\d+\.\d+[^/]*)$/)?.[1]);
|
|
48
|
-
if (fromPath.success) return fromPath.data;
|
|
49
|
-
try {
|
|
50
|
-
const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync(join(dirname(bin), "package.json"), "utf8")));
|
|
51
|
-
if (pkg.success) return pkg.data.version;
|
|
52
|
-
} catch {
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
const r = Bun.spawnSync([bin, "--version"], {
|
|
56
|
-
env: { ...process.env, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) },
|
|
57
|
-
stdout: "pipe",
|
|
58
|
-
stderr: "pipe",
|
|
59
|
-
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
60
|
-
});
|
|
61
|
-
const spawned = VersionSchema.safeParse(r.stdout.toString("utf8").trim().match(/^(\d+\.\d+\.\d+\S*)/)?.[1]);
|
|
62
|
-
if (spawned.success) return spawned.data;
|
|
63
|
-
} catch {
|
|
64
|
-
}
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function claudeUserAgent(): string {
|
|
69
|
-
const version = claudeVersion();
|
|
70
|
-
return version ? `claude-cli/${version} (external, cli)` : "claude-cli (external, cli)";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const LiveTokenSchema = z.discriminatedUnion("ok", [
|
|
74
|
-
z.object({ ok: z.literal(true), accessToken: z.string(), organizationUuid: z.string() }),
|
|
75
|
-
z.object({ ok: z.literal(false), outcome: z.enum(["auth_error", "error"]), detail: z.string() }),
|
|
76
|
-
]);
|
|
77
|
-
type LiveToken = z.infer<typeof LiveTokenSchema>;
|
|
78
|
-
|
|
79
|
-
function liveAccessToken(raw: string | null): string | null {
|
|
80
|
-
if (raw == null) return null;
|
|
81
|
-
const parsed = CredentialBlobSchema.safeParse((() => {
|
|
82
|
-
try { return JSON.parse(raw); } catch { return null; }
|
|
83
|
-
})());
|
|
84
|
-
return parsed.success ? parsed.data.claudeAiOauth.accessToken : null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function liveTokenLocked(account: Account, lock: { compromised: () => boolean }): Promise<LiveToken> {
|
|
88
|
-
const raw = await readItem(liveTarget());
|
|
89
|
-
if (!raw) return { ok: false, outcome: "auth_error", detail: "no live credential" };
|
|
90
|
-
let creds: OAuthCreds;
|
|
91
|
-
try {
|
|
92
|
-
creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
|
|
93
|
-
} catch (e) {
|
|
94
|
-
return { ok: false, outcome: "error", detail: `live credential blob unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)})` };
|
|
95
|
-
}
|
|
96
|
-
if (isDeadCredential(creds)) return { ok: false, outcome: "auth_error", detail: "live credential was cleared after a failed refresh" };
|
|
97
|
-
if (isAccessTokenExpiring(creds, REFRESH_SKEW_MS)) {
|
|
98
|
-
let next: OAuthCreds;
|
|
99
|
-
try {
|
|
100
|
-
next = await refreshCredential(creds);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
const detail = e instanceof Error ? e.message : String(e);
|
|
103
|
-
return { ok: false, outcome: e instanceof InvalidGrantError ? "auth_error" : "error", detail };
|
|
104
|
-
}
|
|
105
|
-
if (lock.compromised()) {
|
|
106
|
-
let owner: Account | null = null;
|
|
107
|
-
try {
|
|
108
|
-
const id = await fetchTokenIdentity(next.accessToken);
|
|
109
|
-
owner = loadAccounts().accounts.find((a) => a.accountUuid === id.accountUuid) ?? null;
|
|
110
|
-
} catch {
|
|
111
|
-
}
|
|
112
|
-
const kept = await keepRotatedPair({ fresh: next, owner, fallback: account, liveOwnerUuid: account.accountUuid, expectedLiveToken: creds.accessToken, lock });
|
|
113
|
-
return { ok: false, outcome: "error", detail: `refresh lock compromised mid-refresh - the rotated token ${kept}` };
|
|
114
|
-
}
|
|
115
|
-
await writeItem(liveTarget(), mergeIntoLive(raw, next));
|
|
116
|
-
creds = next;
|
|
117
|
-
}
|
|
118
|
-
let identity;
|
|
119
|
-
try {
|
|
120
|
-
identity = await fetchTokenIdentity(creds.accessToken);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
return { ok: false, outcome: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
123
|
-
}
|
|
124
|
-
if (identity.accountUuid !== account.accountUuid) {
|
|
125
|
-
return { ok: false, outcome: "error", detail: `live credential belongs to account ${identity.accountUuid.slice(0, 8)}, not the seat` };
|
|
126
|
-
}
|
|
127
|
-
return { ok: true, accessToken: creds.accessToken, organizationUuid: identity.organizationUuid };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const PostResultSchema = z.object({
|
|
131
|
-
outcome: z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "rate_limited", "auth_error", "error"]),
|
|
132
|
-
nextAvailableAt: z.number().nullable(),
|
|
133
|
-
detail: z.string(),
|
|
134
|
-
ambiguous: z.boolean().optional(),
|
|
135
|
-
});
|
|
136
|
-
type PostResult = z.infer<typeof PostResultSchema>;
|
|
137
|
-
|
|
138
|
-
async function postClaim(input: { accessToken: string; organizationUuid: string; userAgent: string }): Promise<PostResult> {
|
|
139
|
-
const url = `${API_BASE_URL}/api/organizations/${input.organizationUuid}/reset_rate_limits`;
|
|
140
|
-
let res: Response;
|
|
141
|
-
try {
|
|
142
|
-
res = await http.post(url, {
|
|
143
|
-
headers: {
|
|
144
|
-
Authorization: `Bearer ${input.accessToken}`,
|
|
145
|
-
"anthropic-beta": OAUTH_BETA,
|
|
146
|
-
"Content-Type": "application/json",
|
|
147
|
-
"User-Agent": input.userAgent,
|
|
148
|
-
},
|
|
149
|
-
body: JSON.stringify({ program: RESET_PROGRAM }),
|
|
150
|
-
timeout: CLAIM_TIMEOUT_MS,
|
|
151
|
-
});
|
|
152
|
-
} catch (e) {
|
|
153
|
-
return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint unreachable: ${e instanceof Error ? e.message : String(e)}` };
|
|
154
|
-
}
|
|
155
|
-
let text: string;
|
|
156
|
-
try {
|
|
157
|
-
text = await res.text();
|
|
158
|
-
} catch (e) {
|
|
159
|
-
return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint answered HTTP ${res.status} but the body could not be read: ${e instanceof Error ? e.message : String(e)}`, ambiguous: true };
|
|
160
|
-
}
|
|
161
|
-
if (!res.ok) {
|
|
162
|
-
const detail = `HTTP ${res.status}: ${safeErrorDetail({ text })}`;
|
|
163
|
-
if (res.status === 429) return { outcome: "rate_limited", nextAvailableAt: null, detail };
|
|
164
|
-
if (res.status === 401 || res.status === 403) return { outcome: "auth_error", nextAvailableAt: null, detail };
|
|
165
|
-
return { outcome: "error", nextAvailableAt: null, detail };
|
|
166
|
-
}
|
|
167
|
-
const parsed = ClaimResponseSchema.safeParse((() => {
|
|
168
|
-
try { return JSON.parse(text); } catch { return null; }
|
|
169
|
-
})());
|
|
170
|
-
if (!parsed.success) {
|
|
171
|
-
return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint answered HTTP ${res.status} with an unrecognized body (${text.length} bytes, withheld)`, ambiguous: true };
|
|
172
|
-
}
|
|
173
|
-
return { outcome: parsed.data.result, nextAvailableAt: normalizeResetsAt(parsed.data.next_available_at), detail: parsed.data.result };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function record(accountUuid: string, rec: BankedResetRecord, reset: boolean, now: number): void {
|
|
177
|
-
const idx = loadAccounts();
|
|
178
|
-
const account = idx.accounts.find((a) => a.accountUuid === accountUuid);
|
|
179
|
-
if (!account) return;
|
|
180
|
-
account.bankedReset = rec;
|
|
181
|
-
if (reset) {
|
|
182
|
-
delete account.enforcedUntil;
|
|
183
|
-
if (account.lastUsage) {
|
|
184
|
-
account.lastUsage = { ...account.lastUsage, fiveHour: { usedPercentage: 0, resetsAt: null } };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
saveAccounts(idx);
|
|
188
|
-
if (!reset) return;
|
|
189
|
-
const u = loadUsage();
|
|
190
|
-
if (u && u.account === accountUuid) {
|
|
191
|
-
writeUsage({ ...u, fiveHour: { usedPercentage: 0, resetsAt: null }, ts: now }, { stamp: true });
|
|
192
|
-
}
|
|
193
|
-
saveLastResetAt(now);
|
|
194
|
-
clearDepletedWait();
|
|
195
|
-
clearNextCheck();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function claimBankedReset(input: { account: Account }): Promise<Claim> {
|
|
199
|
-
const { account } = input;
|
|
200
|
-
const short = account.accountUuid.slice(0, 8);
|
|
201
|
-
let result: PostResult;
|
|
202
|
-
let moved = false;
|
|
203
|
-
try {
|
|
204
|
-
const userAgent = claudeUserAgent();
|
|
205
|
-
({ result, moved } = await withClaudeRefreshLock(async (lock) => {
|
|
206
|
-
const token = await liveTokenLocked(account, lock);
|
|
207
|
-
if (!token.ok) return { result: { outcome: token.outcome, nextAvailableAt: null, detail: token.detail }, moved: false };
|
|
208
|
-
let posted = await postClaim({ accessToken: token.accessToken, organizationUuid: token.organizationUuid, userAgent });
|
|
209
|
-
if (posted.ambiguous) {
|
|
210
|
-
const again = await postClaim({ accessToken: token.accessToken, organizationUuid: token.organizationUuid, userAgent });
|
|
211
|
-
posted =
|
|
212
|
-
again.outcome === "already_used"
|
|
213
|
-
? { outcome: "reset", nextAvailableAt: again.nextAvailableAt, detail: "already_used on the confirming claim, so the ambiguous claim was the reset", ambiguous: true }
|
|
214
|
-
: again;
|
|
215
|
-
}
|
|
216
|
-
return { result: posted, moved: liveAccessToken(await readItem(liveTarget())) !== token.accessToken };
|
|
217
|
-
}));
|
|
218
|
-
} catch (e) {
|
|
219
|
-
result = { outcome: "error", nextAvailableAt: null, detail: `claim aborted: ${e instanceof Error ? e.message : String(e)}` };
|
|
220
|
-
}
|
|
221
|
-
const outcome: BankedResetOutcome = result.outcome;
|
|
222
|
-
const reset = outcome === "reset";
|
|
223
|
-
const completedAt = Date.now();
|
|
224
|
-
record(account.accountUuid, { outcome, at: completedAt, nextAvailableAt: result.nextAvailableAt }, reset, completedAt);
|
|
225
|
-
log(reset ? "bankedreset.claimed" : "bankedreset.refused", {
|
|
226
|
-
account: short,
|
|
227
|
-
outcome,
|
|
228
|
-
nextAvailableAt: result.nextAvailableAt,
|
|
229
|
-
moved,
|
|
230
|
-
ambiguous: result.ambiguous === true ? true : undefined,
|
|
231
|
-
detail: reset && result.ambiguous !== true ? undefined : result.detail.slice(0, 200),
|
|
232
|
-
});
|
|
233
|
-
return reset && !moved ? "reset" : "pass";
|
|
234
|
-
}
|
package/src/lib/codexreset.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { codexIdentityOf, readLiveCodexAuth } from "./codexauth.ts";
|
|
3
|
-
import { allWindows, barFor, liveUsed } from "./codexpick.ts";
|
|
4
|
-
import { loadCodexAccounts, saveCodexAccounts } from "./codexstate.ts";
|
|
5
|
-
import { http, safeErrorDetail } from "./http.ts";
|
|
6
|
-
import { log } from "./log.ts";
|
|
7
|
-
import type { CodexAccount, CodexAuthJson, CodexBankedResetOutcome, Thresholds } from "./types.ts";
|
|
8
|
-
|
|
9
|
-
const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
10
|
-
const CONSUME_URL =
|
|
11
|
-
EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_RESET_URL) ?? "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
|
|
12
|
-
const CONSUME_TIMEOUT_MS = 30_000;
|
|
13
|
-
const RETRY_KEY_TTL_MS = 10 * 60_000;
|
|
14
|
-
const WALL_PERCENT = 100;
|
|
15
|
-
const LIMIT_REACHED = "rate_limit_reached";
|
|
16
|
-
|
|
17
|
-
const VerdictSchema = z.enum(["hold", "claim", "pass"]);
|
|
18
|
-
export type CodexBankedVerdict = z.infer<typeof VerdictSchema>;
|
|
19
|
-
const ClaimSchema = z.enum(["reset", "pass"]);
|
|
20
|
-
export type CodexBankedClaim = z.infer<typeof ClaimSchema>;
|
|
21
|
-
|
|
22
|
-
export function codexBankedResetVerdict(input: { account: CodexAccount; thresholds: Thresholds; now: number; pollTtlMs: number }): CodexBankedVerdict {
|
|
23
|
-
const { account, thresholds, now, pollTtlMs } = input;
|
|
24
|
-
const usage = account.lastUsage;
|
|
25
|
-
if (!usage) return "pass";
|
|
26
|
-
const credits = usage.resetCredits ?? null;
|
|
27
|
-
if (credits == null || credits <= 0) return "pass";
|
|
28
|
-
const rec = account.bankedReset;
|
|
29
|
-
if (rec != null && rec.outcome !== "reset" && now - rec.at < pollTtlMs) return "pass";
|
|
30
|
-
const sampledAt = account.lastUsageAt ?? null;
|
|
31
|
-
const windows = allWindows(account);
|
|
32
|
-
const over = windows.some((window) => liveUsed({ window, now, sampledAt }) >= barFor({ window, thresholds }));
|
|
33
|
-
if (!over) return "pass";
|
|
34
|
-
const atWall = usage.reachedType === LIMIT_REACHED || windows.some((window) => liveUsed({ window, now, sampledAt }) >= WALL_PERCENT);
|
|
35
|
-
return atWall ? "claim" : "hold";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const ConsumeResponseSchema = z.looseObject({
|
|
39
|
-
code: z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed"]),
|
|
40
|
-
windows_reset: z.number().int().nullish(),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const ConsumeResultSchema = z.object({ outcome: z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed", "auth_error", "error"]), detail: z.string() });
|
|
44
|
-
type ConsumeResult = z.infer<typeof ConsumeResultSchema>;
|
|
45
|
-
|
|
46
|
-
export async function consumeCodexBankedReset(input: { auth: CodexAuthJson; idempotencyKey: string }): Promise<ConsumeResult> {
|
|
47
|
-
const { auth, idempotencyKey } = input;
|
|
48
|
-
const identity = codexIdentityOf({ auth });
|
|
49
|
-
let res: Response;
|
|
50
|
-
let text: string;
|
|
51
|
-
try {
|
|
52
|
-
res = await http.post(CONSUME_URL, {
|
|
53
|
-
headers: {
|
|
54
|
-
Authorization: `Bearer ${auth.tokens.access_token}`,
|
|
55
|
-
"ChatGPT-Account-Id": identity.accountId,
|
|
56
|
-
"User-Agent": "codex-cli",
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
},
|
|
59
|
-
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
60
|
-
timeout: CONSUME_TIMEOUT_MS,
|
|
61
|
-
});
|
|
62
|
-
text = await res.text();
|
|
63
|
-
} catch (e) {
|
|
64
|
-
return { outcome: "error", detail: `reset endpoint unreachable: ${e instanceof Error ? e.message : String(e)}` };
|
|
65
|
-
}
|
|
66
|
-
if (!res.ok) {
|
|
67
|
-
const detail = `HTTP ${res.status}: ${safeErrorDetail({ text })}`;
|
|
68
|
-
if (res.status === 401 || res.status === 403) return { outcome: "auth_error", detail };
|
|
69
|
-
return { outcome: "error", detail };
|
|
70
|
-
}
|
|
71
|
-
const parsed = ConsumeResponseSchema.safeParse((() => {
|
|
72
|
-
try {
|
|
73
|
-
return JSON.parse(text);
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
})());
|
|
78
|
-
if (!parsed.success) return { outcome: "error", detail: `reset endpoint returned an unrecognized body (${text.length} bytes, withheld)` };
|
|
79
|
-
return { outcome: parsed.data.code, detail: `${parsed.data.code} (windows reset: ${parsed.data.windows_reset ?? "unknown"})` };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function rememberPendingKey(accountId: string, key: string, now: number): void {
|
|
83
|
-
const index = loadCodexAccounts();
|
|
84
|
-
const entry = index.accounts.find((a) => a.accountId === accountId);
|
|
85
|
-
if (!entry) return;
|
|
86
|
-
entry.bankedReset = { outcome: "error", at: now, key };
|
|
87
|
-
saveCodexAccounts({ index });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function consumeLiveCodexBankedReset(input: { account: CodexAccount; now: number }): Promise<CodexBankedClaim> {
|
|
91
|
-
const { account, now } = input;
|
|
92
|
-
const short = account.accountId.slice(0, 8);
|
|
93
|
-
const live = readLiveCodexAuth();
|
|
94
|
-
let result: ConsumeResult;
|
|
95
|
-
let idempotencyKey: string | null = null;
|
|
96
|
-
if (!live) {
|
|
97
|
-
result = { outcome: "auth_error", detail: "live auth.json vanished" };
|
|
98
|
-
} else if (codexIdentityOf({ auth: live }).accountId !== account.accountId) {
|
|
99
|
-
result = { outcome: "error", detail: "live codex credential belongs to another account" };
|
|
100
|
-
} else {
|
|
101
|
-
const prior = account.bankedReset;
|
|
102
|
-
const retrying = prior != null && prior.outcome === "error" && prior.key != null && now - prior.at < RETRY_KEY_TTL_MS;
|
|
103
|
-
idempotencyKey = retrying ? prior.key! : crypto.randomUUID();
|
|
104
|
-
if (!retrying) rememberPendingKey(account.accountId, idempotencyKey, now);
|
|
105
|
-
result = await consumeCodexBankedReset({ auth: live, idempotencyKey });
|
|
106
|
-
if (result.outcome === "already_redeemed" && retrying) result = { outcome: "reset", detail: `${result.detail}; the retried key had already completed` };
|
|
107
|
-
}
|
|
108
|
-
const outcome: CodexBankedResetOutcome = result.outcome;
|
|
109
|
-
const reset = outcome === "reset";
|
|
110
|
-
const index = loadCodexAccounts();
|
|
111
|
-
const entry = index.accounts.find((a) => a.accountId === account.accountId);
|
|
112
|
-
if (entry) {
|
|
113
|
-
entry.bankedReset = { outcome, at: now, ...(outcome === "error" && idempotencyKey != null ? { key: idempotencyKey } : {}) };
|
|
114
|
-
if (reset) {
|
|
115
|
-
delete entry.lastUsage;
|
|
116
|
-
delete entry.lastUsageAt;
|
|
117
|
-
}
|
|
118
|
-
saveCodexAccounts({ index });
|
|
119
|
-
}
|
|
120
|
-
log(reset ? "codexreset.consumed" : "codexreset.refused", { account: short, outcome, detail: result.detail.slice(0, 200) });
|
|
121
|
-
return reset ? "reset" : "pass";
|
|
122
|
-
}
|