@monotykamary/pi-better-openai 0.1.43 → 0.1.45
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 +1 -1
- package/index.ts +59 -10
- package/package.json +1 -1
- package/src/usage-controller.ts +15 -3
- package/src/usage.ts +53 -18
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Usage display, image generation, and live voice require pi's `openai-codex` OAut
|
|
|
26
26
|
2. Verify subscription usage with `/openai-usage`, or open `/openai-settings` and check **Diagnostics**.
|
|
27
27
|
3. The extension reads auth from pi's agent auth store, normally `~/.pi/agent/auth.json`. Do not copy, paste, or commit values from this file.
|
|
28
28
|
4. If `PI_CODING_AGENT_DIR` is set, the auth store, global extension config, and global generated-image directory use that agent directory instead of `~/.pi/agent`. A leading `~/` is expanded to your home directory.
|
|
29
|
-
5. When [pi-multiprovider](https://github.com/monotykamary/pi-multiprovider) 0.
|
|
29
|
+
5. When [pi-multiprovider](https://github.com/monotykamary/pi-multiprovider) 0.8.0+ pools several `openai-codex` accounts, the session's active account (chosen with `/switch-account`) is resolved first for usage display, image generation, web search, and live voice; the usage widget refreshes on every switch and whenever a resumed session restores the account, so it never keeps billing the account the session used before. Without that extension, credential resolution is unchanged.
|
|
30
30
|
|
|
31
31
|
## Features
|
|
32
32
|
|
package/index.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* Adds `service_tier: "priority"` to OpenAI provider payloads while fast mode is
|
|
5
5
|
* enabled and the selected model is in the configured allow-list.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type ExtensionAPI,
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
type Theme,
|
|
11
|
+
type ThemeColor,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
13
|
import {
|
|
9
14
|
Container,
|
|
10
15
|
Key,
|
|
@@ -58,6 +63,8 @@ import {
|
|
|
58
63
|
applySettingToRawConfig,
|
|
59
64
|
} from "./src/config.ts";
|
|
60
65
|
import {
|
|
66
|
+
type UsageSegment,
|
|
67
|
+
type UsageSeverity,
|
|
61
68
|
formatPercent,
|
|
62
69
|
formatUsageSnapshot,
|
|
63
70
|
parseUsageSnapshot,
|
|
@@ -114,6 +121,46 @@ const requireSettingsListTheme = (): SettingsListTheme => {
|
|
|
114
121
|
return loadedSettingsListTheme;
|
|
115
122
|
};
|
|
116
123
|
|
|
124
|
+
const USAGE_SEVERITY_COLORS: Record<UsageSeverity, ThemeColor> = {
|
|
125
|
+
ok: "success",
|
|
126
|
+
warning: "warning",
|
|
127
|
+
critical: "error",
|
|
128
|
+
muted: "dim",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Keep a coloured segment on one line. Unlike sanitizeStatusText this leaves the
|
|
133
|
+
* spacing shared by neighbouring segments ("Usage: " + "5h: " + "90%") intact.
|
|
134
|
+
*/
|
|
135
|
+
function flattenSegment(text: string): string {
|
|
136
|
+
return text.replace(/[\r\n\t]+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Colour a usage line segment by segment: the percentages carry the severity
|
|
141
|
+
* colour while the labels, countdowns and banked resets stay dim.
|
|
142
|
+
*/
|
|
143
|
+
function colorizeUsageSegments(segments: UsageSegment[], theme: Theme): string {
|
|
144
|
+
return segments
|
|
145
|
+
.map((segment) =>
|
|
146
|
+
theme.fg(USAGE_SEVERITY_COLORS[segment.severity], flattenSegment(segment.text)),
|
|
147
|
+
)
|
|
148
|
+
.join("");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Pair the fast-mode segment with the usage segments for the status widget. */
|
|
152
|
+
function statusWidgetParts(
|
|
153
|
+
fast: string | undefined,
|
|
154
|
+
usage: UsageSegment[] | undefined,
|
|
155
|
+
): UsageSegment[] | undefined {
|
|
156
|
+
const parts: UsageSegment[] = fast ? [{ text: fast, severity: "muted" }] : [];
|
|
157
|
+
if (usage?.length) {
|
|
158
|
+
if (parts.length > 0) parts.push({ text: " · ", severity: "muted" });
|
|
159
|
+
parts.push(...usage);
|
|
160
|
+
}
|
|
161
|
+
return parts.length > 0 ? parts : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
117
164
|
class DynamicBorder {
|
|
118
165
|
readonly #color: (text: string) => string;
|
|
119
166
|
|
|
@@ -1188,8 +1235,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1188
1235
|
const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
|
|
1189
1236
|
const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
|
|
1190
1237
|
|
|
1191
|
-
const
|
|
1192
|
-
const usageLine =
|
|
1238
|
+
const usageStatusSegments = usageController.statusSegments(ctx, cfg, usingSubscription);
|
|
1239
|
+
const usageLine = usageStatusSegments
|
|
1240
|
+
? colorizeUsageSegments(usageStatusSegments, theme)
|
|
1241
|
+
: undefined;
|
|
1193
1242
|
|
|
1194
1243
|
let statsLeft = parts.join(" ");
|
|
1195
1244
|
let statsLeftWidth = visibleWidth(statsLeft);
|
|
@@ -1315,22 +1364,22 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1315
1364
|
statusInstalled = text !== undefined;
|
|
1316
1365
|
}
|
|
1317
1366
|
|
|
1318
|
-
function setStatusWidget(ctx: ExtensionContext,
|
|
1319
|
-
if (!
|
|
1367
|
+
function setStatusWidget(ctx: ExtensionContext, parts: UsageSegment[] | undefined): void {
|
|
1368
|
+
if (!parts && !statusWidgetInstalled) return;
|
|
1320
1369
|
ctx.ui.setWidget(
|
|
1321
1370
|
STATUS_KEY,
|
|
1322
|
-
|
|
1371
|
+
parts
|
|
1323
1372
|
? (_tui, theme) => ({
|
|
1324
1373
|
invalidate() {},
|
|
1325
1374
|
render(width: number): string[] {
|
|
1326
|
-
const line =
|
|
1375
|
+
const line = colorizeUsageSegments(parts, theme);
|
|
1327
1376
|
return [truncateToWidth(line, width, theme.fg("dim", "..."))];
|
|
1328
1377
|
},
|
|
1329
1378
|
})
|
|
1330
1379
|
: undefined,
|
|
1331
1380
|
{ placement: "belowEditor" },
|
|
1332
1381
|
);
|
|
1333
|
-
statusWidgetInstalled =
|
|
1382
|
+
statusWidgetInstalled = parts !== undefined;
|
|
1334
1383
|
}
|
|
1335
1384
|
|
|
1336
1385
|
function updateFooter(ctx: ExtensionContext): void {
|
|
@@ -1366,8 +1415,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
|
|
|
1366
1415
|
}
|
|
1367
1416
|
|
|
1368
1417
|
const fast = fastController.statusSegment(ctx, cfg);
|
|
1369
|
-
const usage = usageController.
|
|
1370
|
-
setStatusWidget(ctx,
|
|
1418
|
+
const usage = usageController.statusSegments(ctx, cfg);
|
|
1419
|
+
setStatusWidget(ctx, statusWidgetParts(fast, usage));
|
|
1371
1420
|
}
|
|
1372
1421
|
|
|
1373
1422
|
pi.on("session_start", (_event, ctx) => {
|
package/package.json
CHANGED
package/src/usage-controller.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ResolvedConfig } from "./config.ts";
|
|
|
3
3
|
import { maskIdentifier, sanitizeDiagnosticError } from "./format.ts";
|
|
4
4
|
import {
|
|
5
5
|
AUTH_FILE,
|
|
6
|
+
type UsageSegment,
|
|
6
7
|
type UsageSnapshot,
|
|
7
8
|
formatResetCountdown,
|
|
8
9
|
formatUsageSnapshot,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
readCodexAuth,
|
|
11
12
|
requestCodexUsage,
|
|
12
13
|
usageScopeForModel,
|
|
14
|
+
usageSegments,
|
|
13
15
|
} from "./usage.ts";
|
|
14
16
|
import { currentModelKey } from "./fast-controller.ts";
|
|
15
17
|
|
|
@@ -70,20 +72,30 @@ export class UsageController {
|
|
|
70
72
|
return this.usageSnapshot;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
statusSegments(
|
|
74
76
|
ctx: ExtensionContext,
|
|
75
77
|
cfg = this.getConfig(ctx),
|
|
76
78
|
isUsingOAuth?: boolean,
|
|
77
|
-
):
|
|
79
|
+
): UsageSegment[] | undefined {
|
|
78
80
|
return this.usageSnapshot &&
|
|
79
81
|
!this.usageError &&
|
|
80
82
|
this.usageSnapshot.scope === usageScopeForModel(ctx.model?.id) &&
|
|
81
83
|
cfg.usage.enabled &&
|
|
82
84
|
isOpenAISubscriptionModel(ctx, cfg, isUsingOAuth)
|
|
83
|
-
?
|
|
85
|
+
? usageSegments(this.usageSnapshot, cfg.usage)
|
|
84
86
|
: undefined;
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
statusLine(
|
|
90
|
+
ctx: ExtensionContext,
|
|
91
|
+
cfg = this.getConfig(ctx),
|
|
92
|
+
isUsingOAuth?: boolean,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
return this.statusSegments(ctx, cfg, isUsingOAuth)
|
|
95
|
+
?.map((segment) => segment.text)
|
|
96
|
+
.join("");
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
formatStatus(ctx: ExtensionContext): string {
|
|
88
100
|
const cfg = this.getConfig(ctx);
|
|
89
101
|
if (!cfg.usage.enabled) return "Usage display is disabled.";
|
package/src/usage.ts
CHANGED
|
@@ -253,11 +253,29 @@ export function formatPercent(value: number | null): string {
|
|
|
253
253
|
: "--";
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
export
|
|
256
|
+
export type UsageSeverity = "ok" | "warning" | "critical" | "muted";
|
|
257
|
+
|
|
258
|
+
export type UsageSegment = {
|
|
259
|
+
text: string;
|
|
260
|
+
severity: UsageSeverity;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** Remaining budget at or below these thresholds turns the percentage amber/red. */
|
|
264
|
+
const WARNING_LEFT_PERCENT = 30;
|
|
265
|
+
const CRITICAL_LEFT_PERCENT = 10;
|
|
266
|
+
|
|
267
|
+
export function severityForLeftPercent(percent: number | null): UsageSeverity {
|
|
268
|
+
if (percent === null) return "muted";
|
|
269
|
+
if (percent <= CRITICAL_LEFT_PERCENT) return "critical";
|
|
270
|
+
if (percent <= WARNING_LEFT_PERCENT) return "warning";
|
|
271
|
+
return "ok";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function usageSegments(
|
|
257
275
|
snapshot: UsageSnapshot,
|
|
258
276
|
options: { showResetTimes: boolean; showBankedResets?: boolean },
|
|
259
277
|
now = Date.now(),
|
|
260
|
-
):
|
|
278
|
+
): UsageSegment[] {
|
|
261
279
|
const windows = [
|
|
262
280
|
{
|
|
263
281
|
label: "5h",
|
|
@@ -276,24 +294,41 @@ export function formatUsageSnapshot(
|
|
|
276
294
|
(window) => window.percent !== null || window.resetSeconds !== null,
|
|
277
295
|
);
|
|
278
296
|
const displayedWindows = availableWindows.length > 0 ? availableWindows : windows;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
.
|
|
293
|
-
|
|
297
|
+
const segments: UsageSegment[] = [{ text: "Usage: ", severity: "muted" }];
|
|
298
|
+
displayedWindows.forEach((window, index) => {
|
|
299
|
+
if (index > 0) segments.push({ text: " · ", severity: "muted" });
|
|
300
|
+
segments.push({ text: `${window.label}: `, severity: "muted" });
|
|
301
|
+
segments.push({
|
|
302
|
+
text: formatPercent(window.percent),
|
|
303
|
+
severity: severityForLeftPercent(window.percent),
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
if (options.showResetTimes) {
|
|
307
|
+
for (const window of displayedWindows) {
|
|
308
|
+
const reset = formatCompactReset(
|
|
309
|
+
displayedWindows.length > 1 ? window.label : undefined,
|
|
310
|
+
remainingResetSeconds(window.resetSeconds, snapshot.capturedAt, now),
|
|
311
|
+
window.includeDate ? { includeDate: true } : undefined,
|
|
312
|
+
now,
|
|
313
|
+
);
|
|
314
|
+
if (reset) segments.push({ text: ` · ${reset}`, severity: "muted" });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
294
317
|
const banked =
|
|
295
318
|
options.showBankedResets === false ? null : formatBankedResetsSuffix(snapshot.bankedResets);
|
|
296
|
-
|
|
319
|
+
if (banked) segments.push({ text: ` · ${banked}`, severity: "muted" });
|
|
320
|
+
return segments;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Flat text form of {@link usageSegments}, used for status lines and notifications. */
|
|
324
|
+
export function formatUsageSnapshot(
|
|
325
|
+
snapshot: UsageSnapshot,
|
|
326
|
+
options: { showResetTimes: boolean; showBankedResets?: boolean },
|
|
327
|
+
now = Date.now(),
|
|
328
|
+
): string {
|
|
329
|
+
return usageSegments(snapshot, options, now)
|
|
330
|
+
.map((segment) => segment.text)
|
|
331
|
+
.join("");
|
|
297
332
|
}
|
|
298
333
|
|
|
299
334
|
export function formatBankedResetsSuffix(count: number | null): string | null {
|