pi-better-openai 0.1.6 → 0.1.8

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 CHANGED
@@ -7,7 +7,7 @@ A pi extension for OpenAI subscription workflows: fast mode, usage visibility, f
7
7
  Install from GitHub:
8
8
 
9
9
  ```bash
10
- pi install github:mattleong/pi-better-openai
10
+ pi install git:github.com/mattleong/pi-better-openai
11
11
  ```
12
12
 
13
13
  Or install from npm:
@@ -5,8 +5,8 @@
5
5
  * enabled and the selected model is in the configured allow-list.
6
6
  */
7
7
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
- import { CONFIG_BASENAME, STATUS_KEY } from "./identity.ts";
9
- import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./format.ts";
8
+ import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
9
+ import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./src/format.ts";
10
10
  import {
11
11
  DEFAULT_CONFIG,
12
12
  DEFAULT_IMAGE_CONFIG,
@@ -15,7 +15,6 @@ import {
15
15
  IMAGE_OUTPUT_FORMATS,
16
16
  IMAGE_SAVE_MODES,
17
17
  configPaths,
18
- type FooterMode,
19
18
  type ResolvedConfig,
20
19
  type SupportedModel,
21
20
  isRecord,
@@ -24,8 +23,8 @@ import {
24
23
  parseModels,
25
24
  readRawConfig,
26
25
  resolveConfig,
27
- writeConfig
28
- } from "./config.ts";
26
+ writeConfig,
27
+ } from "./src/config.ts";
29
28
  import {
30
29
  AUTH_FILE,
31
30
  type UsageSnapshot,
@@ -34,9 +33,9 @@ import {
34
33
  formatUsageSnapshot,
35
34
  parseUsageSnapshot,
36
35
  readCodexAuth,
37
- requestCodexUsage
38
- } from "./usage.ts";
39
- import { registerOpenAIImage, _imageTest } from "./image.ts";
36
+ requestCodexUsage,
37
+ } from "./src/usage.ts";
38
+ import { registerOpenAIImage, _imageTest } from "./src/image.ts";
40
39
 
41
40
  const COMMAND = "fast";
42
41
  const OPENAI_STATUS_COMMAND = "openai-usage";
@@ -49,7 +48,10 @@ type SettingsPickerItem = {
49
48
  description?: string;
50
49
  currentValue: string;
51
50
  values?: string[];
52
- submenu?: (currentValue: string, done: (selectedValue?: string) => void) => { render(width: number): string[]; invalidate(): void; handleInput?(data: string): void };
51
+ submenu?: (
52
+ currentValue: string,
53
+ done: (selectedValue?: string) => void,
54
+ ) => { render(width: number): string[]; invalidate(): void; handleInput?(data: string): void };
53
55
  };
54
56
 
55
57
  function currentModelKey(ctx: ExtensionContext): string {
@@ -57,8 +59,11 @@ function currentModelKey(ctx: ExtensionContext): string {
57
59
  }
58
60
 
59
61
  function supportsFast(ctx: ExtensionContext, supportedModels: SupportedModel[]): boolean {
60
- if (!ctx.model) return false;
61
- return supportedModels.some((model) => model.provider === ctx.model.provider && model.id === ctx.model.id);
62
+ const current = ctx.model;
63
+ if (!current) return false;
64
+ return supportedModels.some(
65
+ (model) => model.provider === current.provider && model.id === current.id,
66
+ );
62
67
  }
63
68
 
64
69
  function modelList(supportedModels: SupportedModel[]): string {
@@ -67,7 +72,12 @@ function modelList(supportedModels: SupportedModel[]): string {
67
72
  : "none configured";
68
73
  }
69
74
 
70
- function stateText(ctx: ExtensionContext, desiredActive: boolean, active: boolean, supportedModels: SupportedModel[]): string {
75
+ function stateText(
76
+ ctx: ExtensionContext,
77
+ desiredActive: boolean,
78
+ active: boolean,
79
+ supportedModels: SupportedModel[],
80
+ ): string {
71
81
  const model = currentModelKey(ctx);
72
82
  if (active) return `Fast mode is on for ${model}.`;
73
83
  if (desiredActive) {
@@ -77,7 +87,8 @@ function stateText(ctx: ExtensionContext, desiredActive: boolean, active: boolea
77
87
  }
78
88
 
79
89
  function isOpenAISubscriptionModel(ctx: ExtensionContext, cfg: ResolvedConfig): boolean {
80
- if (!ctx.model || (ctx.model.provider !== "openai" && ctx.model.provider !== "openai-codex")) return false;
90
+ if (!ctx.model || (ctx.model.provider !== "openai" && ctx.model.provider !== "openai-codex"))
91
+ return false;
81
92
  return !cfg.usage.showOnlyOnSubscriptionModels || ctx.modelRegistry.isUsingOAuth(ctx.model);
82
93
  }
83
94
 
@@ -113,7 +124,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
113
124
  function persist(nextConfig: ResolvedConfig): void {
114
125
  cachedConfig = { ...nextConfig, active, desiredActive };
115
126
  if (!nextConfig.persistState) return;
116
- writeConfig(nextConfig.configPath, { ...readRawConfig(nextConfig.configPath), active, desiredActive });
127
+ writeConfig(nextConfig.configPath, {
128
+ ...readRawConfig(nextConfig.configPath),
129
+ active,
130
+ desiredActive,
131
+ });
117
132
  }
118
133
 
119
134
  function applyDesiredFastState(ctx: ExtensionContext, cfg = config(ctx)): void {
@@ -127,13 +142,20 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
127
142
  persist(nextConfig);
128
143
  updateFooter(ctx);
129
144
  if (next && !active) {
130
- ctx.ui.notify(`Fast mode requested, but ${currentModelKey(ctx)} is unsupported. It will activate automatically when you switch to a supported model: ${modelList(nextConfig.supportedModels)}.`, "warning");
145
+ ctx.ui.notify(
146
+ `Fast mode requested, but ${currentModelKey(ctx)} is unsupported. It will activate automatically when you switch to a supported model: ${modelList(nextConfig.supportedModels)}.`,
147
+ "warning",
148
+ );
131
149
  return;
132
150
  }
133
151
  ctx.ui.notify(stateText(ctx, desiredActive, active, nextConfig.supportedModels), "info");
134
152
  }
135
153
 
136
- async function refreshUsage(ctx: ExtensionContext, modelId = ctx.model?.id, options?: { notify?: boolean }): Promise<void> {
154
+ async function refreshUsage(
155
+ ctx: ExtensionContext,
156
+ modelId = ctx.model?.id,
157
+ options?: { notify?: boolean },
158
+ ): Promise<void> {
137
159
  if (shuttingDown || !ctx.hasUI) return;
138
160
  if (usageRefreshInFlight) {
139
161
  queuedUsageRefresh = { ctx, modelId, notify: queuedUsageRefresh?.notify || options?.notify };
@@ -163,7 +185,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
163
185
  usageUpdatedAt = usageSnapshot ? Date.now() : undefined;
164
186
  usageError = data ? undefined : `Missing openai-codex OAuth credentials in ${AUTH_FILE}.`;
165
187
  if (!shuttingDown) updateFooter(ctx);
166
- if (!shuttingDown && options?.notify) ctx.ui.notify(formatUsageStatus(ctx), usageSnapshot ? "info" : "warning");
188
+ if (!shuttingDown && options?.notify)
189
+ ctx.ui.notify(formatUsageStatus(ctx), usageSnapshot ? "info" : "warning");
167
190
  } catch (error) {
168
191
  if (shuttingDown) return;
169
192
  usageError = error instanceof Error ? error.message : String(error);
@@ -215,25 +238,27 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
215
238
  `Last successful update: ${usageUpdatedAt ? new Date(usageUpdatedAt).toLocaleTimeString() : "never"}`,
216
239
  `Last error: ${usageError ?? "none"}`,
217
240
  `Refresh interval: ${cfg.usage.refreshIntervalMs}ms`,
218
- `Endpoint: https://chatgpt.com/backend-api/wham/usage`
241
+ `Endpoint: https://chatgpt.com/backend-api/wham/usage`,
219
242
  ].join("\n");
220
243
  }
221
244
 
222
245
  function formatUsageStatus(ctx: ExtensionContext): string {
223
246
  const cfg = config(ctx);
224
247
  if (!cfg.usage.enabled) return "Usage display is disabled.";
225
- if (!isOpenAISubscriptionModel(ctx, cfg)) return "Usage hidden: current model is not an OpenAI subscription model.";
248
+ if (!isOpenAISubscriptionModel(ctx, cfg))
249
+ return "Usage hidden: current model is not an OpenAI subscription model.";
226
250
  if (!usageSnapshot) return `Usage unavailable${usageError ? `: ${usageError}` : "."}`;
227
- const stale = usageUpdatedAt && Date.now() - usageUpdatedAt > cfg.usage.refreshIntervalMs * 2
228
- ? ` | stale ${formatResetCountdown((Date.now() - usageUpdatedAt) / 1000)}`
229
- : "";
251
+ const stale =
252
+ usageUpdatedAt && Date.now() - usageUpdatedAt > cfg.usage.refreshIntervalMs * 2
253
+ ? ` | stale ${formatResetCountdown((Date.now() - usageUpdatedAt) / 1000)}`
254
+ : "";
230
255
  return `${formatUsageSnapshot(usageSnapshot, cfg.usage)}${stale}`;
231
256
  }
232
257
 
233
258
  pi.registerFlag(FLAG, {
234
259
  description: "Start with OpenAI fast mode enabled (service_tier=priority)",
235
260
  type: "boolean",
236
- default: false
261
+ default: false,
237
262
  });
238
263
 
239
264
  function formatDebugStatus(ctx: ExtensionContext): string {
@@ -246,10 +271,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
246
271
  `Configured service_tier: ${SERVICE_TIER}`,
247
272
  `Last injected: ${lastInjectedAt ? `${new Date(lastInjectedAt).toLocaleTimeString()} (${lastInjectedModel}, ${lastInjectedTier})` : "never"}`,
248
273
  `Footer mode: ${cfg.footer.mode}`,
249
- `Usage enabled: ${cfg.usage.enabled}`,
274
+ "",
275
+ formatUsageDebug(ctx),
276
+ "",
250
277
  `Image enabled: ${cfg.image.enabled}`,
251
278
  `Image default save: ${cfg.image.defaultSave}`,
252
- `Config: ${cfg.configPath}`
279
+ `Config: ${cfg.configPath}`,
253
280
  ].join("\n");
254
281
  }
255
282
 
@@ -264,14 +291,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
264
291
  const arg = args.trim().toLowerCase();
265
292
  if (!arg) return setActive(ctx, !desiredActive);
266
293
  ctx.ui.notify("Usage: /fast", "error");
267
- }
294
+ },
268
295
  });
269
296
 
270
297
  pi.registerCommand(OPENAI_STATUS_COMMAND, {
271
298
  description: "Show OpenAI subscription usage status",
272
299
  handler: async (_args, ctx) => {
273
300
  ctx.ui.notify(formatOpenAIStatus(ctx), "info");
274
- }
301
+ },
275
302
  });
276
303
 
277
304
  function textPanel(title: string, lines: string[], done: () => void) {
@@ -283,27 +310,124 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
283
310
  invalidate() {},
284
311
  handleInput(data: string) {
285
312
  if (data.includes("\x1b") || data === "escape" || data === "q" || data === "\x03") done();
286
- }
313
+ },
287
314
  };
