pi-better-openai 0.1.19 → 0.1.21
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 +4 -4
- package/index.ts +145 -56
- package/package.json +6 -4
- package/src/codex-auth.ts +38 -7
- package/src/config.ts +3 -2
- package/src/fast-controller.ts +1 -1
- package/src/format.ts +45 -20
- package/src/image.ts +116 -127
- package/src/paths.ts +17 -0
- package/src/pet-footer-controller.ts +37 -21
- package/src/pets.ts +104 -47
- package/src/usage-controller.ts +133 -48
- package/src/usage.ts +91 -23
package/src/usage.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getCodexCredentials } from "./codex-auth.ts";
|
|
3
3
|
export { AUTH_FILE, readCodexAuth } from "./codex-auth.ts";
|
|
4
4
|
|
|
@@ -21,6 +21,8 @@ export type CodexUsageResponse = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type UsageSnapshot = {
|
|
24
|
+
capturedAt: number;
|
|
25
|
+
scope: UsageScope;
|
|
24
26
|
fiveHourLeftPercent: number | null;
|
|
25
27
|
sevenDayLeftPercent: number | null;
|
|
26
28
|
fiveHourResetInSeconds: number | null;
|
|
@@ -28,9 +30,42 @@ export type UsageSnapshot = {
|
|
|
28
30
|
isLimited: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
33
|
+
export type UsageScope = "default" | "spark";
|
|
34
|
+
|
|
31
35
|
export const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
32
36
|
const SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
|
33
37
|
const SPARK_LIMIT_NAME = "GPT-5.3-Codex-Spark";
|
|
38
|
+
type ResetClockFormatters = {
|
|
39
|
+
time: Intl.DateTimeFormat;
|
|
40
|
+
weekday: Intl.DateTimeFormat;
|
|
41
|
+
date: Intl.DateTimeFormat;
|
|
42
|
+
};
|
|
43
|
+
const RESET_CLOCK_FORMATTER_CACHE_LIMIT = 4;
|
|
44
|
+
const resetClockFormatters = new Map<string, ResetClockFormatters>();
|
|
45
|
+
|
|
46
|
+
function currentTimeZoneKey(date: Date): string {
|
|
47
|
+
const zoneLabel = /\(([^)]+)\)$/.exec(date.toString())?.[1] ?? "";
|
|
48
|
+
return `${process.env.TZ ?? ""}:${date.getTimezoneOffset()}:${zoneLabel}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getResetClockFormatters(now: Date, reset: Date): ResetClockFormatters {
|
|
52
|
+
const timeZoneKey = `${currentTimeZoneKey(now)}:${reset.getTimezoneOffset()}`;
|
|
53
|
+
let formatters = resetClockFormatters.get(timeZoneKey);
|
|
54
|
+
if (!formatters) {
|
|
55
|
+
formatters = {
|
|
56
|
+
time: new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }),
|
|
57
|
+
weekday: new Intl.DateTimeFormat(undefined, { weekday: "short" }),
|
|
58
|
+
date: new Intl.DateTimeFormat(undefined, { month: "numeric", day: "numeric" }),
|
|
59
|
+
};
|
|
60
|
+
resetClockFormatters.set(timeZoneKey, formatters);
|
|
61
|
+
while (resetClockFormatters.size > RESET_CLOCK_FORMATTER_CACHE_LIMIT) {
|
|
62
|
+
const oldestKey = resetClockFormatters.keys().next().value;
|
|
63
|
+
if (oldestKey === undefined) break;
|
|
64
|
+
resetClockFormatters.delete(oldestKey);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return formatters;
|
|
68
|
+
}
|
|
34
69
|
|
|
35
70
|
function asObject(value: unknown): Record<string, unknown> | null {
|
|
36
71
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -42,12 +77,12 @@ function clampPercent(value: number): number {
|
|
|
42
77
|
}
|
|
43
78
|
|
|
44
79
|
function usedToLeftPercent(value: number | null | undefined): number | null {
|
|
45
|
-
if (typeof value !== "number" || Number.
|
|
80
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
46
81
|
return clampPercent(100 - value);
|
|
47
82
|
}
|
|
48
83
|
|
|
49
84
|
export function formatResetCountdown(seconds: number | null): string | null {
|
|
50
|
-
if (typeof seconds !== "number" || Number.
|
|
85
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return null;
|
|
51
86
|
const total = Math.max(0, Math.round(seconds));
|
|
52
87
|
const days = Math.floor(total / 86_400);
|
|
53
88
|
const hours = Math.floor((total % 86_400) / 3_600);
|
|
@@ -62,15 +97,17 @@ export function formatResetCountdown(seconds: number | null): string | null {
|
|
|
62
97
|
function formatResetClock(
|
|
63
98
|
seconds: number | null,
|
|
64
99
|
options?: { includeDate?: boolean },
|
|
100
|
+
now = Date.now(),
|
|
65
101
|
): string | null {
|
|
66
|
-
if (typeof seconds !== "number" || Number.
|
|
67
|
-
const resetDate = new Date(
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
102
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return null;
|
|
103
|
+
const resetDate = new Date(now + seconds * 1000);
|
|
104
|
+
const currentDate = new Date(now);
|
|
105
|
+
const formatters = getResetClockFormatters(currentDate, resetDate);
|
|
106
|
+
const time = formatters.time.format(resetDate);
|
|
107
|
+
if (!options?.includeDate && resetDate.toDateString() === currentDate.toDateString()) return time;
|
|
108
|
+
const weekday = formatters.weekday.format(resetDate);
|
|
72
109
|
if (!options?.includeDate) return `${weekday} ${time}`;
|
|
73
|
-
const date =
|
|
110
|
+
const date = formatters.date.format(resetDate);
|
|
74
111
|
return `${weekday} ${date} ${time}`;
|
|
75
112
|
}
|
|
76
113
|
|
|
@@ -78,9 +115,10 @@ function formatCompactReset(
|
|
|
78
115
|
label: string,
|
|
79
116
|
seconds: number | null,
|
|
80
117
|
options?: { includeDate?: boolean },
|
|
118
|
+
now = Date.now(),
|
|
81
119
|
): string | null {
|
|
82
120
|
const countdown = formatResetCountdown(seconds);
|
|
83
|
-
const clock = formatResetClock(seconds, options);
|
|
121
|
+
const clock = formatResetClock(seconds, options, now);
|
|
84
122
|
return countdown && clock ? `${label} ↺ ${countdown} - ${clock}` : null;
|
|
85
123
|
}
|
|
86
124
|
|
|
@@ -94,7 +132,7 @@ export async function requestCodexUsage(
|
|
|
94
132
|
): Promise<CodexUsageResponse | undefined> {
|
|
95
133
|
const ctx = isAbortSignal(ctxOrSignal) ? undefined : ctxOrSignal;
|
|
96
134
|
const requestSignal = isAbortSignal(ctxOrSignal) ? ctxOrSignal : signal;
|
|
97
|
-
const credentials = await getCodexCredentials(ctx);
|
|
135
|
+
const credentials = await getCodexCredentials(ctx, requestSignal);
|
|
98
136
|
if (!credentials) return undefined;
|
|
99
137
|
const response = await fetch(USAGE_URL, {
|
|
100
138
|
headers: {
|
|
@@ -148,34 +186,45 @@ function findSparkRateLimitBucket(data: CodexUsageResponse): RateLimitBucket | n
|
|
|
148
186
|
return null;
|
|
149
187
|
}
|
|
150
188
|
|
|
151
|
-
function getResetSeconds(window: UsageWindow | null | undefined): number | null {
|
|
152
|
-
if (
|
|
189
|
+
function getResetSeconds(window: UsageWindow | null | undefined, now: number): number | null {
|
|
190
|
+
if (
|
|
191
|
+
typeof window?.reset_after_seconds === "number" &&
|
|
192
|
+
Number.isFinite(window.reset_after_seconds)
|
|
193
|
+
)
|
|
153
194
|
return window.reset_after_seconds;
|
|
154
|
-
if (typeof window?.reset_at !== "number" || Number.
|
|
195
|
+
if (typeof window?.reset_at !== "number" || !Number.isFinite(window.reset_at)) return null;
|
|
155
196
|
const resetAtSeconds =
|
|
156
197
|
window.reset_at > 100_000_000_000 ? window.reset_at / 1000 : window.reset_at;
|
|
157
|
-
return Math.max(0, resetAtSeconds -
|
|
198
|
+
return Math.max(0, resetAtSeconds - now / 1000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function usageScopeForModel(modelId: string | undefined): UsageScope {
|
|
202
|
+
return modelId === SPARK_MODEL_ID ? "spark" : "default";
|
|
158
203
|
}
|
|
159
204
|
|
|
160
205
|
export function parseUsageSnapshot(
|
|
161
206
|
data: CodexUsageResponse,
|
|
162
207
|
modelId: string | undefined,
|
|
208
|
+
now = Date.now(),
|
|
163
209
|
): UsageSnapshot {
|
|
210
|
+
const scope = usageScopeForModel(modelId);
|
|
164
211
|
const bucket =
|
|
165
|
-
|
|
166
|
-
? findSparkRateLimitBucket(data)
|
|
212
|
+
scope === "spark"
|
|
213
|
+
? (findSparkRateLimitBucket(data) ?? normalizeRateLimitBucket(data.rate_limit))
|
|
167
214
|
: normalizeRateLimitBucket(data.rate_limit);
|
|
168
215
|
return {
|
|
216
|
+
capturedAt: now,
|
|
217
|
+
scope,
|
|
169
218
|
fiveHourLeftPercent: usedToLeftPercent(bucket?.primary_window?.used_percent),
|
|
170
219
|
sevenDayLeftPercent: usedToLeftPercent(bucket?.secondary_window?.used_percent),
|
|
171
|
-
fiveHourResetInSeconds: getResetSeconds(bucket?.primary_window),
|
|
172
|
-
sevenDayResetInSeconds: getResetSeconds(bucket?.secondary_window),
|
|
220
|
+
fiveHourResetInSeconds: getResetSeconds(bucket?.primary_window, now),
|
|
221
|
+
sevenDayResetInSeconds: getResetSeconds(bucket?.secondary_window, now),
|
|
173
222
|
isLimited: bucket?.limit_reached === true || bucket?.allowed === false,
|
|
174
223
|
};
|
|
175
224
|
}
|
|
176
225
|
|
|
177
226
|
export function formatPercent(value: number | null): string {
|
|
178
|
-
return typeof value === "number" &&
|
|
227
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
179
228
|
? `${Math.round(clampPercent(value))}%`
|
|
180
229
|
: "--";
|
|
181
230
|
}
|
|
@@ -183,14 +232,33 @@ export function formatPercent(value: number | null): string {
|
|
|
183
232
|
export function formatUsageSnapshot(
|
|
184
233
|
snapshot: UsageSnapshot,
|
|
185
234
|
options: { showResetTimes: boolean },
|
|
235
|
+
now = Date.now(),
|
|
186
236
|
): string {
|
|
187
237
|
const fiveHour = formatPercent(snapshot.fiveHourLeftPercent);
|
|
188
238
|
const sevenDay = formatPercent(snapshot.sevenDayLeftPercent);
|
|
189
239
|
const resets = options.showResetTimes
|
|
190
240
|
? [
|
|
191
|
-
formatCompactReset(
|
|
192
|
-
|
|
241
|
+
formatCompactReset(
|
|
242
|
+
"5h",
|
|
243
|
+
remainingResetSeconds(snapshot.fiveHourResetInSeconds, snapshot.capturedAt, now),
|
|
244
|
+
undefined,
|
|
245
|
+
now,
|
|
246
|
+
),
|
|
247
|
+
formatCompactReset(
|
|
248
|
+
"7d",
|
|
249
|
+
remainingResetSeconds(snapshot.sevenDayResetInSeconds, snapshot.capturedAt, now),
|
|
250
|
+
{ includeDate: true },
|
|
251
|
+
now,
|
|
252
|
+
),
|
|
193
253
|
].filter((value): value is string => value !== null)
|
|
194
254
|
: [];
|
|
195
255
|
return `Usage: 5h: ${fiveHour} | 7d: ${sevenDay}${resets.length ? ` | ${resets.join(" | ")}` : ""}`;
|
|
196
256
|
}
|
|
257
|
+
|
|
258
|
+
function remainingResetSeconds(
|
|
259
|
+
seconds: number | null,
|
|
260
|
+
capturedAt: number,
|
|
261
|
+
now: number,
|
|
262
|
+
): number | null {
|
|
263
|
+
return seconds === null ? null : seconds - (now - capturedAt) / 1000;
|
|
264
|
+
}
|