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/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 killer = setTimeout(() => p.kill("SIGKILL"), PROBE_KILL_MS);
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
 
@@ -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
- }
@@ -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
- }