288
315
  }
289
316
 
290
317
  function buildSettingsItems(ctx: ExtensionContext, cfg: ResolvedConfig): SettingsPickerItem[] {
291
318
  return [
292
- { id: "fast.enabled", label: "Fast mode", currentValue: String(desiredActive), values: ["true", "false"], description: `Request OpenAI fast mode. Activates for supported models: ${modelList(cfg.supportedModels)}.` },
293
- { id: "persistState", label: "Persist fast state", currentValue: String(cfg.persistState), values: ["true", "false"], description: "Remember fast-mode state across sessions." },
294
- { id: "footer.mode", label: "Footer mode", currentValue: cfg.footer.mode, values: [...FOOTER_MODES], description: "replace = custom footer, status = pi footer plus status line, off = no Better OpenAI footer/status." },
295
- { id: "usage.enabled", label: "Usage display", currentValue: String(cfg.usage.enabled), values: ["true", "false"], description: "Fetch and display OpenAI subscription usage windows." },
296
- { id: "usage.refreshIntervalMs", label: "Usage refresh", currentValue: String(cfg.usage.refreshIntervalMs), values: ["15000", "30000", "60000", "120000", "300000", "600000"], description: "Usage refresh interval in milliseconds." },
297
- { id: "usage.showOnlyOnSubscriptionModels", label: "Usage only on OAuth", currentValue: String(cfg.usage.showOnlyOnSubscriptionModels), values: ["true", "false"], description: "Only show usage when the current OpenAI model uses subscription/OAuth auth." },
298
- { id: "usage.showResetTimes", label: "Usage reset times", currentValue: String(cfg.usage.showResetTimes), values: ["true", "false"], description: "Include compact reset countdowns and local reset times." },
299
- { id: "image.enabled", label: "Image tool", currentValue: String(cfg.image.enabled), values: ["true", "false"], description: "Allow the openai_image tool to make image requests." },
300
- { id: "image.defaultModel", label: "Image model", currentValue: cfg.image.defaultModel, values: ["gpt-5.5", "gpt-5.4", "gpt-5.2", "gpt-5"], description: "Mainline model used for image generation when current model is not openai-codex." },
301
- { id: "image.defaultSave", label: "Image save", currentValue: cfg.image.defaultSave, values: [...IMAGE_SAVE_MODES], description: "Where generated images are saved by default." },
302
- { id: "image.outputFormat", label: "Image format", currentValue: cfg.image.outputFormat, values: [...IMAGE_OUTPUT_FORMATS], description: "Generated image file format." },
303
- { id: "image.timeoutMs", label: "Image timeout", currentValue: String(cfg.image.timeoutMs), values: ["30000", "60000", "120000", "180000", "300000"], description: "Image request timeout in milliseconds." },
304
- { id: "debug", label: "Debug info", currentValue: "open", description: "Show Better OpenAI diagnostics.", submenu: (_value, done) => textPanel("Debug info", formatDebugStatus(ctx).split("\n"), () => done()) },
305
- { id: "config.path", label: "Config path", currentValue: cfg.configPath, description: `Project: ${cfg.projectConfigPath}\nGlobal: ${cfg.globalConfigPath}` },
306
- { id: "config.print", label: "Print config", currentValue: "open", description: "Show the selected raw config JSON.", submenu: (_value, done) => textPanel("Config", JSON.stringify(readRawConfig(cfg.configPath), null, 2).split("\n"), () => done()) }
319
+ {
320
+ id: "fast.enabled",
321
+ label: "Fast mode",
322
+ currentValue: String(desiredActive),
323
+ values: ["true", "false"],
324
+ description: `Request OpenAI fast mode. Activates for supported models: ${modelList(cfg.supportedModels)}.`,
325
+ },
326
+ {
327
+ id: "persistState",
328
+ label: "Persist fast state",
329
+ currentValue: String(cfg.persistState),
330
+ values: ["true", "false"],
331
+ description: "Remember fast-mode state across sessions.",
332
+ },
333
+ {
334
+ id: "footer.mode",
335
+ label: "Footer mode",
336
+ currentValue: cfg.footer.mode,
337
+ values: [...FOOTER_MODES],
338
+ description:
339
+ "replace = custom footer, status = pi footer plus status line, off = no Better OpenAI footer/status.",
340
+ },
341
+ {
342
+ id: "usage.enabled",
343
+ label: "Usage display",
344
+ currentValue: String(cfg.usage.enabled),
345
+ values: ["true", "false"],
346
+ description: "Fetch and display OpenAI subscription usage windows.",
347
+ },
348
+ {
349
+ id: "usage.refreshIntervalMs",
350
+ label: "Usage refresh",
351
+ currentValue: String(cfg.usage.refreshIntervalMs),
352
+ values: ["15000", "30000", "60000", "120000", "300000", "600000"],
353
+ description: "Usage refresh interval in milliseconds.",
354
+ },
355
+ {
356
+ id: "usage.showOnlyOnSubscriptionModels",
357
+ label: "Usage only on OAuth",
358
+ currentValue: String(cfg.usage.showOnlyOnSubscriptionModels),
359
+ values: ["true", "false"],
360
+ description: "Only show usage when the current OpenAI model uses subscription/OAuth auth.",
361
+ },
362
+ {
363
+ id: "usage.showResetTimes",
364
+ label: "Usage reset times",
365
+ currentValue: String(cfg.usage.showResetTimes),
366
+ values: ["true", "false"],
367
+ description: "Include compact reset countdowns and local reset times.",
368
+ },
369
+ {
370
+ id: "image.enabled",
371
+ label: "Image tool",
372
+ currentValue: String(cfg.image.enabled),
373
+ values: ["true", "false"],
374
+ description: "Allow the openai_image tool to make image requests.",
375
+ },
376
+ {
377
+ id: "image.defaultModel",
378
+ label: "Image model",
379
+ currentValue: cfg.image.defaultModel,
380
+ values: ["gpt-5.5", "gpt-5.4", "gpt-5.2", "gpt-5"],
381
+ description:
382
+ "Mainline model used for image generation when current model is not openai-codex.",
383
+ },
384
+ {
385
+ id: "image.defaultSave",
386
+ label: "Image save",
387
+ currentValue: cfg.image.defaultSave,
388
+ values: [...IMAGE_SAVE_MODES],
389
+ description: "Where generated images are saved by default.",
390
+ },
391
+ {
392
+ id: "image.outputFormat",
393
+ label: "Image format",
394
+ currentValue: cfg.image.outputFormat,
395
+ values: [...IMAGE_OUTPUT_FORMATS],
396
+ description: "Generated image file format.",
397
+ },
398
+ {
399
+ id: "image.timeoutMs",
400
+ label: "Image timeout",
401
+ currentValue: String(cfg.image.timeoutMs),
402
+ values: ["30000", "60000", "120000", "180000", "300000"],
403
+ description: "Image request timeout in milliseconds.",
404
+ },
405
+ {
406
+ id: "debug",
407
+ label: "Debug info",
408
+ currentValue: "open",
409
+ description: "Show Better OpenAI diagnostics.",
410
+ submenu: (_value, done) =>
411
+ textPanel("Debug info", formatDebugStatus(ctx).split("\n"), () => done()),
412
+ },
413
+ {
414
+ id: "config.path",
415
+ label: "Config path",
416
+ currentValue: cfg.configPath,
417
+ description: `Project: ${cfg.projectConfigPath}\nGlobal: ${cfg.globalConfigPath}`,
418
+ },
419
+ {
420
+ id: "config.print",
421
+ label: "Print config",
422
+ currentValue: "open",
423
+ description: "Show the selected raw config JSON.",
424
+ submenu: (_value, done) =>
425
+ textPanel(
426
+ "Config",
427
+ JSON.stringify(readRawConfig(cfg.configPath), null, 2).split("\n"),
428
+ () => done(),
429
+ ),
430
+ },
307
431
  ];
308
432
  }
