pi-chatgpt-pool 0.2.1 → 0.2.3
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 +11 -12
- package/package.json +1 -1
- package/src/index.ts +30 -12
- package/src/panel.ts +17 -9
- package/src/usage.ts +14 -7
package/README.md
CHANGED
|
@@ -12,14 +12,15 @@ Requires pi 0.99.1 or later. The plugin uses pi's own Sign in with ChatGPT flow,
|
|
|
12
12
|
|
|
13
13
|
## Set up accounts
|
|
14
14
|
|
|
15
|
-
Run `/chatgpt-pool`
|
|
15
|
+
Run `/chatgpt-pool`, press `a` to add an account, then Enter to sign in. Or from the editor:
|
|
16
16
|
|
|
17
17
|
```text
|
|
18
18
|
/chatgpt-pool add personal
|
|
19
19
|
/chatgpt-pool add work
|
|
20
|
-
/login
|
|
21
20
|
```
|
|
22
21
|
|
|
22
|
+
After adding an account, pi fills in `/login chatgpt-<n>` for it; press Enter to open the ChatGPT sign-in. Sign in to each account with a different ChatGPT account. `/chatgpt-pool login <label>` does the same for an existing account.
|
|
23
|
+
|
|
23
24
|
`/login` lists each account as `ChatGPT (<label>)`, or under a name you choose with `/chatgpt-pool name`. Renaming keeps the account signed in. Sign in to each one with a different ChatGPT account. Then pick a pooled model:
|
|
24
25
|
|
|
25
26
|
```text
|
|
@@ -34,9 +35,9 @@ The pool offers the ChatGPT subscription models: the OpenAI models that pi also
|
|
|
34
35
|
| --- | --- |
|
|
35
36
|
| `/chatgpt-pool` | Open the pool panel (see below) |
|
|
36
37
|
| `/chatgpt-pool add <label>` | Add an account slot. Sign in to it with `/login` |
|
|
38
|
+
| `/chatgpt-pool login <label>` | Fill in `/login` for that account, so Enter starts its sign-in |
|
|
37
39
|
| `/chatgpt-pool name <label> <name>` | Set the name the account shows in `/login`, for example `Work (jal@mastra.ai)`. Leave the name empty to go back to `ChatGPT (<label>)` |
|
|
38
40
|
| `/chatgpt-pool remove <label>` | Remove an account. Run `/logout` for it first to delete its token |
|
|
39
|
-
| `/chatgpt-pool spend <label>` | Spend one banked reset on that account, after you confirm |
|
|
40
41
|
| `/chatgpt-pool strategy <name>` | Set the routing strategy: `fill-first`, `round-robin`, `least-used`, or `use-it-or-lose-it` |
|
|
41
42
|
| `/chatgpt-pool reset` | Forget recorded limits, for example after a limit clears early |
|
|
42
43
|
|
|
@@ -44,12 +45,12 @@ The command autocompletes its actions and account labels.
|
|
|
44
45
|
|
|
45
46
|
## Pool panel
|
|
46
47
|
|
|
47
|
-
`/chatgpt-pool`
|
|
48
|
+
`/chatgpt-pool` shows each account's sign-in status and any recorded limit countdown. Usage meters and banked resets are hidden for ChatGPT subscription-sharing sign-ins until a compatible usage endpoint is verified. Opening the panel does not send rejected Codex usage requests.
|
|
48
49
|
|
|
49
50
|
| Key | Action |
|
|
50
51
|
| --- | --- |
|
|
51
52
|
| `↑` `↓` or `j` `k` | Select an account |
|
|
52
|
-
| `
|
|
53
|
+
| `enter` | Sign in to the selected account (fills in `/login` for it; press Enter again) |
|
|
53
54
|
| `c` | Clear the selected account's recorded limit |
|
|
54
55
|
| `n` | Set the name the selected account shows in `/login` |
|
|
55
56
|
| `d` | Remove the selected account |
|
|
@@ -60,7 +61,7 @@ The command autocompletes its actions and account labels.
|
|
|
60
61
|
|
|
61
62
|
Over RPC the same actions are offered as a list instead.
|
|
62
63
|
|
|
63
|
-
While a pooled model is selected, the footer shows the account in use
|
|
64
|
+
While a pooled model is selected, the footer shows the account in use, for example `chatgpt: work`, plus how many accounts have recorded limits and when the next one returns. 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.`
|
|
64
65
|
|
|
65
66
|
`/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:
|
|
66
67
|
|
|
@@ -83,7 +84,7 @@ A conversation stays on the same account while it works, so prompt caching and r
|
|
|
83
84
|
| Least used | The account with the lowest usage % |
|
|
84
85
|
| Use it or lose it | The account whose usage window resets soonest, so its remaining quota isn't wasted |
|
|
85
86
|
|
|
86
|
-
Choose one from the **Strategy** row in `/chatgpt-pool`, or run `/chatgpt-pool strategy <name>`. It's saved in `chatgpt-pool.json`, and the round-robin position is shared across pi sessions. Least used and use it or lose it
|
|
87
|
+
Choose one from the **Strategy** row in `/chatgpt-pool`, or run `/chatgpt-pool strategy <name>`. It's saved in `chatgpt-pool.json`, and the round-robin position is shared across pi sessions. Least used and use it or lose it require usage data. The current ChatGPT sign-in flow cannot read it, so these strategies use the configured account order. Round robin spreads new conversations without usage data.
|
|
87
88
|
|
|
88
89
|
## How failover works
|
|
89
90
|
|
|
@@ -92,13 +93,11 @@ Choose one from the **Strategy** row in `/chatgpt-pool`, or run `/chatgpt-pool s
|
|
|
92
93
|
- Reset times are saved in `chatgpt-pool.json`, so they survive restarts.
|
|
93
94
|
- If every account is signed out or limited, the request fails with a message pointing to `/chatgpt-pool`.
|
|
94
95
|
|
|
95
|
-
##
|
|
96
|
-
|
|
97
|
-
ChatGPT gives some accounts reset credits that clear a usage limit early. `/chatgpt-pool` shows how many each account has banked and how many apply right now.
|
|
96
|
+
## Usage and banked resets
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
OpenAI's subscription-sharing sign-in tokens are not accepted by the Codex `backend-api/wham` usage service. Usage meters and banked-reset actions are hidden for these accounts. No reset is spent, and the pool never spends one automatically. Recorded limits and failover still work from inference errors.
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
Review actual usage in [ChatGPT Usage settings](https://chatgpt.com/settings/usage). Codex reset credits have not been verified to reset subscription-sharing limits.
|
|
102
101
|
|
|
103
102
|
## Releasing
|
|
104
103
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -114,7 +114,8 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
114
114
|
};
|
|
115
115
|
const usageCache = new Map<string, { snapshot: UsageSnapshot; at: number }>();
|
|
116
116
|
|
|
117
|
-
const rememberUsage = (accountId: string, usage: Usage) => {
|
|
117
|
+
const rememberUsage = (accountId: string, usage: Usage | undefined) => {
|
|
118
|
+
if (!usage) return;
|
|
118
119
|
const snapshot = snapshotOf(usage);
|
|
119
120
|
if (snapshot) usageCache.set(accountId, { snapshot, at: Date.now() });
|
|
120
121
|
};
|
|
@@ -260,6 +261,7 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
260
261
|
if (!token || signal?.aborted) return { account, signedIn: !!token };
|
|
261
262
|
try {
|
|
262
263
|
const usage = await fetchUsage(token);
|
|
264
|
+
if (!usage) return { account, signedIn: true };
|
|
263
265
|
rememberUsage(account.id, usage);
|
|
264
266
|
const limitedUntil = usage.limitReached ? Math.max(...usage.windows.map((w) => w.resetsAt)) : undefined;
|
|
265
267
|
if (limitedUntil !== account.resetsAt && (usage.limitReached || (account.resetsAt ?? 0) > Date.now())) {
|
|
@@ -284,9 +286,9 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
284
286
|
|
|
285
287
|
const describe = ({ account, signedIn, usage, error }: AccountStatus): string => {
|
|
286
288
|
const limitedUntil = account.resetsAt && account.resetsAt > Date.now() ? account.resetsAt : undefined;
|
|
287
|
-
if (!signedIn) return `${account.label} · not signed in, run /login
|
|
288
|
-
const state = limitedUntil ? `limited until ${formatReset(limitedUntil)}` : "ready";
|
|
289
|
-
if (!usage) return `${account.label} · ${state}
|
|
289
|
+
if (!signedIn) return `${account.label} · not signed in, run /chatgpt-pool login ${account.label}`;
|
|
290
|
+
const state = limitedUntil ? `limited until ${formatReset(limitedUntil)}` : usage ? "ready" : "signed in";
|
|
291
|
+
if (!usage) return `${account.label} · ${state}${error ? ` · ${error}` : ""}`;
|
|
290
292
|
const windows = usage.windows.map(
|
|
291
293
|
(w) => `${formatWindow(w.windowSeconds)} ${Math.round(w.usedPercent)}%, resets ${formatReset(w.resetsAt)}`,
|
|
292
294
|
);
|
|
@@ -306,7 +308,8 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
306
308
|
update((config) => ({ ...config, accounts: [...config.accounts, account] }));
|
|
307
309
|
registerPooledModels();
|
|
308
310
|
pi.registerProvider(accountProvider(openai, account, onReset));
|
|
309
|
-
ctx.ui.notify(`Added ${loginName(account)}
|
|
311
|
+
ctx.ui.notify(`Added ${loginName(account)}.`, "info");
|
|
312
|
+
startLogin(ctx, account);
|
|
310
313
|
};
|
|
311
314
|
|
|
312
315
|
const setLoginName = (ctx: ExtensionContext, account: Account, name: string) => {
|
|
@@ -327,6 +330,14 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
327
330
|
if (name !== undefined) setLoginName(ctx, account, name);
|
|
328
331
|
};
|
|
329
332
|
|
|
333
|
+
const startLogin = (ctx: ExtensionContext, account: Account) => {
|
|
334
|
+
if (ctx.mode !== "tui") {
|
|
335
|
+
return ctx.ui.notify(`Run /login ${account.id} to sign in to ${loginName(account)}.`, "info");
|
|
336
|
+
}
|
|
337
|
+
ctx.ui.setEditorText(`/login ${account.id}`);
|
|
338
|
+
ctx.ui.notify(`Press Enter to sign in to ${loginName(account)}.`, "info");
|
|
339
|
+
};
|
|
340
|
+
|
|
330
341
|
const removeAccount = (ctx: ExtensionContext, account: Account) => {
|
|
331
342
|
update((config) => ({ ...config, accounts: config.accounts.filter((candidate) => candidate.id !== account.id) }));
|
|
332
343
|
pi.unregisterProvider(account.id);
|
|
@@ -341,7 +352,7 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
341
352
|
const token = await ctx.modelRegistry.getApiKeyForProvider(account.id);
|
|
342
353
|
if (!token) return ctx.ui.notify(`ChatGPT (${account.label}) is not signed in.`, "warning");
|
|
343
354
|
const usage = known ?? (await withLoader(ctx, `Checking ${account.label}...`, () => fetchUsage(token)));
|
|
344
|
-
if (!usage) return;
|
|
355
|
+
if (!usage) return ctx.ui.notify("Banked resets are unavailable for this sign-in. No reset spent.", "info");
|
|
345
356
|
const credit = [...usage.credits].sort(
|
|
346
357
|
(a, b) => (a.expiresAt ?? Number.POSITIVE_INFINITY) - (b.expiresAt ?? Number.POSITIVE_INFINITY),
|
|
347
358
|
)[0];
|
|
@@ -398,13 +409,14 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
398
409
|
reload = true;
|
|
399
410
|
} else if (action.type === "add") {
|
|
400
411
|
const label = (await ctx.ui.input("Label for the new ChatGPT account", "work"))?.trim();
|
|
401
|
-
if (label) addAccount(ctx, label);
|
|
402
|
-
reload = !!label;
|
|
412
|
+
if (label) return addAccount(ctx, label);
|
|
403
413
|
} else if (action.type === "strategy") {
|
|
404
414
|
await chooseStrategy(ctx);
|
|
405
415
|
} else if (status && action.type === "spend") {
|
|
406
416
|
await spend(ctx, status.account, status.usage);
|
|
407
417
|
reload = true;
|
|
418
|
+
} else if (status && action.type === "login") {
|
|
419
|
+
return startLogin(ctx, status.account);
|
|
408
420
|
} else if (status && action.type === "name") {
|
|
409
421
|
await askLoginName(ctx, status.account);
|
|
410
422
|
} else if (status && action.type === "clear") {
|
|
@@ -461,14 +473,14 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
461
473
|
const SUBCOMMANDS = [
|
|
462
474
|
{ value: "add", label: "add", description: "Add an account slot" },
|
|
463
475
|
{ value: "remove", label: "remove", description: "Remove an account" },
|
|
464
|
-
{ value: "
|
|
476
|
+
{ value: "login", label: "login", description: "Sign in to an account" },
|
|
465
477
|
{ value: "name", label: "name", description: "Set the name an account shows in /login" },
|
|
466
478
|
{ value: "reset", label: "reset", description: "Forget recorded limits" },
|
|
467
479
|
{ value: "strategy", label: "strategy", description: "Choose how new conversations pick an account" },
|
|
468
480
|
];
|
|
469
481
|
|
|
470
482
|
pi.registerCommand("chatgpt-pool", {
|
|
471
|
-
description: "Manage pooled ChatGPT accounts:
|
|
483
|
+
description: "Manage pooled ChatGPT accounts: sign-in, strategy, add, remove, name, reset",
|
|
472
484
|
getArgumentCompletions(prefix) {
|
|
473
485
|
const [action, ...rest] = prefix.split(" ");
|
|
474
486
|
if (rest.length === 0) return SUBCOMMANDS.filter((item) => item.value.startsWith(action));
|
|
@@ -477,7 +489,7 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
477
489
|
.filter(([name]) => name.startsWith(rest.join(" ")))
|
|
478
490
|
.map(([name, description]) => ({ value: `strategy ${name}`, label: name, description }));
|
|
479
491
|
}
|
|
480
|
-
if (
|
|
492
|
+
if (!["remove", "spend", "name", "login"].includes(action)) return null;
|
|
481
493
|
if (action === "name" && rest.length > 1) return null;
|
|
482
494
|
const partial = rest.join(" ");
|
|
483
495
|
return accounts
|
|
@@ -499,6 +511,12 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
499
511
|
return removeAccount(ctx, account);
|
|
500
512
|
}
|
|
501
513
|
|
|
514
|
+
if (action === "login") {
|
|
515
|
+
const account = findAccount(label);
|
|
516
|
+
if (!account) return ctx.ui.notify("Usage: /chatgpt-pool login <label>", "warning");
|
|
517
|
+
return startLogin(ctx, account);
|
|
518
|
+
}
|
|
519
|
+
|
|
502
520
|
if (action === "name") {
|
|
503
521
|
const [target = "", ...nameParts] = rest;
|
|
504
522
|
const account = findAccount(target);
|
|
@@ -533,7 +551,7 @@ export default function chatgptPool(pi: ExtensionAPI) {
|
|
|
533
551
|
return spend(ctx, account);
|
|
534
552
|
}
|
|
535
553
|
|
|
536
|
-
if (action) return ctx.ui.notify(`Unknown action "${action}". Use add, remove, name,
|
|
554
|
+
if (action) return ctx.ui.notify(`Unknown action "${action}". Use add, login, remove, name, reset, or strategy.`, "warning");
|
|
537
555
|
if (accounts.length === 0) {
|
|
538
556
|
const label = (await ctx.ui.input("No ChatGPT accounts yet. Label for the first one", "personal"))?.trim();
|
|
539
557
|
if (label) addAccount(ctx, label);
|
package/src/panel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Component, Key, matchesKey, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { formatIn, formatWindow, usageBar } from "./format.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { Account } from "./pool.ts";
|
|
5
5
|
import type { Usage } from "./usage.ts";
|
|
6
6
|
|
|
7
7
|
export type AccountStatus = { account: Account; signedIn: boolean; usage?: Usage; error?: string };
|
|
@@ -11,7 +11,7 @@ export type PanelAction =
|
|
|
11
11
|
| { type: "refresh" }
|
|
12
12
|
| { type: "add" }
|
|
13
13
|
| { type: "strategy" }
|
|
14
|
-
| { type: "spend" | "clear" | "remove" | "name"; accountId: string };
|
|
14
|
+
| { type: "spend" | "clear" | "remove" | "name" | "login"; accountId: string };
|
|
15
15
|
|
|
16
16
|
const ACCOUNT_KEYS = new Map<string, "spend" | "clear" | "remove" | "name">([
|
|
17
17
|
["s", "spend"],
|
|
@@ -26,9 +26,9 @@ const POOL_KEYS = new Map<string, "refresh" | "add" | "strategy">([
|
|
|
26
26
|
]);
|
|
27
27
|
const BAR_WIDTH = 16;
|
|
28
28
|
const HINTS = [
|
|
29
|
-
"↑↓ select · s spend reset · c clear limit · n login name · d remove · a add · t strategy · r refresh · esc close",
|
|
30
|
-
"↑↓ · s spend · c clear · n name · d remove · a add · t strategy ·
|
|
31
|
-
"
|
|
29
|
+
"↑↓ select · enter sign in · s spend reset · c clear limit · n login name · d remove · a add · t strategy · r refresh · esc close",
|
|
30
|
+
"↑↓ · enter sign in · s spend · c clear · n name · d remove · a add · t strategy · esc",
|
|
31
|
+
"enter sign in · s spend · n name · d del · a add · esc",
|
|
32
32
|
];
|
|
33
33
|
|
|
34
34
|
export class PoolPanel implements Component {
|
|
@@ -47,9 +47,14 @@ export class PoolPanel implements Component {
|
|
|
47
47
|
if (matchesKey(data, Key.escape) || data === "q") return this.done({ type: "close" });
|
|
48
48
|
if (matchesKey(data, Key.up) || data === "k") return this.move(-1);
|
|
49
49
|
if (matchesKey(data, Key.down) || data === "j") return this.move(1);
|
|
50
|
+
if (matchesKey(data, Key.enter)) {
|
|
51
|
+
const account = this.statuses[this.selected]?.account;
|
|
52
|
+
return account ? this.done({ type: "login", accountId: account.id }) : undefined;
|
|
53
|
+
}
|
|
50
54
|
const poolAction = POOL_KEYS.get(data);
|
|
51
55
|
const accountAction = ACCOUNT_KEYS.get(data);
|
|
52
56
|
const account = this.statuses[this.selected]?.account;
|
|
57
|
+
if (accountAction === "spend" && !this.statuses[this.selected]?.usage?.banked) return;
|
|
53
58
|
if (poolAction) this.done({ type: poolAction });
|
|
54
59
|
else if (accountAction && account) this.done({ type: accountAction, accountId: account.id });
|
|
55
60
|
}
|
|
@@ -66,7 +71,10 @@ export class PoolPanel implements Component {
|
|
|
66
71
|
];
|
|
67
72
|
if (this.statuses.length === 0) lines.push(theme.fg("muted", "No accounts yet. Press a to add one."), "");
|
|
68
73
|
this.statuses.forEach((status, index) => lines.push(...this.renderAccount(status, index === this.selected), ""));
|
|
69
|
-
const
|
|
74
|
+
const hints = this.statuses[this.selected]?.usage?.banked
|
|
75
|
+
? HINTS
|
|
76
|
+
: HINTS.map((text) => text.replace(/s spend(?: reset)? · /g, ""));
|
|
77
|
+
const hint = hints.find((text) => visibleWidth(text) <= width) ?? hints[hints.length - 1];
|
|
70
78
|
lines.push(theme.fg("dim", hint), rule);
|
|
71
79
|
return lines.map((line) => truncateToWidth(line, width));
|
|
72
80
|
}
|
|
@@ -88,7 +96,7 @@ export class PoolPanel implements Component {
|
|
|
88
96
|
? theme.fg("muted", "not signed in")
|
|
89
97
|
: limitedUntil
|
|
90
98
|
? theme.fg("warning", `limited · back in ${formatIn(limitedUntil - now)}`)
|
|
91
|
-
: theme.fg("success", "ready");
|
|
99
|
+
: usage ? theme.fg("success", "ready") : theme.fg("muted", "signed in");
|
|
92
100
|
const banked =
|
|
93
101
|
usage && usage.banked > 0
|
|
94
102
|
? theme.fg("accent", ` ${usage.banked} banked reset${usage.banked === 1 ? "" : "s"}`) +
|
|
@@ -96,8 +104,8 @@ export class PoolPanel implements Component {
|
|
|
96
104
|
: "";
|
|
97
105
|
const header = `${pointer}${name}${active} ${state}${banked}`;
|
|
98
106
|
|
|
99
|
-
if (!signedIn) return [header, theme.fg("dim",
|
|
100
|
-
if (!usage) return [header, theme.fg("
|
|
107
|
+
if (!signedIn) return [header, theme.fg("dim", " press enter to sign in")];
|
|
108
|
+
if (!usage) return error ? [header, theme.fg("muted", ` ${error}`)] : [header];
|
|
101
109
|
return [
|
|
102
110
|
header,
|
|
103
111
|
...usage.windows.map((window) => {
|
package/src/usage.ts
CHANGED
|
@@ -22,25 +22,31 @@ export type Usage = {
|
|
|
22
22
|
};
|
|
23
23
|
export type SpendOutcome = ConsumeResponse["code"];
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
type TokenClaims = { scope?: string; "https://api.openai.com/auth"?: { chatgpt_account_id?: string } };
|
|
26
|
+
|
|
27
|
+
function tokenClaims(token: string): TokenClaims | undefined {
|
|
26
28
|
const payload = token.split(".")[1];
|
|
27
29
|
if (!payload) return undefined;
|
|
28
30
|
try {
|
|
29
|
-
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"))
|
|
30
|
-
?.chatgpt_account_id;
|
|
31
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
31
32
|
} catch {
|
|
32
33
|
return undefined;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function supportsUsage(token: string): boolean {
|
|
38
|
+
const claims = tokenClaims(token);
|
|
39
|
+
return !(typeof claims?.scope === "string" && claims.scope.split(/\s+/).includes("chatgpt.tokens.use.direct"));
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
async function backend<T>(token: string, path: string, init: RequestInit = {}): Promise<T> {
|
|
43
|
+
if (!supportsUsage(token)) throw new Error("Banked resets are unavailable for ChatGPT subscription-sharing sign-ins.");
|
|
37
44
|
const headers = new Headers({ authorization: `Bearer ${token}`, "content-type": "application/json" });
|
|
38
|
-
const accountId =
|
|
45
|
+
const accountId = tokenClaims(token)?.["https://api.openai.com/auth"]?.chatgpt_account_id;
|
|
39
46
|
if (accountId) headers.set("chatgpt-account-id", accountId);
|
|
40
47
|
const response = await fetch(`${BACKEND}/${path}`, { ...init, headers, signal: AbortSignal.timeout(15_000) });
|
|
41
48
|
if (!response.ok) {
|
|
42
|
-
|
|
43
|
-
throw new Error(`ChatGPT ${path} failed (${response.status})${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
49
|
+
throw new Error(`ChatGPT ${path} failed (${response.status}).`);
|
|
44
50
|
}
|
|
45
51
|
return response.json();
|
|
46
52
|
}
|
|
@@ -50,7 +56,8 @@ function toWindow(window: Window | null | undefined): UsageWindow[] {
|
|
|
50
56
|
return [{ usedPercent: window.used_percent, windowSeconds: window.limit_window_seconds, resetsAt: window.reset_at * 1000 }];
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
export async function fetchUsage(token: string): Promise<Usage> {
|
|
59
|
+
export async function fetchUsage(token: string): Promise<Usage | undefined> {
|
|
60
|
+
if (!supportsUsage(token)) return undefined;
|
|
54
61
|
const [usage, credits] = await Promise.all([
|
|
55
62
|
backend<UsageResponse>(token, "usage"),
|
|
56
63
|
backend<CreditsResponse>(token, "rate-limit-reset-credits"),
|