@bacnh85/pi-sub 0.1.11 → 0.1.13
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 +19 -8
- package/extensions/index.ts +52 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,25 +34,29 @@ The footer status appears after Pi's built-in status/token usage line and includ
|
|
|
34
34
|
Example subscription line:
|
|
35
35
|
|
|
36
36
|
```text
|
|
37
|
-
(user@example.com) R:15%/2H W:20%/3D
|
|
37
|
+
(user@example.com) R:15%/2H W:20%/3D 42 tok/s
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
### OpenCode Go
|
|
41
41
|
|
|
42
|
-
OpenCode Go does not expose a public usage-window API, so the footer shows the active account/key label
|
|
42
|
+
OpenCode Go does not expose a public usage-window API, so the footer shows the active account/key label, accumulated session cost, and last response speed:
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
|
-
OpenCode Go (OpenCode Go key#1a2b3c4d) $0.23
|
|
45
|
+
OpenCode Go (OpenCode Go key#1a2b3c4d) $0.23 42 tok/s
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
### Z.ai
|
|
49
49
|
|
|
50
|
-
Z.ai (GLM Coding Plan) shows the active account/key label
|
|
50
|
+
Z.ai (GLM Coding Plan) shows the active account/key label, 5-hour rolling and weekly remaining quota with reset countdowns, and last response speed:
|
|
51
51
|
|
|
52
52
|
```text
|
|
53
|
-
(Z.ai key#1a2b3c4d) R:55%/2H W:80%/3D
|
|
53
|
+
(Z.ai key#1a2b3c4d) R:55%/2H W:80%/3D 42 tok/s
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
### Tokens per second
|
|
57
|
+
|
|
58
|
+
`pi-sub` tracks each response's tokens-per-second (tok/s) speed by measuring the time from provider request to message completion against the response's output token count. The last response's speed is shown in the footer next to usage data. The `/sub` detail view shows both the last response speed and the session-wide average.
|
|
59
|
+
|
|
56
60
|
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
57
61
|
|
|
58
62
|
## Commands
|
|
@@ -63,18 +67,23 @@ When the current model provider is not supported, `pi-sub` clears its subscripti
|
|
|
63
67
|
| `/sub status` | Same as `/sub`. |
|
|
64
68
|
| `/sub refresh` | Force a usage refresh, then show details. |
|
|
65
69
|
|
|
66
|
-
When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
|
|
70
|
+
When Pi OpenAI Codex auth is available, `/sub` shows the active account usage and speed:
|
|
67
71
|
|
|
68
72
|
```text
|
|
73
|
+
Provider: Codex · Model: o4-mini · Fetched: 14:23
|
|
74
|
+
Session cost: $0.12
|
|
75
|
+
Last response: 42 tok/s · Session avg: 39 tok/s
|
|
76
|
+
|
|
69
77
|
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
70
78
|
* user@example.com Plus 15%/2H 20%/3D Now
|
|
71
79
|
```
|
|
72
80
|
|
|
73
|
-
For OpenCode Go, `/sub` shows the provider/model, active account/key label,
|
|
81
|
+
For OpenCode Go, `/sub` shows the provider/model, active account/key label, session cost, and speed:
|
|
74
82
|
|
|
75
83
|
```text
|
|
76
84
|
Provider: OpenCode Go · Model: kimi-k2.6 · Fetched: 14:23
|
|
77
85
|
Session cost: $0.23
|
|
86
|
+
Last response: 42 tok/s · Session avg: 39 tok/s
|
|
78
87
|
|
|
79
88
|
ACCOUNT PLAN LAST ACTIVITY
|
|
80
89
|
------------------------------------------------------
|
|
@@ -83,10 +92,12 @@ Session cost: $0.23
|
|
|
83
92
|
OpenCode Go does not expose usage windows.
|
|
84
93
|
```
|
|
85
94
|
|
|
86
|
-
For Z.ai, `/sub` shows the rolling and weekly quota windows:
|
|
95
|
+
For Z.ai, `/sub` shows the rolling and weekly quota windows and speed:
|
|
87
96
|
|
|
88
97
|
```text
|
|
89
98
|
Provider: Z.ai · Model: glm-5.2 · Fetched: 14:23
|
|
99
|
+
Session cost: $0.05
|
|
100
|
+
Last response: 42 tok/s · Session avg: 39 tok/s
|
|
90
101
|
|
|
91
102
|
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
92
103
|
------------------------------------------------------------------------
|
package/extensions/index.ts
CHANGED
|
@@ -66,7 +66,6 @@ interface SubscriptionUsageSnapshot {
|
|
|
66
66
|
activeAccount?: SubscriptionAccountSnapshot;
|
|
67
67
|
fetchedAt: number;
|
|
68
68
|
error?: string;
|
|
69
|
-
cost?: number;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
type SubscriptionProviderAdapter = {
|
|
@@ -85,6 +84,11 @@ interface State {
|
|
|
85
84
|
inFlight?: Promise<SubscriptionUsageSnapshot>;
|
|
86
85
|
refreshTimer?: NodeJS.Timeout;
|
|
87
86
|
debounceTimer?: NodeJS.Timeout;
|
|
87
|
+
responseStartTime?: number;
|
|
88
|
+
lastTokPerSec?: number;
|
|
89
|
+
cumulativeOutput: number;
|
|
90
|
+
cumulativeDurationMs: number;
|
|
91
|
+
cumulativeCost: number;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
function isCodexModel(model: ModelLike): boolean {
|
|
@@ -209,9 +213,9 @@ function formatRemainingTime(resetAtSec: number | undefined): string | undefined
|
|
|
209
213
|
if (remainingSec <= 0) return "0M";
|
|
210
214
|
const remainingMin = Math.ceil(remainingSec / 60);
|
|
211
215
|
if (remainingMin < 60) return `${remainingMin}M`;
|
|
212
|
-
const remainingH = Math.ceil(
|
|
216
|
+
const remainingH = Math.ceil(remainingSec / 3600);
|
|
213
217
|
if (remainingH < 24) return `${remainingH}H`;
|
|
214
|
-
const remainingD = Math.ceil(
|
|
218
|
+
const remainingD = Math.ceil(remainingSec / 86400);
|
|
215
219
|
return `${remainingD}D`;
|
|
216
220
|
}
|
|
217
221
|
|
|
@@ -272,7 +276,7 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
|
272
276
|
async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
273
277
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
274
278
|
const entry = auth[CODEX_PROVIDER];
|
|
275
|
-
const accountId = getCodexAccountId(entry
|
|
279
|
+
const accountId = getCodexAccountId(entry);
|
|
276
280
|
if (!entry?.access || !accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
277
281
|
return { ...entry, accountId };
|
|
278
282
|
}
|
|
@@ -384,7 +388,7 @@ interface ZaiUsageApiResponse {
|
|
|
384
388
|
interface ZaiUsageApiError {
|
|
385
389
|
code: number;
|
|
386
390
|
msg: string;
|
|
387
|
-
success
|
|
391
|
+
success?: boolean;
|
|
388
392
|
}
|
|
389
393
|
|
|
390
394
|
function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
@@ -426,9 +430,11 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
|
|
|
426
430
|
const body = await response.json();
|
|
427
431
|
|
|
428
432
|
// Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
433
|
+
// Also handle missing success field, empty msg, or presence of code.
|
|
429
434
|
const apiError = body as ZaiUsageApiError;
|
|
430
|
-
if (typeof apiError.success === "boolean" && !apiError.success && apiError.msg) {
|
|
431
|
-
|
|
435
|
+
if (apiError.code >= 400 || (typeof apiError.success === "boolean" && !apiError.success) || (apiError.msg && apiError.msg.length > 0 && apiError.success === undefined)) {
|
|
436
|
+
const message = apiError.msg || `HTTP status ${apiError.code}`;
|
|
437
|
+
throw new Error(`Z.ai API error: ${message}`);
|
|
432
438
|
}
|
|
433
439
|
|
|
434
440
|
const parsed = body as ZaiUsageApiResponse;
|
|
@@ -477,9 +483,8 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
|
|
|
477
483
|
|
|
478
484
|
function formatRemaining(window: UsageWindow | undefined): string {
|
|
479
485
|
if (!window) return "?";
|
|
480
|
-
if (window.
|
|
486
|
+
if (window.remainingLabel) return `${window.remaining}%/${window.remainingLabel}`;
|
|
481
487
|
if (window.remaining !== undefined) return `${window.remaining}%`;
|
|
482
|
-
if (window.percent !== undefined && window.remainingLabel) return `${Math.max(0, 100 - window.percent)}%/${window.remainingLabel}`;
|
|
483
488
|
return "?";
|
|
484
489
|
}
|
|
485
490
|
|
|
@@ -499,16 +504,6 @@ function windowSegments(account: SubscriptionAccountSnapshot | undefined): strin
|
|
|
499
504
|
return segments;
|
|
500
505
|
}
|
|
501
506
|
|
|
502
|
-
function aggregateSessionCost(ctx: ExtensionContext): number {
|
|
503
|
-
let total = 0;
|
|
504
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
505
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
506
|
-
total += (entry.message.usage as any)?.cost?.total ?? 0;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return total;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
507
|
function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
513
508
|
if (!state.adapter) {
|
|
514
509
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -528,9 +523,10 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
|
528
523
|
const windowParts = windowSegments(account);
|
|
529
524
|
const accountPart = formatFooterAccount(account);
|
|
530
525
|
const segments = accountPart ? [accountPart, ...windowParts] : [...windowParts];
|
|
531
|
-
const cost =
|
|
526
|
+
const cost = state.cumulativeCost;
|
|
532
527
|
const hasWindows = windowParts.length > 0;
|
|
533
|
-
if (cost
|
|
528
|
+
if (cost > 0) segments.push(`$${cost.toFixed(2)}`);
|
|
529
|
+
if (state.lastTokPerSec !== undefined) segments.push(`${state.lastTokPerSec} tok/s`);
|
|
534
530
|
if (segments.length === 0) {
|
|
535
531
|
line = `Sub ${state.adapter.displayName}`;
|
|
536
532
|
} else if (!hasWindows) {
|
|
@@ -592,7 +588,6 @@ async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean)
|
|
|
592
588
|
renderSubscriptionLine(ctx, state);
|
|
593
589
|
state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
|
|
594
590
|
if (state.refreshGeneration !== generation) return snapshot;
|
|
595
|
-
snapshot.cost = aggregateSessionCost(ctx);
|
|
596
591
|
state.snapshot = snapshot;
|
|
597
592
|
state.lastRefreshAt = Date.now();
|
|
598
593
|
renderSubscriptionLine(ctx, state);
|
|
@@ -623,7 +618,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
623
618
|
if (!snapshot) return "Subscription usage has not been loaded yet.";
|
|
624
619
|
if (snapshot.error) return `${snapshot.providerDisplayName}: ${snapshot.error}`;
|
|
625
620
|
if (snapshot.accounts.length === 0) {
|
|
626
|
-
const costLine =
|
|
621
|
+
const costLine = state.cumulativeCost > 0 ? `\nSession cost: $${state.cumulativeCost.toFixed(2)}` : "";
|
|
627
622
|
const modelInfo = state.model?.id ? ` · Model: ${state.model.id}` : "";
|
|
628
623
|
return `Provider: ${snapshot.providerDisplayName}${modelInfo} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}\n${snapshot.providerDisplayName} does not expose usage windows.${costLine}`;
|
|
629
624
|
}
|
|
@@ -655,8 +650,14 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
655
650
|
return `${row.active} ${cols.join(" ")} ${row.snapshot.lastActivity ?? ""}`;
|
|
656
651
|
});
|
|
657
652
|
|
|
658
|
-
const costLine =
|
|
659
|
-
const
|
|
653
|
+
const costLine = state.cumulativeCost > 0 ? `\nSession cost: $${state.cumulativeCost.toFixed(2)}` : "";
|
|
654
|
+
const tokPerSecLine = state.lastTokPerSec !== undefined
|
|
655
|
+
? `\nLast response: ${state.lastTokPerSec} tok/s` +
|
|
656
|
+
(state.cumulativeDurationMs > 0
|
|
657
|
+
? ` · Session avg: ${Math.round(state.cumulativeOutput / (state.cumulativeDurationMs / 1000))} tok/s`
|
|
658
|
+
: "")
|
|
659
|
+
: "";
|
|
660
|
+
const lines = [`Provider: ${snapshot.providerDisplayName} · Model: ${state.model?.id ?? "unknown-model"} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}${costLine}${tokPerSecLine}`, "", header, sep, ...body];
|
|
660
661
|
if (!hasFiveHour && !hasWeekly) {
|
|
661
662
|
lines.push("", `${snapshot.providerDisplayName} does not expose usage windows.`);
|
|
662
663
|
}
|
|
@@ -664,7 +665,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
664
665
|
}
|
|
665
666
|
|
|
666
667
|
export default function (pi: ExtensionAPI) {
|
|
667
|
-
const state: State = { lastRefreshAt: 0, refreshGeneration: 0 };
|
|
668
|
+
const state: State = { lastRefreshAt: 0, refreshGeneration: 0, cumulativeOutput: 0, cumulativeDurationMs: 0, cumulativeCost: 0 };
|
|
668
669
|
|
|
669
670
|
pi.on("session_start", async (_event, ctx) => {
|
|
670
671
|
updateActiveAdapter(ctx, state, ctx.model);
|
|
@@ -676,7 +677,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
676
677
|
if (state.adapter) void refreshUsage(ctx, state, true);
|
|
677
678
|
});
|
|
678
679
|
|
|
679
|
-
pi.on("
|
|
680
|
+
pi.on("before_provider_request", async (_event, _ctx) => {
|
|
681
|
+
state.responseStartTime = Date.now();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
pi.on("message_end", async (event, ctx) => {
|
|
685
|
+
if (event.message.role === "assistant") {
|
|
686
|
+
state.cumulativeCost += (event.message.usage as any)?.cost?.total ?? 0;
|
|
687
|
+
if (state.responseStartTime) {
|
|
688
|
+
const output = (event.message.usage as any)?.output ?? 0;
|
|
689
|
+
const elapsed = Date.now() - state.responseStartTime;
|
|
690
|
+
state.responseStartTime = undefined;
|
|
691
|
+
if (elapsed > 0 && output > 0) {
|
|
692
|
+
state.lastTokPerSec = Math.round(output / (elapsed / 1000));
|
|
693
|
+
state.cumulativeOutput += output;
|
|
694
|
+
state.cumulativeDurationMs += elapsed;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (state.adapter) renderSubscriptionLine(ctx, state);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
pi.on("after_provider_response", async (event, ctx) => {
|
|
702
|
+
if (event.status >= 400) {
|
|
703
|
+
state.responseStartTime = undefined;
|
|
704
|
+
}
|
|
680
705
|
if (state.adapter) scheduleRefresh(ctx, state);
|
|
681
706
|
});
|
|
682
707
|
|