309
433
 
@@ -332,7 +456,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
332
456
  } else if (id.startsWith("image.")) {
333
457
  const image = isRecord(current.image) ? current.image : {};
334
458
  const key = id.slice("image.".length);
335
- image[key] = key === "timeoutMs" ? num : rawValue === "true" ? true : rawValue === "false" ? false : rawValue;
459
+ image[key] =
460
+ key === "timeoutMs"
461
+ ? num
462
+ : rawValue === "true"
463
+ ? true
464
+ : rawValue === "false"
465
+ ? false
466
+ : rawValue;
336
467
  current.image = image;
337
468
  }
338
469
  writeConfig(cfg.configPath, current);
@@ -352,37 +483,51 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
352
483
  async function showSettingsPicker(ctx: ExtensionContext): Promise<void> {
353
484
  const [{ getSettingsListTheme }, { Container, SettingsList }] = await Promise.all([
354
485
  import("@mariozechner/pi-coding-agent"),
355
- import("@mariozechner/pi-tui")
486
+ import("@mariozechner/pi-tui"),
356
487
  ]);
357
488
  await ctx.ui.custom((tui, theme, _kb, done) => {
358
489
  const container = new Container();
359
- container.addChild(new (class {
360
- render(_width: number) {
361
- const cfg = config(ctx);
362
- return [theme.fg("accent", theme.bold("Better OpenAI Settings")), theme.fg("dim", cfg.configPath), ""];
363
- }
364
- invalidate() {}
365
- })());
490
+ container.addChild(
491
+ new (class {
492
+ render(_width: number) {
493
+ const cfg = config(ctx);
494
+ return [
495
+ theme.fg("accent", theme.bold("Better OpenAI Settings")),
496
+ theme.fg("dim", cfg.configPath),
497
+ "",
498
+ ];
499
+ }
500
+ invalidate() {}
501
+ })(),
502
+ );
366
503
  const settingsList = new SettingsList(
367
504
  buildSettingsItems(ctx, refresh(ctx)),
368
505
  13,
369
506
  getSettingsListTheme(),
370
507
  (id, newValue) => {
371
508
  writeSetting(ctx, id, newValue);
372
- settingsList.updateValue(id, buildSettingsItems(ctx, config(ctx)).find((item) => item.id === id)?.currentValue ?? newValue);
509
+ settingsList.updateValue(
510
+ id,
511
+ buildSettingsItems(ctx, config(ctx)).find((item) => item.id === id)?.currentValue ??
512
+ newValue,
513
+ );
373
514
  tui.requestRender();
374
515
  },
375
516
  () => done(undefined),
376
- { enableSearch: true }
517
+ { enableSearch: true },
377
518
  );
378
519
  container.addChild(settingsList);
379
520
  return {
380
- render(width: number) { return container.render(width); },
381
- invalidate() { container.invalidate(); },
521
+ render(width: number) {
522
+ return container.render(width);
523
+ },
524
+ invalidate() {
525
+ container.invalidate();
526
+ },
382
527
  handleInput(data: string) {
383
528
  settingsList.handleInput(data);
384
529
  tui.requestRender();
385
- }
530
+ },
386
531
  };
387
532
  });
