@monotykamary/pi-better-openai 0.1.42 → 0.1.43

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/index.ts CHANGED
@@ -62,8 +62,6 @@ import {
62
62
  formatUsageSnapshot,
63
63
  parseUsageSnapshot,
64
64
  readCodexAuth,
65
- requestCodexUsage,
66
- type UsageSnapshot,
67
65
  } from "./src/usage.ts";
68
66
  import {
69
67
  buildBankedResetConfirmation,
@@ -71,10 +69,9 @@ import {
71
69
  formatBankedResetChoice,
72
70
  formatConsumeOutcome,
73
71
  newRedeemRequestId,
74
- requestBankedResetCredits,
75
72
  selectBankedResetCredit,
76
- type BankedResetCredits,
77
73
  } from "./src/resets.ts";
74
+ import { ResetController } from "./src/reset-controller.ts";
78
75
  import { registerOpenAIImage, _imageTest } from "./src/image.ts";
79
76
  import { registerOpenAIWebSearch, _websearchTest } from "./src/websearch.ts";
80
77
  import { registerOpenAILive } from "./src/live/index.ts";
@@ -90,7 +87,7 @@ import {
90
87
  _petsTest,
91
88
  } from "./src/pets.ts";
92
89
  import { FastController, modelList, supportsFast } from "./src/fast-controller.ts";
93
- import { UsageController } from "./src/usage-controller.ts";
90
+ import { isOpenAISubscriptionModel, UsageController } from "./src/usage-controller.ts";
94
91
  import { PetFooterController } from "./src/pet-footer-controller.ts";
95
92
  import {
96
93
  abbreviateHomePath,
@@ -247,6 +244,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
247
244
  let cachedSessionNameLeafId: string | null | undefined;
248
245
  let cachedSessionName: string | undefined;
249
246
  const usageController = new UsageController(config, updateFooter);
247
+ const resetController = new ResetController();
250
248
  const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
251
249
  let multiproviderService: MultiproviderService | undefined;
252
250
  let unsubscribeMultiprovider: (() => void) | undefined;
@@ -265,6 +263,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
265
263
  setActiveMultiproviderService(value);
266
264
  unsubscribeMultiprovider = value.onActiveAccountChanged(CODEX_PROVIDER_ID, (event) => {
267
265
  void usageController.refresh(event.ctx, undefined, { force: true });
266
+ void resetController.refresh(event.ctx, { force: true }).catch(() => {});
268
267
  updateFooter(event.ctx);
269
268
  });
270
269
  const ctx = multiproviderRefreshCtx;
@@ -402,27 +401,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
402
401
  ctx.ui.notify("/openai-resets requires an interactive TUI session.", "warning");
403
402
  return;
404
403
  }
405
- const timeoutSignal = AbortSignal.timeout(15_000);
406
- const signal = ctx.signal ? AbortSignal.any([ctx.signal, timeoutSignal]) : timeoutSignal;
407
- let credits: BankedResetCredits | undefined;
408
- let snapshot: UsageSnapshot | undefined;
409
- try {
410
- const [creditResult, usageResult] = await Promise.all([
411
- requestBankedResetCredits(ctx, signal),
412
- requestCodexUsage(ctx, signal).catch(() => undefined),
413
- ]);
414
- credits = creditResult;
415
- snapshot = usageResult ? parseUsageSnapshot(usageResult, ctx.model?.id) : undefined;
416
- } catch (error) {
417
- ctx.ui.notify(
418
- `Banked reset lookup failed: ${sanitizeDiagnosticError(
419
- error instanceof Error ? error.message : String(error),
420
- )}`,
421
- "error",
422
- );
404
+ let credits = resetController.snapshot?.credits;
405
+ if (!credits) {
406
+ await resetController.refresh(ctx, { force: true }).catch(() => {});
407
+ credits = resetController.snapshot?.credits;
408
+ }
409
+ if (!credits) {
410
+ const reason = resetController.lastError;
411
+ ctx.ui.notify(`Banked reset lookup failed${reason ? `: ${reason}` : "."}`, "error");
423
412
  return;
424
413
  }
425
- if (!credits || credits.availableCount <= 0) {
414
+ void resetController.refresh(ctx).catch(() => {});
415
+ if (credits.availableCount <= 0) {
426
416
  ctx.ui.notify("No banked Codex resets are available for this account.", "info");
427
417
  return;
428
418
  }
@@ -440,17 +430,20 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
440
430
  const confirmation = buildBankedResetConfirmation({
441
431
  credit: selected,
442
432
  availableCount: credits.availableCount,
443
- snapshot,
433
+ snapshot: usageController.snapshot,
444
434
  });
445
435
  if (!(await ctx.ui.confirm(confirmation.title, confirmation.message))) {
446
436
  ctx.ui.notify("Banked reset redemption cancelled.", "info");
447
437
  return;
448
438
  }
449
439
  try {
440
+ const timeoutSignal = AbortSignal.timeout(15_000);
441
+ const signal = ctx.signal ? AbortSignal.any([ctx.signal, timeoutSignal]) : timeoutSignal;
450
442
  const result = await consumeBankedReset(ctx, selected?.id, newRedeemRequestId(), signal);
451
443
  const outcome = formatConsumeOutcome(result);
452
444
  ctx.ui.notify(outcome.message, outcome.level);
453
445
  void usageController.refresh(ctx, ctx.model?.id, { force: true });
446
+ void resetController.refresh(ctx, { force: true }).catch(() => {});
454
447
  updateFooter(ctx);
455
448
  } catch (error) {
456
449
  ctx.ui.notify(
@@ -1393,6 +1386,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1393
1386
  updateFooter(ctx);
1394
1387
  if (hasTerminalUI(ctx) && nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
1395
1388
  usageController.start(ctx);
1389
+ if (nextConfig.usage.enabled && isOpenAISubscriptionModel(ctx, nextConfig))
1390
+ resetController.start(ctx);
1396
1391
  if (fastController.active) ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
1397
1392
  });
1398
1393
 
@@ -1469,6 +1464,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1469
1464
  invalidateSessionName();
1470
1465
  multiproviderRefreshCtx = undefined;
1471
1466
  usageController.shutdown();
1467
+ resetController.stop();
1472
1468
  petController.shutdown();
1473
1469
  });
1474
1470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-openai",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "Improve OpenAI in pi with fast mode, usage stats, realtime voice, image generation, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
@@ -0,0 +1,97 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { sanitizeDiagnosticError } from "./format.ts";
3
+ import { type BankedResetCredits, requestBankedResetCredits } from "./resets.ts";
4
+
5
+ export const BANKED_RESET_CACHE_TTL_MS = 5 * 60_000;
6
+ const BANKED_RESET_REQUEST_TIMEOUT_MS = 10_000;
7
+
8
+ export type BankedResetCache = {
9
+ credits: BankedResetCredits;
10
+ updatedAt: number;
11
+ };
12
+
13
+ // Keeps the banked-reset credit list warm so /openai-resets opens without
14
+ // paying the credential-resolution + wham round trip. The cache is prefetched
15
+ // at session start, refreshed on a TTL, and served even when stale (a stale
16
+ // open still kicks off a background refresh; the backend re-validates every
17
+ // redemption server-side, so a stale offer can never be consumed twice).
18
+ export class ResetController {
19
+ private cache: BankedResetCache | undefined;
20
+ private error: string | undefined;
21
+ private lastFetchAt: number | undefined;
22
+ private inFlight: Promise<void> | undefined;
23
+ private timer: ReturnType<typeof setInterval> | undefined;
24
+ private sessionSignal: AbortSignal | undefined;
25
+ private sessionAbortHandler: (() => void) | undefined;
26
+
27
+ get snapshot(): BankedResetCache | undefined {
28
+ return this.cache;
29
+ }
30
+
31
+ get lastError(): string | undefined {
32
+ return this.error;
33
+ }
34
+
35
+ isFresh(now = Date.now()): boolean {
36
+ return this.cache !== undefined && now - this.cache.updatedAt < BANKED_RESET_CACHE_TTL_MS;
37
+ }
38
+
39
+ refresh(ctx: ExtensionContext, options?: { force?: boolean }): Promise<void> {
40
+ if (this.inFlight) return this.inFlight;
41
+ if (
42
+ !options?.force &&
43
+ this.lastFetchAt !== undefined &&
44
+ Date.now() - this.lastFetchAt < BANKED_RESET_CACHE_TTL_MS
45
+ )
46
+ return Promise.resolve();
47
+ this.lastFetchAt = Date.now();
48
+ const timeoutSignal = AbortSignal.timeout(BANKED_RESET_REQUEST_TIMEOUT_MS);
49
+ const signal = ctx.signal ? AbortSignal.any([ctx.signal, timeoutSignal]) : timeoutSignal;
50
+ const task = (async () => {
51
+ try {
52
+ const credits = await requestBankedResetCredits(ctx, signal);
53
+ if (credits) {
54
+ this.cache = { credits, updatedAt: Date.now() };
55
+ this.error = undefined;
56
+ } else {
57
+ this.error = "OpenAI Codex credentials unavailable.";
58
+ }
59
+ } catch (error) {
60
+ this.error = sanitizeDiagnosticError(
61
+ error instanceof Error ? error.message : String(error),
62
+ );
63
+ } finally {
64
+ this.inFlight = undefined;
65
+ }
66
+ })();
67
+ this.inFlight = task;
68
+ return task;
69
+ }
70
+
71
+ start(ctx: ExtensionContext): void {
72
+ this.stop();
73
+ if (ctx.signal?.aborted) return;
74
+ this.sessionSignal = ctx.signal;
75
+ this.sessionAbortHandler = () => this.stop();
76
+ ctx.signal?.addEventListener("abort", this.sessionAbortHandler, { once: true });
77
+ void this.refresh(ctx).catch(() => {});
78
+ this.timer = setInterval(() => {
79
+ if (ctx.signal?.aborted) {
80
+ this.stop();
81
+ return;
82
+ }
83
+ void this.refresh(ctx).catch(() => {});
84
+ }, BANKED_RESET_CACHE_TTL_MS);
85
+ this.timer.unref?.();
86
+ }
87
+
88
+ stop(): void {
89
+ if (this.timer) clearInterval(this.timer);
90
+ this.timer = undefined;
91
+ if (this.sessionSignal && this.sessionAbortHandler) {
92
+ this.sessionSignal.removeEventListener("abort", this.sessionAbortHandler);
93
+ }
94
+ this.sessionSignal = undefined;
95
+ this.sessionAbortHandler = undefined;
96
+ }
97
+ }