pi-chatgpt-pool 0.1.0 → 0.2.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/README.md +21 -2
- package/package.json +4 -2
- package/src/format.ts +30 -0
- package/src/index.ts +69 -18
- package/src/panel.ts +111 -0
- package/src/pool.ts +4 -0
package/README.md
CHANGED
|
@@ -32,14 +32,33 @@ The pool offers the ChatGPT subscription models: the OpenAI models that pi also
|
|
|
32
32
|
|
|
33
33
|
| Command | What it does |
|
|
34
34
|
| --- | --- |
|
|
35
|
-
| `/chatgpt-pool` | Open the pool
|
|
35
|
+
| `/chatgpt-pool` | Open the pool panel (see below) |
|
|
36
36
|
| `/chatgpt-pool add <label>` | Add an account slot. Sign in to it with `/login` |
|
|
37
37
|
| `/chatgpt-pool remove <label>` | Remove an account. Run `/logout` for it first to delete its token |
|
|
38
38
|
| `/chatgpt-pool spend <label>` | Spend one banked reset on that account, after you confirm |
|
|
39
39
|
| `/chatgpt-pool strategy <name>` | Set the routing strategy: `fill-first`, `round-robin`, `least-used`, or `use-it-or-lose-it` |
|
|
40
40
|
| `/chatgpt-pool reset` | Forget recorded limits, for example after a limit clears early |
|
|
41
41
|
|
|
42
|
-
The command autocompletes its actions and account labels.
|
|
42
|
+
The command autocompletes its actions and account labels.
|
|
43
|
+
|
|
44
|
+
## Pool panel
|
|
45
|
+
|
|
46
|
+
`/chatgpt-pool` opens a panel with every account, its state (ready, limited with a countdown, or not signed in), a usage bar per window with the time until it resets, and its banked resets.
|
|
47
|
+
|
|
48
|
+
| Key | Action |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `↑` `↓` or `j` `k` | Select an account |
|
|
51
|
+
| `s` | Spend a banked reset on the selected account, after you confirm |
|
|
52
|
+
| `c` | Clear the selected account's recorded limit |
|
|
53
|
+
| `d` | Remove the selected account |
|
|
54
|
+
| `a` | Add an account |
|
|
55
|
+
| `t` | Choose the strategy |
|
|
56
|
+
| `r` | Refresh usage |
|
|
57
|
+
| `esc` or `q` | Close |
|
|
58
|
+
|
|
59
|
+
Over RPC the same actions are offered as a list instead.
|
|
60
|
+
|
|
61
|
+
While a pooled model is selected, the footer shows the account in use with its usage and reset time, for example `chatgpt: work 42% · resets in 3d 4h`, plus how many accounts are limited. When a conversation moves to another account, pi shows a notice such as `ChatGPT pool: personal is limited (back in 1h 30m), switched to work.`
|
|
43
62
|
|
|
44
63
|
`/chatgpt-pool` and `spend` need the interactive UI; in `--print` or `--mode json` they stop with an error instead of spending anything. From a shell, use pi's own commands:
|
|
45
64
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-chatgpt-pool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pool several Sign in with ChatGPT accounts in pi, with routing strategies, failover, and banked resets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -37,11 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-ai": "*",
|
|
40
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
40
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
41
|
+
"@earendil-works/pi-tui": "*"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@earendil-works/pi-ai": "^0.99.1",
|
|
44
45
|
"@earendil-works/pi-coding-agent": "^0.99.1",
|
|
46
|
+
"@earendil-works/pi-tui": "^0.99.1",
|
|
45
47
|
"@types/node": "^24.0.0",
|
|
46
48
|
"typescript": "^5.9.0"
|
|
47
49
|
},
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function formatWindow(seconds: number): string {
|
|
2
|
+
if (seconds % 604800 === 0) return seconds === 604800 ? "weekly" : `${seconds / 604800}-week`;
|
|
3
|
+
if (seconds % 86400 === 0) return seconds === 86400 ? "daily" : `${seconds / 86400}-day`;
|
|
4
|
+
if (seconds % 3600 === 0) return `${seconds / 3600}h`;
|
|
5
|
+
return `${Math.round(seconds / 60)}m`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatReset(at: number): string {
|
|
9
|
+
const date = new Date(at);
|
|
10
|
+
const time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
11
|
+
return date.toDateString() === new Date().toDateString()
|
|
12
|
+
? `at ${time}`
|
|
13
|
+
: `${date.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" })} at ${time}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatIn(ms: number): string {
|
|
17
|
+
const minutes = Math.max(1, Math.ceil(ms / 60_000));
|
|
18
|
+
const days = Math.floor(minutes / 1440);
|
|
19
|
+
const hours = Math.floor((minutes % 1440) / 60);
|
|
20
|
+
const rest = minutes % 60;
|
|
21
|
+
if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
22
|
+
if (hours > 0) return rest > 0 ? `${hours}h ${rest}m` : `${hours}h`;
|
|
23
|
+
return `${rest}m`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function usageBar(percent: number, width: number) {
|
|
27
|
+
const clamped = Math.min(100, Math.max(0, percent));
|
|
28
|
+
const filled = Math.round((clamped / 100) * width);
|
|
29
|
+
return { filled: "█".repeat(filled), empty: "░".repeat(width - filled) };
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { BorderedLoader, type ExtensionAPI, type ExtensionContext, getAgentDir }
|
|
|
6
6
|
import {
|
|
7
7
|
type Account,
|
|
8
8
|
isStrategy,
|
|
9
|
+
failoverMessage,
|
|
9
10
|
limitCooldown,
|
|
10
11
|
nextAccountId,
|
|
11
12
|
orderAccounts,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
type Strategy,
|
|
16
17
|
type UsageSnapshot,
|
|
17
18
|
} from "./pool.ts";
|
|
19
|
+
import { formatIn, formatReset, formatWindow } from "./format.ts";
|
|
20
|
+
import { type AccountStatus, type PanelAction, PoolPanel } from "./panel.ts";
|
|
18
21
|
import { fetchUsage, spendReset, type Usage } from "./usage.ts";
|
|
19
22
|
|
|
20
23
|
const POOL_PROVIDER = "chatgpt";
|
|
@@ -96,20 +99,6 @@ function accountProvider(openai: Provider, account: Account, onReset: ResetListe
|
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
function formatWindow(seconds: number): string {
|
|
100
|
-
if (seconds % 604800 === 0) return seconds === 604800 ? "weekly" : `${seconds / 604800}-week`;
|
|
101
|
-
if (seconds % 3600 === 0) return `${seconds / 3600}h`;
|
|
102
|
-
return `${Math.round(seconds / 60)}m`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function formatReset(at: number): string {
|
|
106
|
-
const date = new Date(at);
|
|
107
|
-
const time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
108
|
-
return date.toDateString() === new Date().toDateString()
|
|
109
|
-
? `at ${time}`
|
|
110
|
-
: `${date.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" })} at ${time}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
102
|
export default function chatgptPool(pi: ExtensionAPI) {
|
|
114
103
|
const providers = builtinProviders();
|
|
115
104
|
const openai = providers.find((provider) => provider.id === "openai");
|
|
@@ -142,6 +131,14 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
142
131
|
);
|
|
143
132
|
return new Map([...usageCache].map(([id, entry]) => [id, entry.snapshot]));
|
|
144
133
|
};
|
|
134
|
+
const refreshUsage = async (ctx: ExtensionContext, accountId: string) => {
|
|
135
|
+
const cached = usageCache.get(accountId);
|
|
136
|
+
if (cached && Date.now() - cached.at < USAGE_TTL_MS) return;
|
|
137
|
+
const token = await ctx.modelRegistry.getApiKeyForProvider(accountId).catch(() => undefined);
|
|
138
|
+
if (!token) return;
|
|
139
|
+
await fetchUsage(token).then((usage) => rememberUsage(accountId, usage), () => undefined);
|
|
140
|
+
};
|
|
141
|
+
|
|
145
142
|
const reportedResets = new Map<string, number>();
|
|
146
143
|
const onReset: ResetListener = (accountId, resetsAt) => reportedResets.set(accountId, resetsAt);
|
|
147
144
|
|
|
@@ -165,11 +162,16 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
165
162
|
if (provider !== POOL_PROVIDER) return ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
166
163
|
const now = Date.now();
|
|
167
164
|
const active = accounts.find((account) => account.id === lastRouted);
|
|
165
|
+
const snapshot = active && usageCache.get(active.id)?.snapshot;
|
|
168
166
|
const limited = accounts.filter((account) => (account.resetsAt ?? 0) > now);
|
|
169
167
|
const parts = [`chatgpt: ${active?.label ?? "pool"}`];
|
|
168
|
+
if (snapshot) {
|
|
169
|
+
parts[0] += ` ${Math.round(snapshot.usedPercent)}%`;
|
|
170
|
+
parts.push(`resets in ${formatIn(snapshot.resetsAt - now)}`);
|
|
171
|
+
}
|
|
170
172
|
if (limited.length > 0) {
|
|
171
173
|
const next = Math.min(...limited.map((account) => account.resetsAt ?? now));
|
|
172
|
-
parts.push(`${limited.length} limited, next back ${
|
|
174
|
+
parts.push(`${limited.length} limited, next back in ${formatIn(next - now)}`);
|
|
173
175
|
}
|
|
174
176
|
ctx.ui.setStatus(STATUS_KEY, parts.join(" · "));
|
|
175
177
|
};
|
|
@@ -212,10 +214,18 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
212
214
|
: "Every ChatGPT account in the pool is signed out or limited. Run /chatgpt-pool to see reset times and banked resets.",
|
|
213
215
|
);
|
|
214
216
|
}
|
|
217
|
+
if (sticky !== undefined && sticky !== routed.provider && ctx.hasUI) {
|
|
218
|
+
const from = accounts.find((candidate) => candidate.id === sticky);
|
|
219
|
+
const until = from?.resetsAt && from.resetsAt > Date.now() ? ` (back in ${formatIn(from.resetsAt - Date.now())})` : "";
|
|
220
|
+
ctx.ui.notify(`ChatGPT pool: ${from?.label ?? sticky} is limited${until}, switched to ${account.label}.`, "info");
|
|
221
|
+
}
|
|
215
222
|
if (lastRouted !== routed.provider) {
|
|
216
223
|
lastRouted = routed.provider;
|
|
217
224
|
updateStatus(ctx);
|
|
218
225
|
}
|
|
226
|
+
refreshUsage(ctx, routed.provider)
|
|
227
|
+
.then(() => updateStatus(ctx, POOL_PROVIDER))
|
|
228
|
+
.catch(() => undefined);
|
|
219
229
|
return { model: routed, thinkingLevel: request.thinkingLevel };
|
|
220
230
|
},
|
|
221
231
|
});
|
|
@@ -237,13 +247,11 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
237
247
|
return {
|
|
238
248
|
message: {
|
|
239
249
|
...message,
|
|
240
|
-
errorMessage:
|
|
250
|
+
errorMessage: failoverMessage(account.label),
|
|
241
251
|
},
|
|
242
252
|
};
|
|
243
253
|
});
|
|
244
254
|
|
|
245
|
-
type AccountStatus = { account: Account; signedIn: boolean; usage?: Usage; error?: string };
|
|
246
|
-
|
|
247
255
|
const loadStatuses = (ctx: ExtensionContext, signal?: AbortSignal): Promise<AccountStatus[]> =>
|
|
248
256
|
Promise.all(
|
|
249
257
|
accounts.map(async (account): Promise<AccountStatus> => {
|
|
@@ -352,7 +360,50 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
352
360
|
if (next && isStrategy(next)) setStrategy(ctx, next);
|
|
353
361
|
};
|
|
354
362
|
|
|
363
|
+
const runPanel = async (ctx: ExtensionContext) => {
|
|
364
|
+
let statuses = await withLoader(ctx, "Checking ChatGPT accounts...", (signal) => loadStatuses(ctx, signal));
|
|
365
|
+
while (statuses) {
|
|
366
|
+
updateStatus(ctx);
|
|
367
|
+
const current = statuses.map((status) => ({
|
|
368
|
+
...status,
|
|
369
|
+
account: accounts.find((account) => account.id === status.account.id) ?? status.account,
|
|
370
|
+
}));
|
|
371
|
+
const strategyName = STRATEGIES[strategy].split(":")[0];
|
|
372
|
+
const action = await ctx.ui.custom<PanelAction>(
|
|
373
|
+
(tui, theme, _keybindings, done) => new PoolPanel(tui, theme, current, strategyName, lastRouted, done),
|
|
374
|
+
);
|
|
375
|
+
if (!action || action.type === "close") return;
|
|
376
|
+
const status = "accountId" in action ? current.find((entry) => entry.account.id === action.accountId) : undefined;
|
|
377
|
+
let reload = false;
|
|
378
|
+
if (action.type === "refresh") {
|
|
379
|
+
reload = true;
|
|
380
|
+
} else if (action.type === "add") {
|
|
381
|
+
const label = (await ctx.ui.input("Label for the new ChatGPT account", "work"))?.trim();
|
|
382
|
+
if (label) addAccount(ctx, label);
|
|
383
|
+
reload = !!label;
|
|
384
|
+
} else if (action.type === "strategy") {
|
|
385
|
+
await chooseStrategy(ctx);
|
|
386
|
+
} else if (status && action.type === "spend") {
|
|
387
|
+
await spend(ctx, status.account, status.usage);
|
|
388
|
+
reload = true;
|
|
389
|
+
} else if (status && action.type === "clear") {
|
|
390
|
+
setResetsAt(status.account.id, undefined);
|
|
391
|
+
ctx.ui.notify(`ChatGPT (${status.account.label}) is back in rotation.`, "info");
|
|
392
|
+
} else if (status && action.type === "remove") {
|
|
393
|
+
if (await ctx.ui.confirm(`Remove ${status.account.label}?`, "It will no longer be used by the pool.")) {
|
|
394
|
+
removeAccount(ctx, status.account);
|
|
395
|
+
statuses = statuses.filter((entry) => entry.account.id !== status.account.id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (reload) {
|
|
399
|
+
usageCache.clear();
|
|
400
|
+
statuses = await withLoader(ctx, "Checking ChatGPT accounts...", (signal) => loadStatuses(ctx, signal));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
355
405
|
const openPool = async (ctx: ExtensionContext) => {
|
|
406
|
+
if (ctx.mode === "tui") return runPanel(ctx);
|
|
356
407
|
const statuses = await withLoader(ctx, "Checking ChatGPT accounts...", (signal) => loadStatuses(ctx, signal));
|
|
357
408
|
if (!statuses) return;
|
|
358
409
|
updateStatus(ctx);
|
package/src/panel.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, Key, matchesKey, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatIn, formatWindow, usageBar } from "./format.ts";
|
|
4
|
+
import type { Account } from "./pool.ts";
|
|
5
|
+
import type { Usage } from "./usage.ts";
|
|
6
|
+
|
|
7
|
+
export type AccountStatus = { account: Account; signedIn: boolean; usage?: Usage; error?: string };
|
|
8
|
+
|
|
9
|
+
export type PanelAction =
|
|
10
|
+
| { type: "close" }
|
|
11
|
+
| { type: "refresh" }
|
|
12
|
+
| { type: "add" }
|
|
13
|
+
| { type: "strategy" }
|
|
14
|
+
| { type: "spend" | "clear" | "remove"; accountId: string };
|
|
15
|
+
|
|
16
|
+
const ACCOUNT_KEYS = new Map<string, "spend" | "clear" | "remove">([
|
|
17
|
+
["s", "spend"],
|
|
18
|
+
["c", "clear"],
|
|
19
|
+
["d", "remove"],
|
|
20
|
+
]);
|
|
21
|
+
const POOL_KEYS = new Map<string, "refresh" | "add" | "strategy">([
|
|
22
|
+
["r", "refresh"],
|
|
23
|
+
["a", "add"],
|
|
24
|
+
["t", "strategy"],
|
|
25
|
+
]);
|
|
26
|
+
const BAR_WIDTH = 16;
|
|
27
|
+
const HINTS = [
|
|
28
|
+
"↑↓ select · s spend reset · c clear limit · d remove · a add · t strategy · r refresh · esc close",
|
|
29
|
+
"↑↓ · s spend · c clear · d remove · a add · t strategy · r refresh · esc",
|
|
30
|
+
"s spend · c clear · d del · a add · t strat · esc",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export class PoolPanel implements Component {
|
|
34
|
+
private selected = 0;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly tui: TUI,
|
|
38
|
+
private readonly theme: Theme,
|
|
39
|
+
private readonly statuses: readonly AccountStatus[],
|
|
40
|
+
private readonly strategyName: string,
|
|
41
|
+
private readonly activeId: string | undefined,
|
|
42
|
+
private readonly done: (action: PanelAction) => void,
|
|
43
|
+
) {}
|
|
44
|
+
|
|
45
|
+
handleInput(data: string): void {
|
|
46
|
+
if (matchesKey(data, Key.escape) || data === "q") return this.done({ type: "close" });
|
|
47
|
+
if (matchesKey(data, Key.up) || data === "k") return this.move(-1);
|
|
48
|
+
if (matchesKey(data, Key.down) || data === "j") return this.move(1);
|
|
49
|
+
const poolAction = POOL_KEYS.get(data);
|
|
50
|
+
const accountAction = ACCOUNT_KEYS.get(data);
|
|
51
|
+
const account = this.statuses[this.selected]?.account;
|
|
52
|
+
if (poolAction) this.done({ type: poolAction });
|
|
53
|
+
else if (accountAction && account) this.done({ type: accountAction, accountId: account.id });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
invalidate(): void {}
|
|
57
|
+
|
|
58
|
+
render(width: number): string[] {
|
|
59
|
+
const { theme } = this;
|
|
60
|
+
const rule = theme.fg("borderAccent", "─".repeat(width));
|
|
61
|
+
const lines = [
|
|
62
|
+
rule,
|
|
63
|
+
`${theme.fg("accent", theme.bold("ChatGPT pool"))} ${theme.fg("muted", `strategy: ${this.strategyName}`)}`,
|
|
64
|
+
"",
|
|
65
|
+
];
|
|
66
|
+
if (this.statuses.length === 0) lines.push(theme.fg("muted", "No accounts yet. Press a to add one."), "");
|
|
67
|
+
this.statuses.forEach((status, index) => lines.push(...this.renderAccount(status, index === this.selected), ""));
|
|
68
|
+
const hint = HINTS.find((text) => visibleWidth(text) <= width) ?? HINTS[HINTS.length - 1];
|
|
69
|
+
lines.push(theme.fg("dim", hint), rule);
|
|
70
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private move(delta: number): void {
|
|
74
|
+
if (this.statuses.length === 0) return;
|
|
75
|
+
this.selected = (this.selected + delta + this.statuses.length) % this.statuses.length;
|
|
76
|
+
this.tui.requestRender();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private renderAccount({ account, signedIn, usage, error }: AccountStatus, selected: boolean): string[] {
|
|
80
|
+
const { theme } = this;
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const pointer = selected ? theme.fg("accent", "› ") : " ";
|
|
83
|
+
const name = selected ? theme.fg("accent", theme.bold(account.label)) : theme.bold(account.label);
|
|
84
|
+
const active = account.id === this.activeId ? theme.fg("muted", " (in use)") : "";
|
|
85
|
+
const limitedUntil = account.resetsAt && account.resetsAt > now ? account.resetsAt : undefined;
|
|
86
|
+
const state = !signedIn
|
|
87
|
+
? theme.fg("muted", "not signed in")
|
|
88
|
+
: limitedUntil
|
|
89
|
+
? theme.fg("warning", `limited · back in ${formatIn(limitedUntil - now)}`)
|
|
90
|
+
: theme.fg("success", "ready");
|
|
91
|
+
const banked =
|
|
92
|
+
usage && usage.banked > 0
|
|
93
|
+
? theme.fg("accent", ` ${usage.banked} banked reset${usage.banked === 1 ? "" : "s"}`) +
|
|
94
|
+
(usage.usableNow > 0 ? theme.fg("success", ` (${usage.usableNow} usable now)`) : "")
|
|
95
|
+
: "";
|
|
96
|
+
const header = `${pointer}${name}${active} ${state}${banked}`;
|
|
97
|
+
|
|
98
|
+
if (!signedIn) return [header, theme.fg("dim", ` run /login and pick ChatGPT (${account.label})`)];
|
|
99
|
+
if (!usage) return [header, theme.fg("dim", ` usage unavailable${error ? `: ${error}` : ""}`)];
|
|
100
|
+
return [
|
|
101
|
+
header,
|
|
102
|
+
...usage.windows.map((window) => {
|
|
103
|
+
const bar = usageBar(window.usedPercent, BAR_WIDTH);
|
|
104
|
+
const color = window.usedPercent >= 90 ? "error" : window.usedPercent >= 70 ? "warning" : "success";
|
|
105
|
+
const label = formatWindow(window.windowSeconds).padEnd(7);
|
|
106
|
+
const percent = `${Math.round(window.usedPercent)}%`.padStart(4);
|
|
107
|
+
return ` ${theme.fg("muted", label)} ${theme.fg(color, bar.filled)}${theme.fg("dim", bar.empty)} ${percent} ${theme.fg("muted", `resets in ${formatIn(window.resetsAt - now)}`)}`;
|
|
108
|
+
}),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/pool.ts
CHANGED
|
@@ -3,6 +3,10 @@ export type Account = { id: string; label: string; resetsAt?: number };
|
|
|
3
3
|
const USAGE_LIMIT = /usage.?limit|subscription_sharing_usage_limit_exceeded|insufficient_quota|quota/i;
|
|
4
4
|
const RATE_LIMIT = /rate.?limit|too many requests|\b429\b/i;
|
|
5
5
|
|
|
6
|
+
export function failoverMessage(label: string): string {
|
|
7
|
+
return `ChatGPT account "${label}" hit its rate limit; switching to another account in the pool.`;
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export const USAGE_COOLDOWN_MS = 60 * 60 * 1000;
|
|
7
11
|
export const RATE_COOLDOWN_MS = 60 * 1000;
|
|
8
12
|
|