388
533
  }
@@ -391,10 +536,9 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
391
536
  description: "Open Better OpenAI settings picker",
392
537
  handler: async (_args, ctx) => {
393
538
  await showSettingsPicker(ctx);
394
- }
539
+ },
395
540
  });
396
541
 
397
-
398
542
  registerOpenAIImage(pi, config);
399
543
 
400
544
  function installFooter(ctx: ExtensionContext): void {
@@ -437,18 +581,24 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
437
581
  if (totalCacheWrite) parts.push(`W${formatTokens(totalCacheWrite)}`);
438
582
 
439
583
  const usingSubscription = ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
440
- if (totalCost || usingSubscription) parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
584
+ if (totalCost || usingSubscription)
585
+ parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
441
586
 
442
587
  const contextUsage = ctx.getContextUsage();
443
588
  const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
444
589
  const contextPercentValue = contextUsage?.percent ?? 0;
445
- const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
446
- const contextDisplay = contextPercent === "?" ? `?/${formatTokens(contextWindow)} (auto)` : `${contextPercent}%/${formatTokens(contextWindow)} (auto)`;
447
- const contextText = contextPercentValue > 90
448
- ? theme.fg("error", contextDisplay)
449
- : contextPercentValue > 70
450
- ? theme.fg("warning", contextDisplay)
451
- : contextDisplay;
590
+ const contextPercent =
591
+ contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
592
+ const contextDisplay =
593
+ contextPercent === "?"
594
+ ? `?/${formatTokens(contextWindow)} (auto)`
595
+ : `${contextPercent}%/${formatTokens(contextWindow)} (auto)`;
596
+ const contextText =
597
+ contextPercentValue > 90
598
+ ? theme.fg("error", contextDisplay)
599
+ : contextPercentValue > 70
600
+ ? theme.fg("warning", contextDisplay)
601
+ : contextDisplay;
452
602
  parts.push(contextText);
453
603
 
454
604
  let usageLine: string | undefined;
@@ -466,12 +616,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
466
616
 
467
617
  const modelName = ctx.model?.id || "no-model";
468
618
  const thinkingLevel = pi.getThinkingLevel();
469
- const fastSuffix = active && supportsFast(ctx, config(ctx).supportedModels) ? " fast" : "";
619
+ const fastSuffix =
620
+ active && supportsFast(ctx, config(ctx).supportedModels) ? " fast" : "";
470
621
  let rightWithoutProvider = modelName;
471
622
  if (ctx.model?.reasoning) {
472
- rightWithoutProvider = thinkingLevel === "off"
473
- ? `${modelName}${fastSuffix} • thinking off`
474
- : `${modelName}${fastSuffix} • ${thinkingLevel}`;
623
+ rightWithoutProvider =
624
+ thinkingLevel === "off"
625
+ ? `${modelName}${fastSuffix} • thinking off`
626
+ : `${modelName}${fastSuffix} • ${thinkingLevel}`;
475
627
  } else if (fastSuffix) {
476
628
  rightWithoutProvider = `${modelName}${fastSuffix}`;
477
629
  }
@@ -491,7 +643,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
491
643
  const availableForRight = width - statsLeftWidth - 2;
492
644
  if (availableForRight > 0) {
493
645
  const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
494
- statsLine = statsLeft + " ".repeat(Math.max(0, width - statsLeftWidth - visibleWidth(truncatedRight))) + truncatedRight;
646
+ statsLine =
647
+ statsLeft +
648
+ " ".repeat(Math.max(0, width - statsLeftWidth - visibleWidth(truncatedRight))) +
649
+ truncatedRight;
495
650
  } else {
496
651
  statsLine = statsLeft;
497
652
  }
@@ -499,7 +654,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
499
654
 
500
655
  const lines = [
501
656
  truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")),
502
- theme.fg("dim", statsLeft) + theme.fg("dim", statsLine.slice(statsLeft.length))
657
+ theme.fg("dim", statsLeft) + theme.fg("dim", statsLine.slice(statsLeft.length)),
503
658
  ];
504
659
 
505
660
  if (usageLine) {
@@ -516,7 +671,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
516
671
  }
517
672
 
518
673
  return lines;
519
- }
674
+ },
520
675
  };
