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/src/usage.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
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.isNaN(value)) return null;
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.isNaN(seconds)) return null;
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.isNaN(seconds)) return null;
67
- const resetDate = new Date(Date.now() + Math.max(0, seconds) * 1000);
68
- const now = new Date();
69
- const time = resetDate.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
70
- if (!options?.includeDate && resetDate.toDateString() === now.toDateString()) return time;
71
- const weekday = resetDate.toLocaleDateString(undefined, { weekday: "short" });
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 = resetDate.toLocaleDateString(undefined, { month: "numeric", day: "numeric" });
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 (typeof window?.reset_after_seconds === "number" && !Number.isNaN(window.reset_after_seconds))
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.isNaN(window.reset_at)) return null;
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 - Date.now() / 1000);
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
- modelId === SPARK_MODEL_ID
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" && !Number.isNaN(value)
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("5h", snapshot.fiveHourResetInSeconds),
192
- formatCompactReset("7d", snapshot.sevenDayResetInSeconds, { includeDate: true }),
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
+ }