tokenmaxxing 1.34.0 → 1.38.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 +3 -5
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +23 -40
- package/src/cli/auth.ts +38 -52
- package/src/cli/check.ts +2 -1
- package/src/cli/doctor.ts +13 -13
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +11 -45
- package/src/cli/render.ts +0 -7
- package/src/cli/rm.ts +24 -39
- package/src/cli/setuptoken.ts +16 -16
- package/src/cli/status.ts +95 -238
- package/src/cli/switch.ts +54 -46
- package/src/entries/codexstophook.ts +23 -23
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +5 -3
- package/src/entries/statusline.ts +17 -19
- package/src/entries/stopfailurehook.ts +9 -7
- package/src/entries/stophook.ts +5 -4
- package/src/entries/subagentstatusline.ts +2 -7
- package/src/lib/atomic.ts +2 -8
- package/src/lib/claude.ts +320 -0
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +345 -0
- package/src/lib/codexauth.ts +9 -7
- package/src/lib/codexoauth.ts +5 -11
- package/src/lib/codexpresence.ts +3 -19
- package/src/lib/codexusage.ts +17 -41
- package/src/lib/credstore.ts +3 -7
- package/src/lib/decide.ts +86 -174
- package/src/lib/http.ts +3 -6
- package/src/lib/install.ts +4 -7
- package/src/lib/oauth.ts +14 -12
- package/src/lib/paths.ts +15 -7
- package/src/lib/picker.ts +56 -37
- package/src/lib/proc.ts +4 -4
- package/src/lib/provider.ts +34 -0
- package/src/lib/sample.ts +33 -33
- package/src/lib/state.ts +51 -18
- package/src/lib/swap.ts +41 -38
- package/src/lib/types.ts +31 -51
- package/src/lib/usage.ts +28 -20
- package/src/main.ts +20 -20
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -36
- package/src/cli/codexswitch.ts +0 -113
- package/src/cli/onboard.ts +0 -92
- package/src/lib/codexdecide.ts +0 -157
- package/src/lib/codexpick.ts +0 -80
- package/src/lib/codexsample.ts +0 -48
- package/src/lib/codexstate.ts +0 -22
- package/src/lib/codexswap.ts +0 -70
package/src/lib/codexauth.ts
CHANGED
|
@@ -3,18 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import { codexPaths } from "./paths.ts";
|
|
6
|
-
import { CodexAuthJsonSchema, type CodexAuthJson } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
function isEnoent(e: unknown): boolean {
|
|
9
|
-
return e instanceof Error && "code" in e && e.code === "ENOENT";
|
|
10
|
-
}
|
|
6
|
+
import { CodexAuthJsonSchema, ErrnoSchema, type CodexAuthJson } from "./types.ts";
|
|
11
7
|
|
|
12
8
|
export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
|
|
13
9
|
let raw: string;
|
|
14
10
|
try {
|
|
15
11
|
raw = readFileSync(input.path, "utf8");
|
|
16
12
|
} catch (e) {
|
|
17
|
-
if (
|
|
13
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
18
14
|
throw e;
|
|
19
15
|
}
|
|
20
16
|
const parsed = JSON.parse(raw);
|
|
@@ -27,6 +23,12 @@ export function readLiveCodexAuth(): CodexAuthJson | null {
|
|
|
27
23
|
return readCodexAuthAt({ path: codexPaths.authJson });
|
|
28
24
|
}
|
|
29
25
|
|
|
26
|
+
export function liveCodexAccountId(): string | null {
|
|
27
|
+
const live = readLiveCodexAuth();
|
|
28
|
+
if (!live) return null;
|
|
29
|
+
return codexIdentityOf({ auth: live }).accountId;
|
|
30
|
+
}
|
|
31
|
+
|
|
30
32
|
export function writeLiveCodexAuth(input: { auth: CodexAuthJson }): void {
|
|
31
33
|
writeFileAtomic(codexPaths.authJson, JSON.stringify(CodexAuthJsonSchema.parse(input.auth), null, 2), 0o600);
|
|
32
34
|
}
|
|
@@ -40,7 +42,7 @@ export function readParkedCodexAuth(input: { credFile: string }): CodexAuthJson
|
|
|
40
42
|
try {
|
|
41
43
|
raw = readFileSync(parkedPath(input), "utf8");
|
|
42
44
|
} catch (e) {
|
|
43
|
-
if (
|
|
45
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
44
46
|
throw e;
|
|
45
47
|
}
|
|
46
48
|
return CodexAuthJsonSchema.parse(JSON.parse(raw));
|
package/src/lib/codexoauth.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { env } from "./paths.ts";
|
|
4
|
+
import { CodexAuthJsonSchema, JsonTextSchema, type CodexAuthJson } from "./types.ts";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const CLIENT_ID = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_CLIENT_ID) ?? "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
|
+
const TOKEN_URL = env("TOKENMAXXING_CODEX_TOKEN_URL", "https://auth.openai.com/oauth/token");
|
|
7
|
+
const CLIENT_ID = env("TOKENMAXXING_CODEX_CLIENT_ID", "app_EMoamEEZ73f0CkXaXp7hrann");
|
|
8
8
|
|
|
9
9
|
export class CodexInvalidGrantError extends Error {
|
|
10
10
|
constructor(detail: string) {
|
|
@@ -60,13 +60,7 @@ export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: numbe
|
|
|
60
60
|
throw new CodexRefreshFailedError(`HTTP ${res.status}: ${detail}`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const parsed = CodexRefreshResponseSchema.safeParse((
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(text);
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
})());
|
|
63
|
+
const parsed = CodexRefreshResponseSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
70
64
|
if (!parsed.success) {
|
|
71
65
|
throw new CodexRefreshFailedError("endpoint returned an unexpected body shape (withheld: may carry tokens)");
|
|
72
66
|
}
|
package/src/lib/codexpresence.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { codexPaths } from "./paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
6
|
import { pidExists, pidStartTime } from "./proc.ts";
|
|
7
|
+
import { ErrnoSchema, JsonTextSchema } from "./types.ts";
|
|
7
8
|
|
|
8
9
|
const PresenceSchema = z.object({
|
|
9
10
|
accountId: z.string(),
|
|
@@ -38,17 +39,10 @@ export function livingCodexPresences(): LivingPresence[] {
|
|
|
38
39
|
try {
|
|
39
40
|
raw = readFileSync(file, "utf8");
|
|
40
41
|
} catch (e) {
|
|
41
|
-
|
|
42
|
-
if (errno.success && errno.data.code === "ENOENT") continue;
|
|
42
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
|
|
43
43
|
throw e;
|
|
44
44
|
}
|
|
45
|
-
const parsed = PresenceSchema.safeParse((
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(raw);
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
})());
|
|
45
|
+
const parsed = PresenceSchema.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
52
46
|
if (!parsed.success) {
|
|
53
47
|
throw new Error(`${file} is not a readable presence record - it may belong to a RUNNING codex session, refusing to treat it as absent; remove the file (or respawn that session) to proceed`);
|
|
54
48
|
}
|
|
@@ -68,13 +62,3 @@ export function livingCodexPresences(): LivingPresence[] {
|
|
|
68
62
|
export function presentCodexAccountIds(): Set<string> {
|
|
69
63
|
return new Set(livingCodexPresences().map((presence) => presence.accountId));
|
|
70
64
|
}
|
|
71
|
-
|
|
72
|
-
export function targetableCodexAccounts<T extends { accountId: string }>(input: {
|
|
73
|
-
accounts: T[];
|
|
74
|
-
activeAccountId: string | null;
|
|
75
|
-
}): T[] {
|
|
76
|
-
const present = presentCodexAccountIds();
|
|
77
|
-
return input.accounts.filter(
|
|
78
|
-
(account) => account.accountId === input.activeAccountId || !present.has(account.accountId),
|
|
79
|
-
);
|
|
80
|
-
}
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, safeErrorDetail } from "./http.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { env } from "./paths.ts";
|
|
4
|
+
import { CodexUsageSchema, JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
|
|
4
5
|
import { codexIdentityOf } from "./codexauth.ts";
|
|
5
6
|
import { familyTokens } from "./usage.ts";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const USAGE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_USAGE_URL) ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
8
|
+
const USAGE_URL = env("TOKENMAXXING_CODEX_USAGE_URL", "https://chatgpt.com/backend-api/wham/usage");
|
|
9
9
|
|
|
10
10
|
export class CodexUsageReadError extends Error {
|
|
11
11
|
constructor(detail: string) {
|
|
@@ -35,21 +35,23 @@ const WireUsageSchema = z.looseObject({
|
|
|
35
35
|
.nullish(),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undefined):
|
|
39
|
-
const out:
|
|
38
|
+
function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undefined, name: string | null, at: number): Window[] {
|
|
39
|
+
const out: Window[] = [];
|
|
40
40
|
for (const wire of [rateLimit?.primary_window, rateLimit?.secondary_window]) {
|
|
41
41
|
if (wire == null) continue;
|
|
42
42
|
out.push({
|
|
43
|
+
name,
|
|
43
44
|
usedPercentage: wire.used_percent,
|
|
44
45
|
resetsAt: wire.reset_at != null ? wire.reset_at * 1000 : null,
|
|
45
46
|
windowSeconds: wire.limit_window_seconds ?? null,
|
|
47
|
+
sampledAt: at,
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
return out;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<CodexUsage> {
|
|
52
|
-
const { auth } = input;
|
|
53
|
+
export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }): Promise<CodexUsage> {
|
|
54
|
+
const { auth, at } = input;
|
|
53
55
|
const identity = codexIdentityOf({ auth });
|
|
54
56
|
let res: Response;
|
|
55
57
|
try {
|
|
@@ -68,53 +70,27 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<C
|
|
|
68
70
|
if (!res.ok) {
|
|
69
71
|
throw new CodexUsageReadError(`HTTP ${res.status}: ${safeErrorDetail({ text })}`);
|
|
70
72
|
}
|
|
71
|
-
const parsed = WireUsageSchema.safeParse((
|
|
72
|
-
try {
|
|
73
|
-
return JSON.parse(text);
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
})());
|
|
73
|
+
const parsed = WireUsageSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
78
74
|
if (!parsed.success) {
|
|
79
75
|
throw new CodexUsageReadError("endpoint returned an unexpected body shape (withheld)");
|
|
80
76
|
}
|
|
81
77
|
const wire = parsed.data;
|
|
82
78
|
|
|
83
|
-
const perLimit: Record<string, CodexWindow[]> = {};
|
|
84
|
-
for (const row of wire.additional_rate_limits ?? []) {
|
|
85
|
-
const windows = toWindows(row.rate_limit);
|
|
86
|
-
if (windows.length > 0) perLimit[row.limit_name] = windows;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
79
|
return CodexUsageSchema.parse({
|
|
90
80
|
accountId: wire.account_id,
|
|
91
81
|
email: wire.email ?? null,
|
|
92
82
|
planType: wire.plan_type ?? null,
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
windows: [
|
|
84
|
+
...toWindows(wire.rate_limit, null, at),
|
|
85
|
+
...(wire.additional_rate_limits ?? []).flatMap((row) => toWindows(row.rate_limit, row.limit_name, at)),
|
|
86
|
+
],
|
|
95
87
|
});
|
|
96
88
|
}
|
|
97
89
|
|
|
98
90
|
const LIMIT_LABEL_ABBREVIATIONS = new Map([["reserve", "rsrv"]]);
|
|
99
91
|
|
|
100
|
-
export function codexLimitLabel(
|
|
101
|
-
const tokens = familyTokens(
|
|
102
|
-
const label = tokens.at(-1) ??
|
|
92
|
+
export function codexLimitLabel(limitName: string): string {
|
|
93
|
+
const tokens = familyTokens(limitName).filter((t) => Number.isNaN(Number(t)));
|
|
94
|
+
const label = tokens.at(-1) ?? limitName.trim().toLowerCase();
|
|
103
95
|
return LIMIT_LABEL_ABBREVIATIONS.get(label) ?? label;
|
|
104
96
|
}
|
|
105
|
-
|
|
106
|
-
const SESSION_WINDOW_MAX_S = 6 * 3600;
|
|
107
|
-
|
|
108
|
-
export function isSessionWindow(input: { window: CodexWindow }): boolean {
|
|
109
|
-
const seconds = input.window.windowSeconds;
|
|
110
|
-
return seconds != null && seconds <= SESSION_WINDOW_MAX_S;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function weeklyWindowOf(input: { aggregate: CodexWindow[] }): CodexWindow | null {
|
|
114
|
-
let best: CodexWindow | null = null;
|
|
115
|
-
for (const window of input.aggregate) {
|
|
116
|
-
if (isSessionWindow({ window })) continue;
|
|
117
|
-
if (best == null || (window.windowSeconds ?? 0) > (best.windowSeconds ?? 0)) best = window;
|
|
118
|
-
}
|
|
119
|
-
return best;
|
|
120
|
-
}
|
package/src/lib/credstore.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import * as kc from "./keychain.ts";
|
|
6
6
|
import { credDir, keychain as kcNames, namespacedCredService, paths } from "./paths.ts";
|
|
7
|
-
import { CredentialBlobSchema } from "./types.ts";
|
|
7
|
+
import { CredentialBlobSchema, ErrnoSchema } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
const CredTargetSchema = z.discriminatedUnion("kind", [
|
|
10
10
|
z.object({ kind: z.literal("keychain"), service: z.string(), account: z.string() }),
|
|
@@ -16,16 +16,12 @@ const darwin = process.platform === "darwin";
|
|
|
16
16
|
|
|
17
17
|
const BlobRecordSchema = z.record(z.string(), z.unknown());
|
|
18
18
|
|
|
19
|
-
function isEnoent(e: unknown): boolean {
|
|
20
|
-
return e instanceof Error && "code" in e && e.code === "ENOENT";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
export async function readItem(t: CredTarget): Promise<string | null> {
|
|
24
20
|
if (t.kind === "keychain") return kc.readItem(t);
|
|
25
21
|
try {
|
|
26
22
|
return readFileSync(t.path, "utf8");
|
|
27
23
|
} catch (e) {
|
|
28
|
-
if (
|
|
24
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
29
25
|
throw e;
|
|
30
26
|
}
|
|
31
27
|
}
|
|
@@ -42,7 +38,7 @@ export async function deleteItem(t: CredTarget): Promise<boolean> {
|
|
|
42
38
|
unlinkSync(t.path);
|
|
43
39
|
return true;
|
|
44
40
|
} catch (e) {
|
|
45
|
-
if (
|
|
41
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return false;
|
|
46
42
|
throw e;
|
|
47
43
|
}
|
|
48
44
|
}
|
package/src/lib/decide.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { maxBy } from "es-toolkit";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { withLock } from "./lock.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { isSkippableSwapError, performSwap } from "./swap.ts";
|
|
8
|
-
import { isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
|
|
9
|
-
import { familyTokens, gatedFamilies, keepRows, probeUsage } from "./usage.ts";
|
|
3
|
+
import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, saveAccounts, saveDepletedWait } from "./state.ts";
|
|
4
|
+
import { isExhausted, limitWindows, nextWeeklyReset, pickBest, pickEarliestReset, sessionWindow, thresholdBars, usableAt, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
|
+
import { familyTokens } from "./usage.ts";
|
|
10
6
|
import { log } from "./log.ts";
|
|
11
|
-
import
|
|
7
|
+
import type { Observation, Provider } from "./provider.ts";
|
|
8
|
+
import { AccountSchema, type Account, type EnforcedLimit } from "./types.ts";
|
|
12
9
|
|
|
13
10
|
const SwapDecisionSchema = z.object({
|
|
14
11
|
swapped: z.boolean(),
|
|
@@ -21,201 +18,132 @@ export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
|
|
|
21
18
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
22
19
|
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
23
20
|
|
|
24
|
-
function
|
|
25
|
-
const { window: w, windowMs, sampledAt, now } = input;
|
|
26
|
-
if (w.resetsAt != null) return w.resetsAt <= now ? 0 : w.usedPercentage;
|
|
27
|
-
if (now >= sampledAt + windowMs) return 0;
|
|
28
|
-
return w.usedPercentage;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function rowsSampledAt(account: Account, now: number): number {
|
|
32
|
-
return account.lastUsage?.rowsAt ?? account.lastUsageAt ?? now;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function capForFamily(account: Account, family: string, now: number): UsageWindow | undefined {
|
|
36
|
-
const rows = Object.entries(account.lastUsage?.perModel ?? {})
|
|
37
|
-
.filter(([k]) => familyTokens(k).includes(family))
|
|
38
|
-
.map(([, w]) => w);
|
|
39
|
-
return maxBy(rows, (w) => liveUsed({ window: w, windowMs: WEEK_MS, sampledAt: rowsSampledAt(account, now), now }));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isOver(u: UsageState | null, account: Account | undefined, bars: Thresholds, cfg: Config, now: number): boolean {
|
|
21
|
+
function isOver(account: Account | undefined, observed: Observation | null, ctx: PickCtx): boolean {
|
|
43
22
|
if (!account) return false;
|
|
44
|
-
if (account.
|
|
45
|
-
if (
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now }) >= bars.weekly
|
|
49
|
-
) return true;
|
|
50
|
-
for (const family of gatedFamilies(u.model, cfg.policy.switchModels)) {
|
|
51
|
-
const cap = capForFamily(account, family, now);
|
|
52
|
-
if (cap && liveUsed({ window: cap, windowMs: WEEK_MS, sampledAt: rowsSampledAt(account, now), now }) >= bars.weekly) return true;
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function needsPerModel(u: UsageState | null, cfg: Config): boolean {
|
|
58
|
-
return u != null && gatedFamilies(u.model, cfg.policy.switchModels).length > 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const SnapshotsSchema = z.object({
|
|
62
|
-
u: UsageStateSchema.nullable(),
|
|
63
|
-
uAt: z.number().nullable(),
|
|
64
|
-
});
|
|
65
|
-
type Snapshots = z.infer<typeof SnapshotsSchema>;
|
|
66
|
-
|
|
67
|
-
function usageFresh(u: UsageState | null, uAt: number | null, account: string | null, ttl: number, now: number): boolean {
|
|
68
|
-
return u != null && u.account === account && uAt != null && now - uAt <= ttl;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function freshest(u: UsageState | null, uAt: number | null, account: Account | undefined): UsageState | null {
|
|
72
|
-
if (!u || uAt == null) return u;
|
|
73
|
-
const stored = account?.lastUsage;
|
|
74
|
-
if (stored && account.lastUsageAt != null && u.account === account.accountUuid && uAt < account.lastUsageAt) {
|
|
75
|
-
return { ...u, fiveHour: stored.fiveHour, sevenDay: stored.sevenDay, ts: account.lastUsageAt };
|
|
76
|
-
}
|
|
77
|
-
return { ...u, ts: uAt };
|
|
23
|
+
if (account.needsReauth === true) return true;
|
|
24
|
+
if (account.enforcedUntil != null && account.enforcedUntil > ctx.now) return true;
|
|
25
|
+
if (!observed) return false;
|
|
26
|
+
return isExhausted({ ...account, windows: observed.windows }, ctx);
|
|
78
27
|
}
|
|
79
28
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (teed && usageFresh(teed.state, teed.at, account, ttl, ts)) {
|
|
96
|
-
u = teed.state;
|
|
97
|
-
uAt = teed.at;
|
|
98
|
-
} else {
|
|
99
|
-
u = { fiveHour: full.fiveHour, sevenDay: full.sevenDay, account, ts, model: null };
|
|
100
|
-
writeUsage(u);
|
|
101
|
-
uAt = ts;
|
|
102
|
-
}
|
|
103
|
-
const expected = gatedFamilies(u?.model ?? null, cfg.policy.switchModels);
|
|
104
|
-
const rows = Object.keys(full.perModel);
|
|
105
|
-
if (expected.length > 0 && !expected.some((f) => rows.some((k) => familyTokens(k).includes(f)))) {
|
|
106
|
-
log("usage.no_permodel_row", { families: expected.join(","), rows: rows.join(",") });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
await withLock(paths.lockFile, () => {
|
|
110
|
-
const idx = loadAccounts();
|
|
111
|
-
const a = idx.accounts.find((x) => x.accountUuid === account);
|
|
112
|
-
if (!a) return;
|
|
113
|
-
a.lastProbeAt = startedAt;
|
|
114
|
-
if (full) {
|
|
115
|
-
if (!a.lastUsage) a.lastUsageAt = startedAt;
|
|
116
|
-
a.lastUsage = keepRows({ ...(a.lastUsage ?? full), perModel: full.perModel }, a.lastUsage, startedAt);
|
|
117
|
-
}
|
|
118
|
-
saveAccounts(idx);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return { u, uAt };
|
|
29
|
+
function enforcedWall(limit: EnforcedLimit, account: Account, now: number): number {
|
|
30
|
+
const { family } = limit;
|
|
31
|
+
const familyReset =
|
|
32
|
+
family == null
|
|
33
|
+
? null
|
|
34
|
+
: limitWindows(account)
|
|
35
|
+
.filter((w) => familyTokens(w.name ?? "").includes(family))
|
|
36
|
+
.map((w) => w.resetsAt)
|
|
37
|
+
.find((r): r is number => r != null) ?? null;
|
|
38
|
+
const session = sessionWindow(account)?.resetsAt ?? null;
|
|
39
|
+
const cachedReset =
|
|
40
|
+
limit.kind === "session"
|
|
41
|
+
? session != null && session > now ? session : null
|
|
42
|
+
: nextWeeklyReset(familyReset ?? weeklyWindow(account)?.resetsAt ?? null, now);
|
|
43
|
+
return limit.resetsAt ?? cachedReset ?? now + (limit.kind === "session" ? FIVE_HOURS_MS : WEEK_MS);
|
|
123
44
|
}
|
|
124
45
|
|
|
125
|
-
export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = false, enforced: EnforcedLimit | null = null): Promise<SwapDecision> {
|
|
126
|
-
const
|
|
46
|
+
export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), anticipatory = false, enforced: EnforcedLimit | null = null): Promise<SwapDecision> {
|
|
47
|
+
const activeId = p.liveId();
|
|
127
48
|
|
|
128
|
-
const lastSwapAt = loadLastSwapAt();
|
|
49
|
+
const lastSwapAt = loadLastSwapAt(p.pool);
|
|
129
50
|
if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
130
|
-
return depletedReplay(now) ?? { swapped: false, account: null, reason: "post-swap-cooldown" };
|
|
51
|
+
return depletedReplay(p, now) ?? { swapped: false, account: null, reason: "post-swap-cooldown" };
|
|
131
52
|
}
|
|
132
53
|
|
|
133
54
|
const cfg = loadConfig();
|
|
134
55
|
const bars = thresholdBars(cfg);
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (!enforced && !isOver(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const replay = depletedReplay(now);
|
|
56
|
+
const stored0 = loadAccounts(p.pool).accounts.find((a) => a.id === activeId);
|
|
57
|
+
const walled0 = stored0?.enforcedUntil != null && stored0.enforcedUntil > now;
|
|
58
|
+
const observed = stored0 ? await p.observeLive(stored0, cfg, now, { probe: enforced == null && !walled0 }) : null;
|
|
59
|
+
const stored = loadAccounts(p.pool).accounts.find((a) => a.id === activeId);
|
|
60
|
+
|
|
61
|
+
if (!enforced && !isOver(stored, observed, { now, thresholds: bars, currentId: activeId, families: p.gatedFamilies(cfg) })) {
|
|
62
|
+
if (!observed) {
|
|
63
|
+
const replay = depletedReplay(p, now);
|
|
144
64
|
if (replay) return replay;
|
|
145
65
|
}
|
|
146
66
|
return { swapped: false, account: null, reason: "under-threshold-or-stale" };
|
|
147
67
|
}
|
|
148
68
|
|
|
149
|
-
return withLock(
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
69
|
+
return withLock(p.pool.lockFile, async () => {
|
|
70
|
+
const lastSwapAt2 = loadLastSwapAt(p.pool);
|
|
71
|
+
if (!enforced && lastSwapAt2 != null && now - lastSwapAt2 < POST_SWAP_COOLDOWN_MS) {
|
|
72
|
+
return { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
73
|
+
}
|
|
74
|
+
const idx = loadAccounts(p.pool);
|
|
75
|
+
const id2 = p.liveId();
|
|
76
|
+
const active = id2 ? idx.accounts.find((a) => a.id === id2) : undefined;
|
|
155
77
|
|
|
156
|
-
const origin = enforced ? idx.accounts.find((a) => a.
|
|
78
|
+
const origin = enforced ? idx.accounts.find((a) => a.id === enforced.account) : undefined;
|
|
157
79
|
const prior = origin?.enforcedUntil != null && origin.enforcedUntil > now;
|
|
158
80
|
if (enforced && origin) {
|
|
159
81
|
origin.enforcedUntil = Math.max(origin.enforcedUntil ?? 0, enforcedWall(enforced, origin, now));
|
|
160
82
|
origin.lastProbeAt = now;
|
|
161
|
-
saveAccounts(idx);
|
|
83
|
+
saveAccounts(p.pool, idx);
|
|
162
84
|
log("usage.enforced_limit", { kind: enforced.kind, family: enforced.family ?? undefined, resetsAt: origin.enforcedUntil, blind: prior || enforced.blind, live: origin === active });
|
|
163
85
|
}
|
|
164
86
|
const enforced2 = enforced && origin && origin === active ? enforced : null;
|
|
165
87
|
|
|
166
|
-
if (
|
|
88
|
+
if (id2 != null && !active) {
|
|
167
89
|
return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
|
|
168
90
|
}
|
|
169
91
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
active.
|
|
173
|
-
|
|
92
|
+
const obs2 = active ? await p.observeLive(active, cfg, now, { probe: false }) : null;
|
|
93
|
+
if (active && obs2 && (active.lastUsageAt == null || obs2.at > active.lastUsageAt)) {
|
|
94
|
+
active.windows = p.mergeWindows(obs2.windows, active.windows);
|
|
95
|
+
active.lastUsageAt = obs2.at;
|
|
96
|
+
saveAccounts(p.pool, idx);
|
|
174
97
|
}
|
|
175
98
|
|
|
99
|
+
const families = p.gatedFamilies(cfg);
|
|
176
100
|
const walled = active?.enforcedUntil != null && active.enforcedUntil > now;
|
|
177
101
|
const blindScreen = (enforced != null && (prior || enforced.blind)) || (walled && !enforced2);
|
|
178
|
-
const
|
|
102
|
+
const screened = blindScreen && families != null ? cfg.policy.switchModels : families;
|
|
179
103
|
const family = enforced?.family ?? null;
|
|
180
|
-
const switchFamilies = family != null && !
|
|
104
|
+
const switchFamilies = screened == null ? null : family != null && !screened.includes(family) ? [...screened, family] : screened;
|
|
181
105
|
|
|
182
|
-
const seatExhausted = active != null && isExhausted(active, { now, thresholds: bars,
|
|
183
|
-
if (!enforced2 && !isOver(
|
|
184
|
-
return depletedReplay(now) ?? { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
106
|
+
const seatExhausted = active != null && isExhausted(active, { now, thresholds: bars, currentId: id2, families: switchFamilies });
|
|
107
|
+
if (!enforced2 && !isOver(active, obs2, { now, thresholds: bars, currentId: id2, families }) && !(enforced && seatExhausted)) {
|
|
108
|
+
return depletedReplay(p, now) ?? { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
185
109
|
}
|
|
186
110
|
|
|
187
|
-
const seatOf = (
|
|
188
|
-
|
|
189
|
-
idx2.accounts.find((a) => a.accountUuid === idx2.activeAccountUuid) ??
|
|
190
|
-
null;
|
|
111
|
+
const seatOf = (cur: { activeId: string | null; accounts: Account[] }): Account | null =>
|
|
112
|
+
cur.accounts.find((a) => a.id === id2) ?? cur.accounts.find((a) => a.id === cur.activeId) ?? null;
|
|
191
113
|
|
|
114
|
+
const present = p.presentIds();
|
|
192
115
|
const rejected = new Set<string>();
|
|
193
|
-
const usable = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.
|
|
116
|
+
const usable = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (a.id === id2 || !present.has(a.id)));
|
|
194
117
|
const skipOrThrow = (e: unknown, candidate: Account): void => {
|
|
195
|
-
if (
|
|
196
|
-
rejected.add(candidate.
|
|
197
|
-
log("decide.candidate_rejected", { account: candidate.
|
|
118
|
+
if (p.classifySwapError(e) === "fatal") throw e;
|
|
119
|
+
rejected.add(candidate.id);
|
|
120
|
+
log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error: e instanceof Error ? e.message : String(e) });
|
|
198
121
|
};
|
|
199
122
|
while (true) {
|
|
200
|
-
const cur = loadAccounts();
|
|
123
|
+
const cur = loadAccounts(p.pool);
|
|
201
124
|
const seat = seatOf(cur);
|
|
202
|
-
const ctx = { now, thresholds: bars,
|
|
125
|
+
const ctx: PickCtx = { now, thresholds: bars, currentId: seat?.id ?? null, families: switchFamilies };
|
|
203
126
|
const best = pickBest(usable(cur.accounts), ctx);
|
|
204
127
|
if (!best) break;
|
|
205
128
|
try {
|
|
206
|
-
await
|
|
129
|
+
await p.swap(best);
|
|
207
130
|
} catch (e) {
|
|
208
131
|
skipOrThrow(e, best);
|
|
209
132
|
continue;
|
|
210
133
|
}
|
|
211
|
-
log("decide.swap", { account: best.
|
|
134
|
+
log("decide.swap", { account: best.id.slice(0, 8), enforced: enforced2 != null });
|
|
212
135
|
return { swapped: true, account: best, reason: "swapped" };
|
|
213
136
|
}
|
|
214
137
|
|
|
138
|
+
if (!p.waitsWhenDepleted) {
|
|
139
|
+
log("decide.depleted", { waitUntil: 0 });
|
|
140
|
+
return { swapped: false, account: null, reason: "all-depleted" };
|
|
141
|
+
}
|
|
142
|
+
|
|
215
143
|
while (true) {
|
|
216
|
-
const fresh = loadAccounts();
|
|
144
|
+
const fresh = loadAccounts(p.pool);
|
|
217
145
|
const current = seatOf(fresh);
|
|
218
|
-
const ctx = { now, thresholds: bars,
|
|
146
|
+
const ctx: PickCtx = { now, thresholds: bars, currentId: current?.id ?? null, families: switchFamilies };
|
|
219
147
|
const currentAt = current ? usableAt(current, ctx) : Number.POSITIVE_INFINITY;
|
|
220
148
|
const other = pickEarliestReset(usable(fresh.accounts), ctx);
|
|
221
149
|
|
|
@@ -230,48 +158,32 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
230
158
|
return { swapped: false, account: null, reason: "all-depleted", ...(Number.isFinite(waitUntil) ? { waitUntil } : {}) };
|
|
231
159
|
}
|
|
232
160
|
|
|
233
|
-
const isCurrent = target.
|
|
161
|
+
const isCurrent = target.id === (current?.id ?? null);
|
|
234
162
|
if (!isCurrent && !anticipatory) {
|
|
235
|
-
log("decide.depleted_no_park", { account: target.
|
|
163
|
+
log("decide.depleted_no_park", { account: target.id.slice(0, 8), waitUntil });
|
|
236
164
|
return { swapped: false, account: null, reason: "all-depleted", waitUntil };
|
|
237
165
|
}
|
|
238
166
|
if (!isCurrent) {
|
|
239
167
|
try {
|
|
240
|
-
await
|
|
168
|
+
await p.swap(target);
|
|
241
169
|
} catch (e) {
|
|
242
170
|
skipOrThrow(e, target);
|
|
243
171
|
continue;
|
|
244
172
|
}
|
|
245
173
|
}
|
|
246
|
-
saveDepletedWait({ waitUntil,
|
|
247
|
-
log("decide.depleted_wait", { account: target.
|
|
174
|
+
saveDepletedWait({ waitUntil, id: target.id, ts: now });
|
|
175
|
+
log("decide.depleted_wait", { account: target.id.slice(0, 8), waitUntil });
|
|
248
176
|
return { swapped: !isCurrent, account: target, reason: "depleted-wait", waitUntil };
|
|
249
177
|
}
|
|
250
178
|
});
|
|
251
179
|
}
|
|
252
180
|
|
|
253
|
-
function depletedReplay(now: number): SwapDecision | null {
|
|
181
|
+
function depletedReplay(p: Provider, now: number): SwapDecision | null {
|
|
182
|
+
if (!p.waitsWhenDepleted) return null;
|
|
254
183
|
const rec = loadDepletedWait();
|
|
255
184
|
if (!rec || rec.waitUntil <= now) return null;
|
|
256
|
-
const account = loadAccounts().accounts.find((a) => a.
|
|
185
|
+
const account = loadAccounts(p.pool).accounts.find((a) => a.id === rec.id) ?? null;
|
|
257
186
|
if (!account) return null;
|
|
258
|
-
if (account.
|
|
187
|
+
if (account.id !== p.liveId()) return null;
|
|
259
188
|
return { swapped: false, account, reason: "depleted-wait", waitUntil: rec.waitUntil };
|
|
260
189
|
}
|
|
261
|
-
|
|
262
|
-
function enforcedWall(limit: EnforcedLimit, account: Account, now: number): number {
|
|
263
|
-
const cached = account.lastUsage;
|
|
264
|
-
const { family } = limit;
|
|
265
|
-
const familyReset =
|
|
266
|
-
family == null
|
|
267
|
-
? null
|
|
268
|
-
: Object.entries(cached?.perModel ?? {})
|
|
269
|
-
.filter(([k]) => familyTokens(k).includes(family))
|
|
270
|
-
.map(([, w]) => w.resetsAt)
|
|
271
|
-
.find((r): r is number => r != null) ?? null;
|
|
272
|
-
const cachedReset =
|
|
273
|
-
limit.kind === "session"
|
|
274
|
-
? cached?.fiveHour.resetsAt != null && cached.fiveHour.resetsAt > now ? cached.fiveHour.resetsAt : null
|
|
275
|
-
: nextWeeklyReset(familyReset ?? cached?.sevenDay.resetsAt ?? null, now);
|
|
276
|
-
return limit.resetsAt ?? cachedReset ?? now + (limit.kind === "session" ? FIVE_HOURS_MS : WEEK_MS);
|
|
277
|
-
}
|
package/src/lib/http.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ky from "ky";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { JsonTextSchema } from "./types.ts";
|
|
3
4
|
|
|
4
5
|
const OAuthErrorBodySchema = z.looseObject({
|
|
5
6
|
error: z.string(),
|
|
@@ -20,12 +21,8 @@ const ErrorDetailSchema = z.object({ code: z.string().nullable(), fields: z.arra
|
|
|
20
21
|
type ErrorDetail = z.infer<typeof ErrorDetailSchema>;
|
|
21
22
|
|
|
22
23
|
function parseErrorBody(text: string): ErrorDetail | null {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
json = JSON.parse(text);
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
24
|
+
const json = JsonTextSchema.safeParse(text).data;
|
|
25
|
+
if (json === undefined) return null;
|
|
29
26
|
const oauth = OAuthErrorBodySchema.safeParse(json);
|
|
30
27
|
if (oauth.success) return { code: oauth.data.error, fields: [oauth.data.error, oauth.data.error_description ?? ""] };
|
|
31
28
|
const nested = NestedErrorBodySchema.safeParse(json);
|