521
676
  });
522
677
  }
@@ -535,8 +690,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
535
690
  ctx.ui.setStatus(STATUS_KEY, undefined);
536
691
  return;
537
692
  }
538
- const fast = active && supportsFast(ctx, cfg.supportedModels) ? `${ctx.model?.id ?? "model"} fast` : undefined;
539
- const usage = usageSnapshot && cfg.usage.enabled && isOpenAISubscriptionModel(ctx, cfg) ? formatUsageSnapshot(usageSnapshot, cfg.usage) : undefined;
693
+ const fast =
694
+ active && supportsFast(ctx, cfg.supportedModels)
695
+ ? `${ctx.model?.id ?? "model"} fast`
696
+ : undefined;
697
+ const usage =
698
+ usageSnapshot && cfg.usage.enabled && isOpenAISubscriptionModel(ctx, cfg)
699
+ ? formatUsageSnapshot(usageSnapshot, cfg.usage)
700
+ : undefined;
540
701
  ctx.ui.setStatus(STATUS_KEY, [fast, usage].filter(Boolean).join(" | ") || undefined);
541
702
  }
542
703
 
@@ -545,14 +706,19 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
545
706
  desiredActive = nextConfig.persistState ? nextConfig.desiredActive : false;
546
707
  if (pi.getFlag(FLAG) === true) desiredActive = true;
