@bacnh85/pi-sub 0.1.12 → 0.1.14
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/extensions/index.ts +35 -49
- package/package.json +2 -2
package/extensions/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { readStoredCredential, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
|
|
@@ -28,8 +27,6 @@ type UsageApiSnapshot = {
|
|
|
28
27
|
plan_type?: string;
|
|
29
28
|
};
|
|
30
29
|
|
|
31
|
-
type PiAuthFile = Record<string, PiAuthEntry | undefined>;
|
|
32
|
-
|
|
33
30
|
type PiAuthEntry = {
|
|
34
31
|
type?: string;
|
|
35
32
|
access?: string;
|
|
@@ -66,7 +63,6 @@ interface SubscriptionUsageSnapshot {
|
|
|
66
63
|
activeAccount?: SubscriptionAccountSnapshot;
|
|
67
64
|
fetchedAt: number;
|
|
68
65
|
error?: string;
|
|
69
|
-
cost?: number;
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
type SubscriptionProviderAdapter = {
|
|
@@ -89,6 +85,7 @@ interface State {
|
|
|
89
85
|
lastTokPerSec?: number;
|
|
90
86
|
cumulativeOutput: number;
|
|
91
87
|
cumulativeDurationMs: number;
|
|
88
|
+
cumulativeCost: number;
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
function isCodexModel(model: ModelLike): boolean {
|
|
@@ -109,10 +106,6 @@ function piAuthPath(): string {
|
|
|
109
106
|
return path.join(configDir, "auth.json");
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
async function readJsonFile<T>(file: string): Promise<T> {
|
|
113
|
-
return JSON.parse(await fs.readFile(file, "utf8")) as T;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
109
|
function decodeJwtPayload(token: string | undefined): Record<string, any> | undefined {
|
|
117
110
|
if (!token) return undefined;
|
|
118
111
|
const parts = token.split(".");
|
|
@@ -213,9 +206,9 @@ function formatRemainingTime(resetAtSec: number | undefined): string | undefined
|
|
|
213
206
|
if (remainingSec <= 0) return "0M";
|
|
214
207
|
const remainingMin = Math.ceil(remainingSec / 60);
|
|
215
208
|
if (remainingMin < 60) return `${remainingMin}M`;
|
|
216
|
-
const remainingH = Math.ceil(
|
|
209
|
+
const remainingH = Math.ceil(remainingSec / 3600);
|
|
217
210
|
if (remainingH < 24) return `${remainingH}H`;
|
|
218
|
-
const remainingD = Math.ceil(
|
|
211
|
+
const remainingD = Math.ceil(remainingSec / 86400);
|
|
219
212
|
return `${remainingD}D`;
|
|
220
213
|
}
|
|
221
214
|
|
|
@@ -274,23 +267,20 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
|
274
267
|
}
|
|
275
268
|
|
|
276
269
|
async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const accountId = getCodexAccountId(entry!);
|
|
270
|
+
const entry = readStoredCredential(CODEX_PROVIDER, piAuthPath()) as PiAuthEntry | undefined;
|
|
271
|
+
const accountId = getCodexAccountId(entry);
|
|
280
272
|
if (!entry?.access || !accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
281
273
|
return { ...entry, accountId };
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
async function readOpenCodeGoAuth(): Promise<SubscriptionAccountSnapshot> {
|
|
285
|
-
const
|
|
286
|
-
const entry = auth[OPC_PROVIDER];
|
|
277
|
+
const entry = readStoredCredential(OPC_PROVIDER, piAuthPath()) as PiAuthEntry | undefined;
|
|
287
278
|
if (!entry?.key && !entry?.accountId) throw new Error("Missing opencode-go API key or accountId in Pi auth");
|
|
288
279
|
return authAccountSnapshot("OpenCode Go", entry, { plan: "Go" });
|
|
289
280
|
}
|
|
290
281
|
|
|
291
282
|
async function readZaiAuth(): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
292
|
-
const
|
|
293
|
-
const entry = auth[ZAI_PROVIDER];
|
|
283
|
+
const entry = readStoredCredential(ZAI_PROVIDER, piAuthPath()) as PiAuthEntry | undefined;
|
|
294
284
|
if (!entry?.key) throw new Error("Missing zai API key in Pi auth");
|
|
295
285
|
return { key: entry.key, account: authAccountSnapshot("Z.ai", entry) };
|
|
296
286
|
}
|
|
@@ -388,7 +378,7 @@ interface ZaiUsageApiResponse {
|
|
|
388
378
|
interface ZaiUsageApiError {
|
|
389
379
|
code: number;
|
|
390
380
|
msg: string;
|
|
391
|
-
success
|
|
381
|
+
success?: boolean;
|
|
392
382
|
}
|
|
393
383
|
|
|
394
384
|
function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
@@ -430,9 +420,11 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
|
|
|
430
420
|
const body = await response.json();
|
|
431
421
|
|
|
432
422
|
// Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
423
|
+
// Also handle missing success field, empty msg, or presence of code.
|
|
433
424
|
const apiError = body as ZaiUsageApiError;
|
|
434
|
-
if (typeof apiError.success === "boolean" && !apiError.success && apiError.msg) {
|
|
435
|
-
|
|
425
|
+
if (apiError.code >= 400 || (typeof apiError.success === "boolean" && !apiError.success) || (apiError.msg && apiError.msg.length > 0 && apiError.success === undefined)) {
|
|
426
|
+
const message = apiError.msg || `HTTP status ${apiError.code}`;
|
|
427
|
+
throw new Error(`Z.ai API error: ${message}`);
|
|
436
428
|
}
|
|
437
429
|
|
|
438
430
|
const parsed = body as ZaiUsageApiResponse;
|
|
@@ -481,9 +473,8 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
|
|
|
481
473
|
|
|
482
474
|
function formatRemaining(window: UsageWindow | undefined): string {
|
|
483
475
|
if (!window) return "?";
|
|
484
|
-
if (window.
|
|
476
|
+
if (window.remainingLabel) return `${window.remaining}%/${window.remainingLabel}`;
|
|
485
477
|
if (window.remaining !== undefined) return `${window.remaining}%`;
|
|
486
|
-
if (window.percent !== undefined && window.remainingLabel) return `${Math.max(0, 100 - window.percent)}%/${window.remainingLabel}`;
|
|
487
478
|
return "?";
|
|
488
479
|
}
|
|
489
480
|
|
|
@@ -503,16 +494,6 @@ function windowSegments(account: SubscriptionAccountSnapshot | undefined): strin
|
|
|
503
494
|
return segments;
|
|
504
495
|
}
|
|
505
496
|
|
|
506
|
-
function aggregateSessionCost(ctx: ExtensionContext): number {
|
|
507
|
-
let total = 0;
|
|
508
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
509
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
510
|
-
total += (entry.message.usage as any)?.cost?.total ?? 0;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return total;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
497
|
function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
517
498
|
if (!state.adapter) {
|
|
518
499
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -532,9 +513,9 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
|
532
513
|
const windowParts = windowSegments(account);
|
|
533
514
|
const accountPart = formatFooterAccount(account);
|
|
534
515
|
const segments = accountPart ? [accountPart, ...windowParts] : [...windowParts];
|
|
535
|
-
const cost =
|
|
516
|
+
const cost = state.cumulativeCost;
|
|
536
517
|
const hasWindows = windowParts.length > 0;
|
|
537
|
-
if (cost
|
|
518
|
+
if (cost > 0) segments.push(`$${cost.toFixed(2)}`);
|
|
538
519
|
if (state.lastTokPerSec !== undefined) segments.push(`${state.lastTokPerSec} tok/s`);
|
|
539
520
|
if (segments.length === 0) {
|
|
540
521
|
line = `Sub ${state.adapter.displayName}`;
|
|
@@ -597,7 +578,6 @@ async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean)
|
|
|
597
578
|
renderSubscriptionLine(ctx, state);
|
|
598
579
|
state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
|
|
599
580
|
if (state.refreshGeneration !== generation) return snapshot;
|
|
600
|
-
snapshot.cost = aggregateSessionCost(ctx);
|
|
601
581
|
state.snapshot = snapshot;
|
|
602
582
|
state.lastRefreshAt = Date.now();
|
|
603
583
|
renderSubscriptionLine(ctx, state);
|
|
@@ -628,7 +608,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
628
608
|
if (!snapshot) return "Subscription usage has not been loaded yet.";
|
|
629
609
|
if (snapshot.error) return `${snapshot.providerDisplayName}: ${snapshot.error}`;
|
|
630
610
|
if (snapshot.accounts.length === 0) {
|
|
631
|
-
const costLine =
|
|
611
|
+
const costLine = state.cumulativeCost > 0 ? `\nSession cost: $${state.cumulativeCost.toFixed(2)}` : "";
|
|
632
612
|
const modelInfo = state.model?.id ? ` · Model: ${state.model.id}` : "";
|
|
633
613
|
return `Provider: ${snapshot.providerDisplayName}${modelInfo} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}\n${snapshot.providerDisplayName} does not expose usage windows.${costLine}`;
|
|
634
614
|
}
|
|
@@ -660,7 +640,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
660
640
|
return `${row.active} ${cols.join(" ")} ${row.snapshot.lastActivity ?? ""}`;
|
|
661
641
|
});
|
|
662
642
|
|
|
663
|
-
const costLine =
|
|
643
|
+
const costLine = state.cumulativeCost > 0 ? `\nSession cost: $${state.cumulativeCost.toFixed(2)}` : "";
|
|
664
644
|
const tokPerSecLine = state.lastTokPerSec !== undefined
|
|
665
645
|
? `\nLast response: ${state.lastTokPerSec} tok/s` +
|
|
666
646
|
(state.cumulativeDurationMs > 0
|
|
@@ -675,7 +655,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
675
655
|
}
|
|
676
656
|
|
|
677
657
|
export default function (pi: ExtensionAPI) {
|
|
678
|
-
const state: State = { lastRefreshAt: 0, refreshGeneration: 0, cumulativeOutput: 0, cumulativeDurationMs: 0 };
|
|
658
|
+
const state: State = { lastRefreshAt: 0, refreshGeneration: 0, cumulativeOutput: 0, cumulativeDurationMs: 0, cumulativeCost: 0 };
|
|
679
659
|
|
|
680
660
|
pi.on("session_start", async (_event, ctx) => {
|
|
681
661
|
updateActiveAdapter(ctx, state, ctx.model);
|
|
@@ -692,20 +672,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
692
672
|
});
|
|
693
673
|
|
|
694
674
|
pi.on("message_end", async (event, ctx) => {
|
|
695
|
-
if (event.message.role === "assistant"
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
state.
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
675
|
+
if (event.message.role === "assistant") {
|
|
676
|
+
state.cumulativeCost += (event.message.usage as any)?.cost?.total ?? 0;
|
|
677
|
+
if (state.responseStartTime) {
|
|
678
|
+
const output = (event.message.usage as any)?.output ?? 0;
|
|
679
|
+
const elapsed = Date.now() - state.responseStartTime;
|
|
680
|
+
state.responseStartTime = undefined;
|
|
681
|
+
if (elapsed > 0 && output > 0) {
|
|
682
|
+
state.lastTokPerSec = Math.round(output / (elapsed / 1000));
|
|
683
|
+
state.cumulativeOutput += output;
|
|
684
|
+
state.cumulativeDurationMs += elapsed;
|
|
685
|
+
}
|
|
704
686
|
}
|
|
687
|
+
if (state.adapter) renderSubscriptionLine(ctx, state);
|
|
705
688
|
}
|
|
706
689
|
});
|
|
707
690
|
|
|
708
|
-
pi.on("after_provider_response", async (
|
|
691
|
+
pi.on("after_provider_response", async (event, ctx) => {
|
|
692
|
+
if (event.status >= 400) {
|
|
693
|
+
state.responseStartTime = undefined;
|
|
694
|
+
}
|
|
709
695
|
if (state.adapter) scheduleRefresh(ctx, state);
|
|
710
696
|
});
|
|
711
697
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-sub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Pi extension showing subscription usage for supported model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"node": ">=20.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": "
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=0.80.8 <0.81.0"
|
|
42
42
|
}
|
|
43
43
|
}
|