547
708
  applyDesiredFastState(ctx, nextConfig);
548
- if (desiredActive !== nextConfig.desiredActive || active !== nextConfig.active) persist(nextConfig);
709
+ if (desiredActive !== nextConfig.desiredActive || active !== nextConfig.active)
710
+ persist(nextConfig);
549
711
  if (desiredActive && !active) {
550
- ctx.ui.notify(`Fast mode requested, but ${currentModelKey(ctx)} is unsupported. It will activate automatically when you switch to a supported model: ${modelList(nextConfig.supportedModels)}.`, "warning");
712
+ ctx.ui.notify(
713
+ `Fast mode requested, but ${currentModelKey(ctx)} is unsupported. It will activate automatically when you switch to a supported model: ${modelList(nextConfig.supportedModels)}.`,
714
+ "warning",
715
+ );
551
716
  }
552
717
  refreshFooterTotals(ctx);
553
718
  updateFooter(ctx);
554
719
  startUsageRefresh(ctx);
555
- if (active) ctx.ui.notify(stateText(ctx, desiredActive, active, nextConfig.supportedModels), "info");
720
+ if (active)
721
+ ctx.ui.notify(stateText(ctx, desiredActive, active, nextConfig.supportedModels), "info");
556
722
  });
557
723
 
558
724
  pi.on("turn_end", (_event, ctx) => {
@@ -577,7 +743,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
577
743
  applyDesiredFastState(ctx, cfg);
578
744
  if (active !== wasActive) {
579
745
  persist(cfg);
580
- ctx.ui.notify(active ? stateText(ctx, desiredActive, active, cfg.supportedModels) : `Fast mode inactive for unsupported model ${currentModelKey(ctx)}.`, active ? "info" : "warning");
746
+ ctx.ui.notify(
747
+ active
748
+ ? stateText(ctx, desiredActive, active, cfg.supportedModels)
749
+ : `Fast mode inactive for unsupported model ${currentModelKey(ctx)}.`,
750
+ active ? "info" : "warning",
751
+ );
581
752
  }
582
753
  updateFooter(ctx);
583
754
  void refreshUsage(ctx, event.model.id);
@@ -594,7 +765,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
594
765
 
595
766
  pi.on("before_provider_request", (event, ctx) => {
596
767
  const nextConfig = config(ctx);
597
- if (!active || !supportsFast(ctx, nextConfig.supportedModels) || !isRecord(event.payload)) return;
768
+ if (!active || !supportsFast(ctx, nextConfig.supportedModels) || !isRecord(event.payload))
769
+ return;
598
770
  lastInjectedAt = Date.now();
599
771
  lastInjectedModel = currentModelKey(ctx);
600
772
  lastInjectedTier = SERVICE_TIER;
@@ -619,5 +791,5 @@ export const _test = {
619
791
  formatPercent,
620
792
  formatUsageSnapshot,
621
793
  readCodexAuth,
622
- imageTest: _imageTest
794
+ imageTest: _imageTest,
623
795
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-openai",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
@@ -19,7 +19,8 @@
19
19
  "url": "git+https://github.com/mattleong/pi-better-openai.git"
20
20
  },
21
21
  "files": [
22
- "extensions/",
22
+ "index.ts",
23
+ "src/",
23
24
  "README.md",
24
25
  "LICENSE"
25
26
  ],
@@ -28,7 +29,19 @@
28
29
  "access": "public"
29
30
  },
30
31
  "scripts": {
31
- "test": "node tests/basic.mjs"
32
+ "check": "npm run typecheck && npm run lint && npm run format:check && npm test",
33
+ "format": "oxfmt .",
34
+ "format:check": "oxfmt --check .",
35
+ "lint": "oxlint --deny-warnings .",
36
+ "lint:fix": "oxlint --fix .",
37
+ "test": "node tests/basic.mjs",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.6.0",
42
+ "oxfmt": "^0.47.0",
43
+ "oxlint": "^1.62.0",
44
+ "typescript": "^6.0.3"
32
45
  },
33
46
  "peerDependencies": {
34
47
  "@mariozechner/pi-coding-agent": ">=0.57.0",
@@ -36,7 +49,7 @@
36
49
  },
37
50
  "pi": {
38
51
  "extensions": [
39
- "./extensions/pi-better-openai.ts"
52
+ "./index.ts"
40
53
  ]
41
54
  }
42
55
  }